持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第14天,点击查看活动详情
启动Server
Server端存储模式:
- file:单机模式,全局事务会话信息内存中读写并持久化本地文件root.data,性能较高
- db:高可用模式,全局事务会话信息通过db共享,相应性能差些
- redis:性能较高,存在事务信息丢失风险,请提前配置合适当前场景的redis持久化配置。
下载包
建表
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_status` (`status`),
KEY `idx_branch_id` (`branch_id`),
KEY `idx_xid_and_branch_id` (`xid` , `branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
CREATE TABLE IF NOT EXISTS `distributed_lock`
(
`lock_key` CHAR(20) NOT NULL,
`lock_value` VARCHAR(20) NOT NULL,
`expire` BIGINT,
primary key (`lock_key`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
修改store.mode
seata-->conf-->application.yml,修改store.mode=db
修改数据库连接
模版在seata-->conf-->application.example.yml中:
store:
# support: file 、 db 、 redis
mode: db
db:
datasource: druid
db-type: mysql
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata?rewriteBatchedStatements=true
user: root
password: root
min-conn: 5
max-conn: 100
global-table: global_table
branch-table: branch_table
lock-table: lock_table
distributed-lock-table: distributed_lock
query-limit: 100
max-wait: 5000
启动
进入seata bin目录:
cd /xxx/seata/bin/
执行命令
./seata-server.sh -h 127.0.0.1 -p 8091 -m db
需要注意
mysql版本和mysql-connector-java 驱动jar包版本的匹配。笔者mysql版本是8.0.29,启动后如下:
搜下seata-->lib下的mysql-connector-java会发现在mysql文件夹下seata为你准备:
明显不匹配,自己下载mysql-connector-java-8.0.29.jar放在lib目录下即可。
业务系统集成-AT
添加依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2.2.2.RELEASE</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.5.2</version>
</dependency>
undo_log表新建、配置参数(仅AT模式)
- ddl
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
- 参数
- registry.type:注册中心类型,默认file,支持file 、nacos 、eureka、redis、zk、consul、etcd3、sofa、custom
- config.type:配置中心类型,默认file,支持file、nacos 、apollo、zk、consul、etcd3、custom
- service.vgroupMapping.my_test_tx_group:事务群组,my_test_tx_group为分组,配置项值为TC集群名
事务分组是seata的资源逻辑,类似于服务实例。在file.conf中的my_test_tx_group就是一个事务分组。事务分组的配置项值就是TC集群名称,去相应注册中心拉取相应服务名的服务列表,获取真实的TC服务列表。
这样设计的目的是事务分组可以作为资源的逻辑隔离单位,当发生故障时可以快速failover
- service.default.grouplist:TC服务列表,仅注册中心为file时使用
可以配置多个,配置多个意味着集群。不推荐使用,不是真正的注册中心,不具服务的健康检查机制当tc不可用时无法自动剔除列表,推荐使用nacos 、eureka、redis、zk、consul、etcd3、sofa。registry.type=file或config.type=file 设计的初衷是让用户再不依赖第三方注册中心或配置中心的前提下,通过直连的方式,快速验证seata服务。
- service.disableGlobalTransaction:全局事务开关,默认false。false为开启,true为关闭
以下是demo配置:
seata:
tx-service-group: my_test_tx_group
service:
vgroup-mapping:
my_test_tx_group: default
grouplist:
default: 127.0.0.1:8091
数据源代理
我们使用了seata-starter,默认的data-source-proxy-mode为AT,默认开启enable-auto-data-source-proxy。
初始化GlobalTransactionScanner
自动:引入了seata-spring-boot-starter、spring-cloud-starter-alibaba-seata会自动注入。
实现xid跨服务传递
自动:springCloud用户可以引入spring-cloud-starter-alibaba-seata,内部已经实现xid传递
使用
不管是发起方还是被调用方都需要添加依赖、undo_log表新建、配置参数。
发起方
package com.study.seata.controller;
import com.study.seata.service.SeataService;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SeataController {
@Autowired
private SeataService seataService;
@RequestMapping("/testSeata")
@GlobalTransactional
public void testSeata() {
seataService.testSeata();
int i = 1 / 0;
}
}
package com.study.seata.service.impl;
import com.study.common.entity.UserDo;
import com.study.common.service.UserService;
import com.study.ordinary.api.TestRemote;
import com.study.seata.service.SeataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service("seataService")
public class SeataServiceImpl implements SeataService {
@Autowired
private UserService userService;
@Resource
private TestRemote testRemote;
@Override
@Transactional(rollbackFor = Exception.class)
public void testSeata() {
UserDo userUpdate = new UserDo();
userUpdate.setId(1L);
userUpdate.setUserName("waer");
userService.updateById(userUpdate);
UserDo userUpdate1 = new UserDo();
userUpdate1.setId(2L);
userUpdate1.setUserName("waer");
userService.updateById(userUpdate1);
testRemote.testSeata();
}
}
简单例子。需要注意的是笔者这里还是使用了本地事务,因为需要保证本地事务,不能因为有全局事务就丢弃了本地事务,此外因为AT模式全局事务不具备隔离性事务过程中的数据会被其他客户端读到,为了避免脏读需要加for update,但是性能不高,所以还是把本地事务加上。
调用方
package com.study.ordinary.api;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
@FeignClient(name = "${spring.application.name}",url = "http://localhost:${server.port}")
public interface TestRemote {
@RequestMapping("/test")
void testSeata();
}
package com.study.ordinary.controller;
import com.study.ordinary.api.TestRemote;
import com.study.common.entity.TestDo;
import com.study.common.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController implements TestRemote {
@Autowired
private TestService testService;
@Override
public void testSeata() {
TestDo testUpdate = new TestDo();
testUpdate.setId(6L);
testUpdate.setName("wff");
testService.updateById(testUpdate);
}
}
简单的例子,对test表进行插入。
观察下与seata相关的表的内容
seata-服务端
global_table
CREATE TABLE `global_table` (
`xid` varchar(128) NOT NULL,
`transaction_id` bigint DEFAULT NULL,
`status` tinyint NOT NULL,
`application_id` varchar(32) DEFAULT NULL,
`transaction_service_group` varchar(32) DEFAULT NULL,
`transaction_name` varchar(128) DEFAULT NULL,
`timeout` int DEFAULT NULL,
`begin_time` bigint DEFAULT NULL,
`application_data` varchar(2000) DEFAULT NULL,
`gmt_create` datetime DEFAULT NULL,
`gmt_modified` datetime DEFAULT NULL,
PRIMARY KEY (`xid`),
KEY `idx_status_gmt_modified` (`status`,`gmt_modified`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
因为官方没有注释,只能根据例子猜测下。
- 例子:
xid transaction_id status application_id transaction_service_group transaction_name timeout begin_time application_data gmt_create gmt_modified
192.168.2.147:8091:6917850988250068680 6917850988250068680 1 java-2022-seata my_test_tx_group testSeata() 60000 1667119746694 2022-10-30 08:49:06 2022-10-30 08:49:06
- xid:全局事务的唯一标识,用于传播事务。seata服务ip+端口+transaction_id。
- transaction_id:事务id。
- status:这里的1表示全局事务开始,具体见官方文档。
- application_id:应用id。
- transaction_service_group:事务服务分组。
- transaction_name:事务名。这里是被@GlobalTransactional注释的方法。
- timeout:事务超时时间,可以使@GlobalTransactional.timeoutMills指定。
branch_table
CREATE TABLE `branch_table` (
`branch_id` bigint NOT NULL,
`xid` varchar(128) NOT NULL,
`transaction_id` bigint DEFAULT NULL,
`resource_group_id` varchar(32) DEFAULT NULL,
`resource_id` varchar(256) DEFAULT NULL,
`branch_type` varchar(8) DEFAULT NULL,
`status` tinyint DEFAULT NULL,
`client_id` varchar(64) DEFAULT NULL,
`application_data` varchar(2000) DEFAULT NULL,
`gmt_create` datetime(6) DEFAULT NULL,
`gmt_modified` datetime(6) DEFAULT NULL,
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
- 例子
branch_id xid transaction_id resource_group_id resource_id branch_type status client_id application_data gmt_create gmt_modified
6917850988250068682 192.168.2.147:8091:6917850988250068680 6917850988250068680 jdbc:mysql://localhost:3306/my_new_test AT 0 java-2022-ordianry:127.0.0.1:62247 {"autoCommit":false} 2022-10-30 08:49:08.727008 2022-10-30 08:49:08.727008
6917850988250068684 192.168.2.147:8091:6917850988250068680 6917850988250068680 jdbc:mysql://localhost:3306/my_test AT 0 java-2022-seata:127.0.0.1:62411 {"autoCommit":false} 2022-10-30 08:49:08.808038 2022-10-30 08:49:08.808038
- branch_id:分支id
- xid:全局事务的唯一标识,用于传播事务。seata服务ip+端口+transaction_id。
- transaction_id
- resource_group_id:资源id
- resource_id:mysql url
- branch_type:分支事务模式。
- status:分支状态。这里为0表示未知。
- client_id:客户端id,name+ip+端口。
- application_data:应用数据,比如使用了tcc
{
"actionContext": {
"action-start-time": 1667651422889,
"useTCCFence": true,
"sys::prepare": "testSeata",
"sys::rollback": "rollback",
"sys::commit": "commit",
"host-name": "198.18.0.1",
"params": {
"test": "test"
},
"actionName": "seataService"
}
}
lock_table
CREATE TABLE `lock_table` (
`row_key` varchar(128) NOT NULL,
`xid` varchar(128) DEFAULT NULL,
`transaction_id` bigint DEFAULT NULL,
`branch_id` bigint NOT NULL,
`resource_id` varchar(256) DEFAULT NULL,
`table_name` varchar(32) DEFAULT NULL,
`pk` varchar(36) DEFAULT NULL,
`status` tinyint NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
`gmt_create` datetime DEFAULT NULL,
`gmt_modified` datetime DEFAULT NULL,
PRIMARY KEY (`row_key`),
KEY `idx_status` (`status`),
KEY `idx_branch_id` (`branch_id`),
KEY `idx_xid_and_branch_id` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
- 例子:
row_key xid transaction_id branch_id resource_id table_name pk status gmt_create gmt_modified
jdbc:mysql://localhost:3306/my_new_test^^^test^^^6 192.168.2.147:8091:6917850988250068680 6917850988250068680 6917850988250068682 jdbc:mysql://localhost:3306/my_new_test test 6 0 2022-10-30 08:49:08 2022-10-30 08:49:08
jdbc:mysql://localhost:3306/my_test^^^user^^^1 192.168.2.147:8091:6917850988250068680 6917850988250068680 6917850988250068684 jdbc:mysql://localhost:3306/my_test user 1 0 2022-10-30 08:49:08 2022-10-30 08:49:08
jdbc:mysql://localhost:3306/my_test^^^user^^^2 192.168.2.147:8091:6917850988250068680 6917850988250068680 6917850988250068684 jdbc:mysql://localhost:3306/my_test user 2 0 2022-10-30 08:49:08 2022-10-30 08:49:08
- row_key:行key。这里为mysql url+表名+主键
- xid:全局事务的唯一标识,用于传播事务。seata服务ip+端口+transaction_id。
业务-客户端
CREATE TABLE `undo_log` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'increment id',
`branch_id` bigint NOT NULL COMMENT 'branch transaction id',
`xid` varchar(100) NOT NULL COMMENT 'global transaction id',
`context` varchar(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` longblob NOT NULL COMMENT 'rollback info',
`log_status` int NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` datetime NOT NULL COMMENT 'create datetime',
`log_modified` datetime NOT NULL COMMENT 'modify datetime',
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8mb3 COMMENT='AT transaction mode undo table';
随便看一个就行。
- 例子:
id branch_id xid context rollback_info log_status log_created log_modified
26 6917850988250068682 192.168.2.147:8091:6917850988250068680 serializer=jackson&compressorType=NONE {"@class":"io.seata.rm.datasource.undo.BranchUndoLog","xid":"192.168.2.147:8091:6917850988250068680","branchId":6917850988250068682,"sqlUndoLogs":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.undo.SQLUndoLog","sqlType":"UPDATE","tableName":"test","beforeImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords","tableName":"test","rows":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Row","fields":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"id","keyType":"PRIMARY_KEY","type":-5,"value":["java.math.BigInteger",6]},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"name","keyType":"NULL","type":1,"value":"111"}]]}]]},"afterImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords","tableName":"test","rows":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Row","fields":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"id","keyType":"PRIMARY_KEY","type":-5,"value":["java.math.BigInteger",6]},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"name","keyType":"NULL","type":1,"value":"wff"}]]}]]}}]]} 0 2022-10-30 08:49:09 2022-10-30 08:49:09
- branch_id:分支id。
- context:内容,包含了一些技术信息。这里为
serializer=jackson&compressorType=NONE - rollback_info:回滚信息
{
"@class": "io.seata.rm.datasource.undo.BranchUndoLog",
"xid": "192.168.2.147:8091:6917850988250068680",
"branchId": 6917850988250068682,
"sqlUndoLogs": ["java.util.ArrayList", [{
"@class": "io.seata.rm.datasource.undo.SQLUndoLog",
"sqlType": "UPDATE",
"tableName": "test",
"beforeImage": {
"@class": "io.seata.rm.datasource.sql.struct.TableRecords",
"tableName": "test",
"rows": ["java.util.ArrayList", [{
"@class": "io.seata.rm.datasource.sql.struct.Row",
"fields": ["java.util.ArrayList", [{
"@class": "io.seata.rm.datasource.sql.struct.Field",
"name": "id",
"keyType": "PRIMARY_KEY",
"type": -5,
"value": ["java.math.BigInteger", 6]
}, {
"@class": "io.seata.rm.datasource.sql.struct.Field",
"name": "name",
"keyType": "NULL",
"type": 1,
"value": "111"
}]]
}]]
},
"afterImage": {
"@class": "io.seata.rm.datasource.sql.struct.TableRecords",
"tableName": "test",
"rows": ["java.util.ArrayList", [{
"@class": "io.seata.rm.datasource.sql.struct.Row",
"fields": ["java.util.ArrayList", [{
"@class": "io.seata.rm.datasource.sql.struct.Field",
"name": "id",
"keyType": "PRIMARY_KEY",
"type": -5,
"value": ["java.math.BigInteger", 6]
}, {
"@class": "io.seata.rm.datasource.sql.struct.Field",
"name": "name",
"keyType": "NULL",
"type": 1,
"value": "wff"
}]]
}]]
}
}]]
}
里面有xid、branchId、sqlType(这里为UPDATE)、tableName(这里为test)、beforeImage(记录了提交前的数据状态,只记录会变化的值)、afterImage(记录了提交后的数据状态,只记录会变化的值),利用这些值生成回滚sql。
业务系统集成-TCC
AT分布式事务缺点在《Seata之AT初识》说过,一个比较明显的是基于本地ACID事务的关系型数据库,比如我们常用的插入数据库之后更新缓存,就不能适用,我们自己处理的话就是在catch中写回退逻辑,但是不够优雅,就可以利用tcc。
tcc给笔者的感觉就像编程范式。
package com.study.seata.controller;
import com.study.seata.service.SeataService;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
@RestController
public class SeataController {
@Autowired
private SeataService seataService;
@RequestMapping("/testSeata")
@GlobalTransactional
public void testSeata() {
seataService.testSeata(new HashMap<String, String>() {{
put("test", "test");
}});
}
}
package com.study.seata.service;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
import java.util.Map;
@LocalTCC
public interface SeataService {
@TwoPhaseBusinessAction(name = "seataService", commitMethod = "commit", rollbackMethod = "rollback")
void testSeata(@BusinessActionContextParameter(paramName = "params") Map<String, String> params);
void commit(BusinessActionContext context);
void rollback(BusinessActionContext context);
}
package com.study.seata.service.impl;
import com.alibaba.fastjson.JSON;
import com.study.common.entity.UserDo;
import com.study.common.service.UserService;
import com.study.common.api.TestRemote;
import com.study.seata.service.SeataService;
import io.seata.rm.tcc.api.BusinessActionContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.Map;
@Service("seataService")
public class SeataServiceImpl implements SeataService {
@Autowired
private UserService userService;
@Resource
private TestRemote testRemote;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
@Transactional(rollbackFor = Exception.class)
public void testSeata(Map<String, String> params) {
UserDo userUpdate = new UserDo();
userUpdate.setId(1L);
userUpdate.setUserName("waer");
userService.updateById(userUpdate);
UserDo userUpdate1 = new UserDo();
userUpdate1.setId(2L);
userUpdate1.setUserName("waer");
userService.updateById(userUpdate1);
testRemote.test();
stringRedisTemplate.opsForValue().set("test", "1");
int i = 1 / 0;
}
@Override
public void commit(BusinessActionContext context) {
System.out.println("ddd:" + JSON.toJSONString(context));
}
@Override
public void rollback(BusinessActionContext context) {
stringRedisTemplate.delete("test");
}
}
这里其实就是AT+TCC,利用tcc做AT干不了的事情。