基于数据库的事务消息解决分布式事务方案
转载请注明出处:http://www.cnblogs.com/lizo/p/8516502.html
概述
当单库已不能支撑当前业务的时候,我们往往都考虑进行分库(横向拆分或者纵向拆分)。但分库有个无法回避的问题,就是事务问题。网上有很多分布式事务解决方案,例如XA,TCC等,但是最常用,也是改造成本最低就是使用最终一致性来保证分布式事务。
比较常用的就是使用消息中间件(RabbitMq,RocketMq),通过事务消息来解决最终一致性。参考https://zhuanlan.zhihu.com/p/25933039?utm_source=tuicool&utm_medium=referral。
本篇文章将使用数据库的来达到最终一致性的实现方案。
名词解释
- 主库-拆分前,业务访问的数据库
- 分库-拆分后,部分业务数据放入到分库中
注:以下有些内容是在使用事务消息(无论是基于数据库还是基于消息队列)应该考虑的地方。
基于数据库的事务消息
事务消息
所谓基于数据库的事务消息,其实很好理解,就是在数据库中创建一个类似消息队列的表,用于保存事务消息。在拆分前,一个事务中,有多个主库的数据操作。如下图,
但是在拆分数据库后,有业务被拆分到分库中去了,这样,原有的单库事务被打破,但是通过把拆分出去的业务使用一个事务消息来代替(事务消息表也是在主库中,所以这里还是单库事务),后续再通过其他方式去执行该事务消息所对应的业务逻辑即可,这样,就可以达到最终一致性,如下图
事务消息执行器
前面说到了,事务消息需要一个处理器来进行执行事务消息所对应的业务逻辑。事务处理器应该是顺序的去读取并执行的。
设想一个场景:当出现某一条消息处理失败,如果执行器要等当前消息执行成功才继续往后执行(甚至该消息永远不会处理成功),那么会影响后续消息的执行,导致整个系统出现问题。
因此,消息处理器即要保证消息处理尽可能处理快,又能保证消息最终能执行成功。 在消息执行器中必须设置2个任务:
- 第一个任务,消息处理任务,已最快的速度执行消息,如果消息处理失败了,跳过该消息继续执行后面的消息。
- 第二个任务,消息校验任务,这个任务就是顺序检查消息,保证所有消息都执行成功,如果失败,进行重试,多次重试失败以后发出告警以让人工介入处理。 如下图
注:上图左边那个是消息队列及其处理状态
消息执行的特性
- 延迟处理性。消息不是实时处理的,而是用过消息执行器来异步执行的。因此,如果在原有逻辑中,需要特别注意后续流程对该消息处理结果是不是有实时依赖性(例如后续业务逻辑中会使用该消息处理结果来做一些计算等)。
- 处理无序性。由于消息不一定是顺序执行的,所有保证即使后生成的消息先执行,也不能出现问题。
- 最终成功性。对每条插入的消息,保证该条消息一定要能执行成功
如何确认消息已执行成功
设想,如果分库业务执行成功(更新分库),然后去更新消息状态(主库),这样,又是一个夸库事务,所以,得想其他办法来避免,最简单的方法,就是在分库里面也建一个消息表,保存处理的成功的消息。这样,通过对比主库和分库的消息表,就知道哪些事务消息没有执行成功
消息处理器基本框架
前面介绍了,消息处理器的核心功能就:
- 获取消息,并把消息发送给业务放处理
- 保证消息执行的成功
为了完成上面功能,需要消息处理任务和消息校验任务,通过定时调度任务来触发这2个任务(例如,5s触发一次)
消息处理任务
消息处理任务就是通过扫描待处理的消息,然后通知业务系统执行。
再次强调,消息处理任务不会管消息是否执行成功。都是按照消息队列表顺序执行下去。
消息校验任务
校验任务就是比较主库和分库中的消息记录(主库中记录的所有消息,分库中记录的执行成功的消息),对执行未成功的消息发起重试,如果多次重试失败则发出告警,需要人工介入。
和基于消息中间件的事务消息比较
相同点
- 都是采用异步确保最终一致性:
- 可以控制异步执行消息的速率,可以利用RPC调用的负载均衡
- 消息处理都必须支持重试和幂等性
- 事务消息异步执行失败,都没办法回滚产生事务消息的事务
不同点
消息事务的提交
使用消息中间件,一般都需要在代码中显示的编写提交中间件事务消息的代码,类似下面
public boolean transaction(String text){ try { 发送事务消息 执行本地事务 提交事务消息 return true; } catch (TmcException e) { return false; } }
但在实际项目中,事务的传播性的问题(spring 的事务注解是支持事务的传播性),就需要修改业务代码。但使用基于数据库的消息队列就没有这个问题
@Transactional public void publishAS(String text){ 执行本地事务逻辑 插入事务消息 }
所以在既有代码改造上(特别是复杂系统中),使用数据库的事务消息可以减少代码的改动
不需要回调check
我们知道,在使用消息中间件的时候,都需要实现一个回调接口,当事务消息长时间没有commit的时候,会调用该接口来确认是否需要commit(例如发送消息成功,但是在commit的时候网络不可用)。而基于数据局的事务消息队列就没有这个问题
更多的数据库访问资源
基于数据库的事务消息也有一个比较明显的缺点:
- 占用更多的数据库空间和数据库访问资源
- 需要额外编写DAO层代码
小结
基于数据库和基于消息队列的事务消息的基本思路都一样,使用最终一致性来避免分布式事务带来的额外系统复杂性和代码开销。基于数据库的事务消息在既有业务改造中,代码变动较小,也不需要额外的引入消息中间件,但是带来的问题就是对数据库更多的访问。而基于消息中间件的问题就是如何避免在与消息中间件交互的出现问题的时候如何应对。当然,以上只是我个人理解,如果系统有什么设计不合理或者有改进的地方,欢迎讨论。