数据库事务
在编程的世界里,数据非常重要,数据库担任了很重要的角色,数据库拥有的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<String> 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服务来管理,并不会影响库存的归还,因此也能保证最终一致性
网友评论