分布式事务-消息事务的实现
在企业快速发展的阶段,我们经常需要进行分库分表,一旦从单一的数据库分为多个库以后,原本一个事务就可以处理的内容,需要在不同的库操作多个事务才能完成,这就涉及到常说的分布式事务。
市面上常用的分布式事务方案有大概以下几种:
- 2PC
- TCC
- 基于MQ的消息事务
- 事务状态表
这里主要讲基于MQ的消息事务,因为这是实现相对简单的一种。
普通消息
我们在日常业务中经常使用MQ消息系统,来做一些异步的事情,比如用户下了一个订单,我们使用异步的方式给用户发送一条短信通知用户下单成功。
简易流程如下:
大致的消息发送如下:
c端用户下单后,在系统中将数据写入DB1,然后发送消息到消息中间件,最后通过消费给用户发送短信。
这个看似简单的消息模式,如何实现分布式事务乃?
这个需要从分布式事务的一个基本问题说起
一个基本问题
分布式事务,要保证的在分布式的事务执行一样具有ACID,要么都成功,要么都不成功。
但是这种保证几乎是不可能的,我们无法去实现强一致性,但是可以考虑最终一致性,过程虽然不是一致的,但等一段时间后,最终保持了一致就可以,这种最终一致性在分布式系统中有大量的运用。
现在的大量系统都逐渐在采用微服务的方式,微服务模式下都是基于服务的调用,很少有能直接操作数据库的,所以这种分布式事务方式在这种情况下就很受用。
衍生新问题
虽然我们可以采用这种消息事务的方式,但是还是存在一个问题,就是写入数据库和发送消息的问题:
假如,我们先写入数据库,后发送消息,如果在发送消息的时候,系统崩溃了,后续的系统就无法执行内容,这个情况怎么办?
又假如,我们先发送消息,后写入数据库,如果消息发送成功了,写入数据库的时候发生了错误回滚,已经发出去的消息又怎么办?
再假如,我们先发送消息,后写入数据库,返回消息发送失败,我们就默认为就失败了,可是消息发送失败在http情况下是消息中间件给我们返回应答的时候失败还是消息本身没有发送成功的失败,我们是很难判断的。
所以无论是先发送消息,还是先写数据库都无法解决这个问题。
但是如果我们再把这个分布式事务再延迟一点乃? 我们给要发送的消息增加一个消息表,在写入数据库的时候,就可以使用事务完成同时写入数据表和消息表,然后通过后台程序去跑这个消息表,将消息表的数据发送到MQ消息队列。
于是就有了以下方案
实现-消息表方式
幂等情况
通过上图我们可以看到
- 事务同时写业务表和消息表
- 定时任务或者后台程序将消息表数据发送到消息中间件
既然是基于消息中间件实现的分布式事务,那么久无法避免一个问题,就是消息重复。
比如消费端实际消费了消息,但是在ACK回复的时候程序崩溃了,消息中间件没有收到ACK回复,这个消费其实是完成了的,当消费端重启的时候又会去消费这条消息。
所以这种情况下就需要做到幂等,这样当再次消费的时候发现消息是消费成功了,直接ACK应答成功就行了。
- 消费消息,判断是否已经消费过了(幂等)
- 如果没有消费过,写入数据
- ACK回复
非幂等情况
当然,我们也有可能由于某些业务因素,无法直接实现幂等,那么我们就需要像消息表一样,实现一个判重表。实现逻辑思路是一样的。 先将消息写入判重表,然后通过事务同时写入数据和更新判重表状态就行了。
实现-RocketMQ消息事务
由于这种模式的优点,为了简化消息发送的消息表,RocketMQ实现了一个消息事务。原理为发送消息分为两个阶段。
- 发送prepare准备消息
- 发送comfirm确认消息
我们只发送了prepare消息,如果没有发送comfirm消息,那么这条消息就不会真正的执行,只有RocketMQ收到了comfirm消息,才会真正的执行。
那么新的问题来了,只要是发送给消息中间件的消息就由可能存在失败,如果comfirm发送失败了怎么办?
RocketMQ实现了一个检测机制,会每隔一段时间去检测这些prepare消息是否需要执行,但是这个需要业务端实现一个回调接口,RocketMQ调用这个接口看是否需要执行comfirm还是取消这条消息。
这种方式虽然少了消息表,但是增加了多余的接口,其实也没有减少非常多的工作量。
幂等情况
非幂等情况
如果消费无法做到幂等,那么就只能通过其他方式去实现幂等。
最后
即使是基于这种消息分布式事务,还是难免会出现各种异常情况,所以最好还是需要有兜底方案。
而最为直接的兜底方案其实就是“对账”,我们卖商品的时候都会做每日盘点、每月盘点,其实就是一种对账方式。分布式事务也需要做对账,通过系统检测,如果发现不一致,然后人工的去修复。这样才能尽较大的可能保证我们的数据是最终一致的。