聊聊分布式事务

数据库事务

在编程的世界里,数据非常重要,数据库担任了很重要的角色,数据库拥有的ACID特性,我们只管声明事务,通过sql对数据库进行批量操作,就能够达到目标,其背后是数据库做了很多工作,帮我们处理了很多异常,比如数据库机器断电,如何保证强一致性呢,原来是数据库会有两个文件,数据库文件和日志文件,调用方法开启事务,执行的sql,都会存储在日志文件中,捕捉到调用方发起的commit命令后,才将日志中存储的事务sql执行到数据库,在调用方提交事务后,机器断电的情况下,这是事务sql已经存在于日志文件中,在机器重启后,通过检查日志文件,对已提交状态但未完成的事务进行后续处理,提取事务sql恢复执行来保证强一致性。

XA Transaction

在单机环境下,通过数据库事务,我们就能很完美的解决一致性的问题。随着系统访问量的上升,单机数据库慢慢出现性能瓶颈,这是会对单体服务进行拆分,同时对数据库也进行拆分,伴随着垂直分库,进入到分布式领域,出现了新的问题。原先要执行的业务操作sql都是在一个数据库中完成,但如今却分布在不同的物理库上,无法通过原有的事务来处理,数据库厂商引入了2pc理论(两阶段提交)即XA,引入协调者,参与者事务,在开启全局事务时,协调者会锁住整个事务,现在各个分库执行precommit预提交,协调者检测到所有的commit都通过后,通知各个分库执行commit,这种方式有个致命缺点,会锁住整个事务,这期间相应的表都不能访问,随着并发量的上升,性能会急剧下降。这是通过牺牲一定的可用性来换去一致性的做法

MQ消息

以订单扣库存生成订单为例

//业务逻辑
try{
  discontStock(skuId, quantity);
  saveOrderDb();
}catch(Execption e){
  try{
    returnStock(skuId, quantity);
  }catch(e){
    try{
      sendReturnStockMsg();
    }catch(e){
      saveReturnStockMsgToDeadLetter();
    }
  }
}
//通过定时任务进行消息重发
task.ScanDeadLetterForRePushMsg()

缺点:

  • 1.订单在下单失败后,需要干很多杂活,下单是关键业务,会影响损耗一部分性能
  • 2.订单服务在下单失败,catch回滚资源阶段宕机了,这个情况下,不能保证数据一致性,需要人工介入修依赖方数据

补偿事务TCC

为解决性能问题,引入了事务的补偿机制,和XA正好相反,着力于提高可用性,属于3pc,根据CAP和BASE原理,通过Try、Confirm、Cancel来实现,核心思想是为 每个操作都注册一对确认和取消操作。在整个try成功则执行各系统的confirm方法,失败,则执行各系统的cancel方法。只要try成功,confirm阶段一定成功,会通过重试来保证:

  • try阶段: 业务检查和预留系统资源
  • confirm阶段:try成功,就会开始执行confirm,并且通过重试保证confirm一定能成功,这里不允许出错
  • cancel阶段:try阶段出错后执行cancel,并且通过重试保证cancel逻辑,一定能被调用成功。

基于tcc_transaction框架二次开发来适应业务

  • 参考:https://github.com/changmingxie/tcc-transaction/tree/master
  • 原理:通过在try方法的接口上添加注解Compensable,注解信息提供confirm和cancel方法调用位置,使用aop拦截try方法,提取compensable注解信息,完成为事务,注册确认和取消操作的操作。在事务发起方的try阶段完成后,aop根据 try阶段是否抛异常来判定进入事务confirm or cancel阶段,完成状态流转
  • 场景分析:目标方法调用前 做事务状态存储,如try阶段,事务存储成功,目标方法执行前宕机,会由事务发起方进行rollback操作,此时要求目标机器在 confirm和cancel阶段实现业务幂等;
  • 优点:
    • 提供了对场景各种传播特性的支持: required require_new supports mandatory
  • 缺点:
    • 1.要求事务参与方,提供事务的存储方式,还需要在理解的原理的基础上进行配置(比如:配置当前参与者事务的存储位置,手动依赖spring相关的xml配置),相对复杂;
    • 2.事务管理操作完全依赖于业务系统的 aop逻辑,会给业务系统造成一定性能损耗,而且在业务系统宕机时,会中断事务管理,虽然业务重启也能恢复,但会延长事务数据流转到最终态的时间;
    • 3.要求tcc三阶段操作的入参相同,使用起来不太灵活。

针对上述问题,进行了二次开发

改进:

  • 通过一个tcc服务来做tcc事务管理,aop的功能弱化为在try执行后调用tcc服务上传try阶段的状态
  • 调整confirm 和cancel方法参数为 transId\branchId,由事务参与方自己维护,业务参数到 transId\branchId的映射
  • 简化为只在try方法调用之后才通过aop调用tcc服务进行事务的上传操作,事务状态维护交由tcc服务来管理
  • 增强:添加熔断降级逻辑,假如调用tcc服务上传try阶段状态失败了,先尝试重试几次,记录失败次数,到达一定次数(150次)了触发熔断,直接通过aop去进行参与者事务的confirm or cancel逻辑调用(有损)

实现:

@Description("事务实体类")
public class Entity extends BaseEntity implements Serializable {
    @Description("id")
    public long id;
    @Description("分布式事务事务id")
    public String transId;
    @Description("分布式事务事务分支id")
    public String branchId;
    @Description("分布式事务步骤id")
    public String stepId;
    @Description("分布式事务服务名")
    public String serviceName;
    @Description("分布式事务失败方法名")
    public String cancelName;
    @Description("分布式事务成功方法名")
    public String confirmName;
    @Description("是否成功")
    public boolean flag;
    @Description("分布式事务调用方法")
    public List invokeList = new CopyOnWriteArrayList<>();
    @Description("分布式事务是否操作")
    public boolean isTccOperator;
    @Description("分布式事务校验码")
    public String checkSum;
    @Description("分布式事务操作异常信息")
    public String errorMsg;
    @Description("分布式事务创建时间")
    public Date date;
}
  • 1.实现一个aop拦截注解Compensable,获取try对应的confirm和cancel方法,以及try调用结果,提交给tccService管理
  • 2.tccService 在接受到参与者事务后,状态保存到redis,在检测到事务发起方的try阶段提交结果后,判断走整个事务走confirm or cancel逻辑,confirm or cancel逻辑由tcc服务发起dubbo泛化调用来完成,每完成一组confirm or cancel调用,立即更新redis参与者事务的状态。
  • 3.实现一个定时任务,用redis scan扫描未完成的事务,拉取到服务中,检测事务状态整个事务中有出现参与者在try阶段失败,走cancel逻辑,没有参与者失败,走confirm逻辑,在完成后,完成事务统计和添加redis事务key前缀为completed_, 这样做的原因是,redis是单线程执行的,可以避免执行scan命令过长,影响性能。

可用性分析:

  • 1.在事务执行过程中,tccService(redis)宕机了,在重启时,通过定时任务用ScheduledThreadPoolExecutor,用redis scan扫描未完成的事务,拉取到服务中,检测事务状态整个事务中有出现参与者在try阶段失败,走cancel逻辑,没有参与者失败,走confirm逻辑,通过这样的手段保证数据的最终一致性
  • 2.业务系统有一台宕机了,因为状态已经上传到redis中,并且通过tcc服务来管理,并不会影响库存的归还,因此也能保证最终一致性

你可能感兴趣的:(聊聊分布式事务)