基于本地消息表实现分布式事务

210 阅读6分钟

背景

我们有一个服务叫做安全培训服务,这个服务需要在安全培训计划审批通过的时候推送到第三方的课程中心。大致的流程如下:

数据推送的过程是在两个不同的服务、两个不同的库中进行的,这个过程至少会遇到两个问题:

  1. 数据一致性问题。安全培训业务数据修改(安全培训计划审批状态修改为审批通过)和课程中心服务保存安全培训计划,没办法原子性的完成,即:没办法保证要么一起成功,要么一起失败。
  2. 性能问题。安全培训业务数据修改和数据推送同步进行的话,接口性能受限于数据推送服务是一个外部提供的远程调用接口,接口稳定性和可靠性无法保证,可能出现因为课程中心服务不可用导致的服务调用超市,从而拖垮安全培训服务。

本地消息表实现分布式事务

标准方案

本地消息表实现分布式事务的思路如下:

  1. 安全培训服务修改本地业务数据的同时写入一条状态为“处理中”的消息到本地消息表中(上图1、2两步),这两个操作需要保证在同一个事务内进行;
  2. 将本地事务处理成功的消息投递到MQ中(上图第3步)
  3. 课程中心服务订阅安全培训服务本地事务处理成功的消息(上图第4步),当收到消息后开始处理本地事务,将安全培训计划保存到数据库中(上图第5步)
  4. 课程中心服务处理完本地事务后,把处理成功的消息投递到MQ中(上图第6步)
  5. 安全培训服务订阅课程培训中心保存安全培训计划成功的消息(上图第7步),当收到消息后,将当前消息在本地消息表中更新为“已成功”(上图第8步)
  6. 调度任务中心会定时捞取消息表中状态为“处理中”的消息(上图第9步),重新投递到MQ中让课程培训中心重新消费(上图第10步),在这个过程中作为消费者的课程培训中心需要保证消息的幂等性。

异常情况分析

序号安全培训服务处理业务数据消息表SUBMIT_QUEUE课程培训中心处理业务数据ACC_QUEUE说明
1失败----安全培训服务回滚
2成功失败---安全培训服务回滚
3成功成功失败--调度任务中心定时调度重发消息
4成功成功成功失败-MQ自动重试,注意接口幂等
5成功成功成功成功失败调度任务中心定时调度重发消息,注意接口幂等
6成功成功成功成功成功数据最终一致
7成功成功超时超时超时消息超时未被处理,死信补偿

依次解释下上述七种情况

  1. 第1、2两种情况比较好理解,安全培训服务回滚事务即可
  2. 第3种情况是因为安全培训服务向MQ投递消息时失败或者课程中心服务未成功消费消息导致的,此时依靠调度任务中心定时调度重发消息到MQ可以处理
  3. 第4种情况是因为课程中心服务本地事务异常导致的,此时可以依靠MQ自动重试保证消息被消费,此时消费者课程中心服务需要考虑接口幂等
  4. 第5种情况是因为消费者往MQ投递消息时失败或者安全培训服务未成功消费消息导致的,此时课程培训中心没有调度任务作为补偿,因此只能再等待安全培训服务的调度任务定时触发,从而再次投递消息给MQ
  5. 第6种情况即是我们期待的情况
  6. 第7种情况是因为MQ、课程培训中心服务不可用,也未响应失败的信息,从而导致安全培训服务中堆积了大量状态为“处理中”的消息,此时可以将这些长期未处理成功的消息投递到一张死信表中,进行手工补偿。

此外,调度任务中心定时调度重发消息,对于每一条消息不应该无无限制的重发,需要统计消息的重发次数,业务上需要定一个阈值,达到阈值后可以当作死信进行手工补偿处理。

优缺点

优点

  1. 可靠性高:将消息持久化和本地事务进行原子提交,可以保证消息的可靠性;
  2. 使用范围广:基于本地消息表实现分布式事务,可以满足不同的业务场景;
  3. 可扩展性好:将消息的发送和本地事务的执行分开,可以提高系统的扩展性

缺点

  1. 本地消息表与业务耦合在一起,定时扫描消息表可能影响核心业务对数据库的读写
  2. 实现负责度高:使用这种方式实现分布式事务,需要使用MQ、定时任务等组件,增加了系统的维护成本,同时,需要考虑异常操作和补偿操作,复杂度也会有所增加

一些思考

消息的幂等保证

安全培训服务和课程培训中心服务保证消息幂等

安全培训服务需要保证消息不要重复推送,课程培训中心服务要保证消息不要重复消费,这两个服务保证消息的幂等的方式相对容易。安全培训服务可以提前查询一下课程培训中心服务中是否存在要推送的数据,不存在再进行推送,课程培训中心服务也可以提前查下自己的数据库中是否存在推送过来的数据,不存在再进行处理。最后,兜底的考虑是,课程培训中心需要在数据库层面增加唯一索引保证数据的唯一性。

调度任务中心保证消息幂等

调度任务中心需要保证消息不要被重复扫描推送,调度任务触发扫表任务时,安全培训服务可以通过加分布式锁,锁定某一段数据,只将某一段数据进行推送。加锁的key可以是:服务实例编号+数据id范围,例如:service:001_msgid:1-1000。如果加锁失败,可以尝试增加另一个数据段的锁,例如:service:001_msgid:1001-1001。这里考虑到安全培训服务可能有多个实例,如果多个实例同时被调度任务调到,应该是不同的实例处理不同段的数据。这里加锁的key也需要根据具体的场景来设置,数据id未必是连续的,甚至某一个id范围内的数据也未都是需要推送的。