分布式事务解决方案(一)Seata集成和使用
前言
什么是Seata?
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双11,对各BU业务进行了有力的支撑。经过多年沉淀与积累,商业化产品先后在阿里云、金融云进行售卖。2019.1 为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源,未来 Seata 将以社区共建的形式帮助其技术更加可靠与完备。
官网 https://seata.io/zh-cn/
架构图
准备工作
准备业务场景
准备并启动nacos服务 nacos官网
为了实现分布式事务的场景需要先搭建好一些微服务并注册到nacos
订单服务 - 负责创建订单,减库存,扣余额
@GetMapping("/createOrder")
@GlobalTransactional
public String createOrder(){
log.info(Thread.currentThread().getName()+":下单");
UserOrder entity = new UserOrder();
entity.setMoney(100L);
entity.setOrderNo(System.currentTimeMillis()+"");
orderService.save(entity); //下订单
String result = storeService.countDown(1L);//减库存
log.info("减库存:"+result);
String result1 = userService.spendMoney(1L);//扣钱
log.info("扣钱:"+result1);
return "ok";
}
库存服务 - 扣减库存
@PostMapping("/countDown/{id}")
public String countDown(@PathVariable Long id){
log.info(Thread.currentThread().getName()+":减库存!");
GoodsStore byId = goodsStoreService.getById(id);
byId.setCount(byId.getCount()-10);
if(byId.getCount()<0){
throw new RuntimeException("库存不足");
}
boolean res = goodsStoreService.updateById(byId);
if(res){
log.info(Thread.currentThread().getName()+":减库存成功");
}else{
log.error(Thread.currentThread().getName()+":减库存失败");
}
return "ok";
}
用户服务 - 扣减账户余额
@GetMapping("/money/spend/{id}")
public String spendMoney(@PathVariable Long id){
log.info("配置文件:"+name);
log.info(Thread.currentThread().getName()+":扣钱");
QueryWrapper<SysUserMoney> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(SysUserMoney::getUserId,id);
SysUserMoney one = userMoneyService.getOne(wrapper);
one.setMoney(one.getMoney()-200);
if(one.getMoney()<0){
throw new RuntimeException("余额不足!");
}
boolean res = userMoneyService.updateById(one);
if(res){
log.info(Thread.currentThread().getName()+":更新余额成功");
}else{
log.error(Thread.currentThread().getName()+":更新余额失败");
}
return "ok";
}
业务场景:订单服务在下订单的时候会分别调用 库存服务扣减库存,用户服务扣减余额。任意一个服务调用失败都将进行事务回滚。
搭建Seata的TC Server
下载:
https://seata.io/zh-cn/blog/download.html
点击下载
下载解压修改配置
1.修改seata/seata-server-1.4.2/conf/registry.conf
# 注册中心
registry {
# 注册中心类型 file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-tc-server"
serverAddr = "192.168.31.46:8848"
group = "DEFAULT_GROUP" #nacos中的服务分组, 跟订单,仓促,用户服务同一个组
namespace = "3920cea4-c7cc-42e2-a818-3423ec169157" #NACOS 的命名空间ID
cluster = "CD" #集群名称
username = "nacos"
password = "nacos"
}
}
# 配置中心 同上
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "192.168.31.46:8848"
namespace = "3920cea4-c7cc-42e2-a818-3423ec169157"
group = "DEFAULT_GROUP"
username = "nacos"
password = "nacos"
dataId = "seata-tc-server.properties"
}
}
添加Nacos配置
seata-tc-server.properties 内容如下
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://192.168.31.60:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=123456
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
建库建表
创建MySQL数据库: seata
创建表:
-- -------------------------------- 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_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- 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 = utf8;
-- 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`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
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);
启动TC 服务
[root@localhost bin]# pwd
/usr/local/seata/seata-server-1.4.2/bin
[root@localhost bin]# ll
总用量 12
-rwxr-xr-x. 1 502 games 3685 4月 25 2021 seata-server.bat
-rwxr-xr-x. 1 502 games 4212 4月 25 2021 seata-server.sh
[root@localhost bin]# nohup ./seata-server.sh >log.out 2>1 &
到这里 Seata-tc-server 启动完成了
集成Seata
集成到每个参与事务的服务
引入依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!-- 排除 1.3.0 的依赖 改为1.4.2的依赖包 -->
<exclusion>
<artifactId>seata-spring-boot-starter</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
修改配置:
application.yaml
建议复制粘贴,手写容易打错字。
# seata 分布式事务配置
seata:
application-id: ${spring.application.name}
tx-service-group: my-group # 事务组,根据这个获取集群名称
registry:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.server-addr}
group: DEFAULT_GROUP
namespace: "3920cea4-c7cc-42e2-a818-3423ec169157"
username: nacos
password: nacos
application: seata-tc-server
service:
vgroup-mapping: # 事务组与TC服务 集群的映射关系
my-group: CD
data-source-proxy-mode: XA # 默认是AT模式
启动我们的三个服务
2022-01-06 18:58:52.159 INFO 2552 --- [ main] i.s.c.r.netty.NettyClientChannelManager : will connect to 192.168.31.56:8091
2022-01-06 18:58:52.162 INFO 2552 --- [ main] i.s.c.rpc.netty.RmNettyRemotingClient : RM will register :jdbc:mysql://192.168.31.60:3306/storecenter
2022-01-06 18:58:52.176 INFO 2552 --- [ main] i.s.core.rpc.netty.NettyPoolableFactory : NettyPool create channel to transactionRole:RMROLE,address:192.168.31.56:8091,msg:< RegisterRMRequest{resourceIds='jdbc:mysql://192.168.31.60:3306/storecenter', applicationId='store-service', transactionServiceGroup='my-group'} >
2022-01-06 18:58:53.181 INFO 2552 --- [ main] i.s.c.rpc.netty.RmNettyRemotingClient : register RM success. client version:1.4.2, server version:1.4.2,channel:[id: 0x8b1b6db5, L:/192.168.31.53:61915 - R:/192.168.31.56:8091]
2022-01-06 18:58:53.201 INFO 2552 --- [ main] i.s.core.rpc.netty.NettyPoolableFactory : register success, cost 171 ms, version:1.4.2,role:RMROLE,channel:[id: 0x8b1b6db5, L:/192.168.31.53:61915 - R:/192.168.31.56:8091]
org.apache.ibatis.session.defaults.DefaultSqlSessionFactory@362b384c
通过日志是能够看到RM 成功注册到了 seata-tc-server 服务器上了。
测试
通过 @GlobalTransactional 注解配置事务的入口
正常提交
先测试正常的情况, 余额充足。
初始数据:
浏览器发起请求
再次查看数据
整个过程是处于正常的情况
创建了订单, 减了库存10, 扣了余额200
后台日志
2022-01-06 20:58:36.396 INFO 14052 --- [nio-8080-exec-2] i.seata.tm.api.DefaultGlobalTransaction : Begin new global transaction [192.168.31.56:8091:2459182619839676463]
2022-01-06 20:58:36.397 INFO 14052 --- [nio-8080-exec-2] c.r.d.o.controller.IndexController : http-nio-8080-exec-2:下单
2022-01-06 20:58:37.036 INFO 14052 --- [nio-8080-exec-2] c.r.d.o.controller.IndexController : 减库存:ok
2022-01-06 20:58:37.403 INFO 14052 --- [nio-8080-exec-2] c.r.d.o.controller.IndexController : 扣钱:ok
2022-01-06 20:58:37.513 INFO 14052 --- [ch_RMROLE_1_3_8] i.s.c.r.p.c.RmBranchCommitProcessor : rm client handle branch commit process:xid=192.168.31.56:8091:2459182619839676463,branchId=2459182619839676465,branchType=XA,resourceId=jdbc:mysql://192.168.31.60:3306/ordercenter,applicationData=null
2022-01-06 20:58:37.514 INFO 14052 --- [ch_RMROLE_1_3_8] io.seata.rm.AbstractRMHandler : Branch committing: 192.168.31.56:8091:2459182619839676463 2459182619839676465 jdbc:mysql://192.168.31.60:3306/ordercenter null
2022-01-06 20:58:37.637 INFO 14052 --- [ch_RMROLE_1_3_8] i.s.rm.datasource.xa.ResourceManagerXA : 192.168.31.56:8091:2459182619839676463-2459182619839676465 was committed.
2022-01-06 20:58:37.638 INFO 14052 --- [ch_RMROLE_1_3_8] io.seata.rm.AbstractRMHandler : Branch commit result: PhaseTwo_Committed
2022-01-06 20:58:38.672 INFO 14052 --- [nio-8080-exec-2] i.seata.tm.api.DefaultGlobalTransaction : Suspending current transaction, xid = 192.168.31.56:8091:2459182619839676463
2022-01-06 20:58:38.674 INFO 14052 --- [nio-8080-exec-2] i.seata.tm.api.DefaultGlobalTransaction : [192.168.31.56:8091:2459182619839676463] commit status: Committed
异常回滚
当在次发起请求由于余额低于200 会出现扣减余额失败的情况
查看数据库的数据
订单未产生,库存未变更, 余额也未扣减。 说明事务已经回滚了。
查看日志
Seata 事务模式
XA
优点:
事务强一致性,满足ACID原则
常用数据库都支持,实现简单,没有代码侵入
缺点:
因为一阶段需要锁定数据库资源,等待第二阶段结束才释放,性能较差 牺牲了可用性, 保证一致性
依赖关系型数据库实现事务。
AT (默认)
优点:
1阶段直接提交,记录undo_log 日志 01 事务提交之前的日志
2阶段提交,先记录日志02 进行对比,如果和提交之前的一致那么就删日志, 事务完成。
如果二阶段提交时发现和之前的日志不一致(不一致的原因,其他事务非Seata 管理的事务对数据进行了操作),则抛异常, 需要人工介入
2阶段回滚,先记录日志02 和日志01进行对比,如果和提交之前的一致那么就使用日志进行回滚在删除日志, 事务完成。
如果二阶段回滚时发现和之前的日志不一致(不一致的原因,其他事务非Seata 管理的事务对数据进行了操作),则抛异常, 需要人工介入
不需要等待
引入了全局锁 多个事务并发提交的时候需要获取表数据对应的全局锁,针对死锁的情况,如果超过时间未获取到全局锁则释放当前持有的锁,并回滚数据。
没有代码上的侵入,每个参与事务的服务需要单独建表。
缺点:
两阶段之间属于软状态,属于最终一致性。
框架的快照会影响数据库性能,但比XA模式要好很多
默认是AT 模式, 需要在参与事物的服务数据库创建表
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 = utf8 COMMENT ='AT transaction mode undo table';
TCC模式和 SAGA模式
这两种模式需要人工手写业务逻辑代码对事务回滚事件做补偿的业务逻辑。 这里就没有做实现了。 感兴趣的小伙伴百度一下。
AT 模式与XA模式的区别
XA 模式一阶段不提交事务 锁定资源 ,AT 模式一阶段直接提交不锁定资源
XA 模式依赖数据库回滚机制实现回滚,AT 模式利用数据快照实现数据回滚
XA 强一致,AT 最终一致