29| SpringCloudAlibaba微服务整合分布式事务Seata实战
新版本-分布式事务框架Seata 服务端部署安装最佳实践
-
基于AT模式
- 创建undo_log表, 每个库都需要
- https://seata.apache.org/zh-cn/docs/dev/mode/at-mode/
sql-- 注意此处0.7.0+ 增加字段 context CREATE TABLE `undo_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `branch_id` bigint(20) NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int(11) NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
-
下载部署Seata的TC服务端
- https://seata.apache.org/zh-cn/unversioned/download/seata-server
- 我们下载1.3就行,不要下载最新的(除非对新的很有把握不出问题)
-
Linux/Mac/Windows服务器安装
- 解压
- 修改jvm内存(默认是2g,防止内存不够)
- ./seata-server.sh 启动,默认是8091端口(记得防火墙开放端口,也可以nohup守护进程启动)
-
TC需要存储全局事务和分支事务的记录,支持三种存储模式
- file模式 (默认):性能高, 适合单机模式,在内存中读写,并持久化到本地文件中
- 在 bin/sessionStore/root.data文件
- db模式 :性能较差,适合tc集群模式
- redis模式:性能教高,适合tc集群模式
- file模式 (默认):性能高, 适合单机模式,在内存中读写,并持久化到本地文件中
-
问题:
- seata 在 JDK11下运行报错
- 解决: 下载下来的seata 默认没有存放日志文件的目录, 手动创建seata/logs/seata_gc.log 目录和文件
SpringCloudAlibaba微服务整合Seata分布式事务框架
用户注册同时发放注册优惠券
- common项目添加依赖
- 出现的问题:no available service 'null' found, please make sure registry config correct
- 安装的服务端版本必须要和你客户端的版本保持一样
xml
<!--alibaba微服务整合分布式事务,上面的方式不行 mvn 包冲突-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!--alibaba微服务整合分布式事务,这个方式才行-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<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.3.0</version>
</dependency>
- 各个微服务配置文件修改:配置规则
- tx-service-group对应的值:
项目名-group
, 与vgroup-mapping下对应一致 - grouplist对应的值: 与最后xx-group对应一致
- tx-service-group对应的值:
yaml
#nla-user-service服务配置seata
seata:
tx-service-group: ${spring.application.name}-group
service:
grouplist:
nla: 127.0.0.1:8091
vgroup-mapping:
nla-user-service-group: nla
#nla-coupon-service服务配置seata
seata:
tx-service-group: ${spring.application.name}-group
service:
grouplist:
nla: 127.0.0.1:8091
vgroup-mapping:
nla-coupon-service-group: nla
- 注释掉全局异常
java
//@ControllerAdvice
//public class CustomExceptionHandler ..
- 开发测试接口
User服务中通过FeignClient来调用新用户注册发放优惠券接口
java
@FeignClient(name = "nla-coupon-service")
public interface CouponFeignService {
@PostMapping("/cop/coupon/v1/new_user_coupon")
JsonData addNewUserCoupon(@RequestBody NewUserCouponRequest newUserCouponRequest);
}
补充用户注册:初始化福利信息
java
/**
* 用户注册,初始化福利信息
*
* @param userEntity 用户对象信息
*/
private void userRegisterInitTask(UserEntity userEntity) {
log.info("初始化福利信息 TODO... {}", userEntity);
NewUserCouponRequest request = new NewUserCouponRequest();
request.setName(userEntity.getName());
request.setUserId(userEntity.getId());
JsonData jsonData = couponFeignService.addNewUserCoupon(request);
// if(jsonData.getCode()!=0){
// throw new RuntimeException("发放优惠券异常");
// }
log.info("发放新用户注册优惠券:{},结果:{}",request.toString(),jsonData.toString());
}
用户组成添加异常:模拟异常
java
@Override
@GlobalTransactional
public JsonData register(UserRegisterRequest registerRequest) {
// ...
//新用户注册成功,初始化信息,发放福利等
userRegisterInitTask(userEntity);
//模拟异常
int b = 1/0;
return JsonData.buildSuccess();
}
- 测试分布式事务
- TM入口service的方法增加 @GlobalTransactional 注解,同时在启动类开启事务注解@EnableTransactionManagement
Seata分布式事务undo_log表-分析AT模式执行机制回顾
-
AT
-
AT模式可以应对大多数的业务场景,并且基本可以做到无业务入侵、开发者无感知
-
用户只需关心自己的 业务SQL. AT 模式分为两个阶段,可以认为是2PC
- 一阶段:执行用户SQL
javaSeata 会拦截“业务 SQL”,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据 在业务数据更新之后,再将其保存成“after image”,最后生成行锁 以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性
- 二阶段:
Seata
框架自动生成提交或者回滚
二阶段提交: 因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将阶段一保存的快照数据和行锁删掉,完成数据清理即可。
二阶段回滚: 还原业务数据, 回滚方式便是用“before image”还原业务数据;
但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”
如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理 -
-
undo_log表的rollback_info字段
json
{"@class":"io.seata.rm.datasource.undo.BranchUndoLog","xid":"192.168.0.115:8091:107926818928988160","branchId":107926819939815424,"sqlUndoLogs":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.undo.SQLUndoLog","sqlType":"INSERT","tableName":"user","beforeImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords$EmptyTableRecords","tableName":"user","rows":["java.util.ArrayList",[]]},"afterImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords","tableName":"user","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",26]},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"name","keyType":"NULL","type":12,"value":null},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"pwd","keyType":"NULL","type":12,"value":"34234234"},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"head_img","keyType":"NULL","type":12,"value":null},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"slogan","keyType":"NULL","type":12,"value":null},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"sex","keyType":"NULL","type":-6,"value":1},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"points","keyType":"NULL","type":4,"value":0},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"create_time","keyType":"NULL","type":93,"value":["java.sql.Timestamp",[1614166959000,0]]},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"mail","keyType":"NULL","type":12,"value":"2343223@qq.com"},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"secret","keyType":"NULL","type":12,"value":"111"}]]}]]}}]]}
全局异常下微服务整合Seata分布式事务失效解决方案
- 问题: 微服务场景下,配置了统一全局异常处理,导致seata在AT模式下无法正常回滚问题
- 如果使用Feign 配置了容错类(fallback)或者容错工厂(fallbackFactory),也是一样的问题
- 原因:服务A调用服务B, 服务B发生异常,由于全局异常处理的存在(@ControllerAdvice), seata 无法拦截到B服务的异常,从而导致分布式事务未生效
- 解决思路
java
配置了全局异常处理,所以rpc一定会有返回值, 所以在每个全局事务方法最后, 需要判断rpc是否发生异常
发生异常则抛出 RuntimeException或者子类
- 方式一:RPC接口不配置全局异常
- 方式二:利用AOP切面解决
- 方式三:程序代码各自判断RPC响应码是否正常,再抛出异常
高并发下分布式事务下的总结和思考
- 分布式事务解决方案很多,XA的2PC、TCC、MQ事务消息等
- 框架也有Seata, 同时支持多种方式模式
- 重点
java
不管选哪一种方案,在项目中应用都要谨慎再思考,除特定的数据强一致性场景外,能不用尽量就不要用
因为无论它们性能如何优越,一旦项目链路加入分布式事务,整体效率会几倍的下降,在高并发情况下弊端尤为明显
- 任何多链路的操作,换个方案或者换个思路,可以避免使用分布式事务(接下去大课就是)
- 下单商品库存锁定
- 下单优惠券记录锁定
- 总之
- 分布式事务和分布式锁一样,能不用就不用
- 实在要用,使用优先是 柔性事务,实在无法满足再考虑 刚性事务
- 分布式锁也是,尽量降低锁的粒度
评论区