29| SpringCloudAlibaba微服务整合分布式事务Seata实战

新版本-分布式事务框架Seata 服务端部署安装最佳实践

  • 基于AT模式

    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服务端

  • Linux/Mac/Windows服务器安装

    • 解压
    • 修改jvm内存(默认是2g,防止内存不够)
    • ./seata-server.sh 启动,默认是8091端口(记得防火墙开放端口,也可以nohup守护进程启动)
  • TC需要存储全局事务和分支事务的记录,支持三种存储模式

    • file模式 (默认):性能高, 适合单机模式,在内存中读写,并持久化到本地文件中
      • 在 bin/sessionStore/root.data文件
    • db模式 :性能较差,适合tc集群模式
    • redis模式:性能教高,适合tc集群模式
  • 问题:

    • 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对应一致
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
    java 复制代码
    Seata 会拦截“业务 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 复制代码
不管选哪一种方案,在项目中应用都要谨慎再思考,除特定的数据强一致性场景外,能不用尽量就不要用
因为无论它们性能如何优越,一旦项目链路加入分布式事务,整体效率会几倍的下降,在高并发情况下弊端尤为明显
  • 任何多链路的操作,换个方案或者换个思路,可以避免使用分布式事务(接下去大课就是)
    • 下单商品库存锁定
    • 下单优惠券记录锁定
  • 总之
    • 分布式事务和分布式锁一样,能不用就不用
    • 实在要用,使用优先是 柔性事务,实在无法满足再考虑 刚性事务
    • 分布式锁也是,尽量降低锁的粒度

评论区

评论系统加载区域 (例如 Disqus, Utterances 等)