首先,我们说下事务,严格意义的事务实现应该具有4个属性: 原子性,一致性,隔离性,持久性。这四个属性通常称为ACID特性。
原子性: 指一个事务不可分割,要么成功,要么失败,不能有一般成功一半失败的情况。
一致性: 指这些数据在事务执行完成这个时间点之前,读到的一定是更新前的数据,之后读到的一定是更新后的数据,
不应该存在一个时刻,让用户读到更新过程中的数据。
隔离性: 指一个书屋的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对正在进行的其他事务是隔离的,并发执行
的各个事务之间不能互相干扰。
持久性: 指一个事务一旦完成提交,后续的其它操作和故障不会对事务的结果产生任何影响。
大部分传统的单体关系型数据库都完整的实现了ACID,但是,对于分布式系统来说,严格的实现ACID这四个特性几乎是
不可能的,或者说实现代价太大,大到我们无法接受。
分布式事务就是要在分布式系统中的实现事务。在分布式系统中,在保证可用性和不严重牺牲性能的前提下,光是要实现数据
的一致性就已经非常困难了,所以出现了很多"残血版"的一致性,比如顺序一致性,最终一致性等等。
显然实现严格的分布式事务是更加不可能完成的任务。所以,目前大家说的分布式事务,更多情况下,是在分布式系统中事务
的不完整实现。在不同的应用场景中,有不同的实现,目的都是通过一些妥协来解决实际问题。
在实际应用中,比较常见的分布式事务实现有2PC(Two-phase Commit, 也叫二阶段提交),TCC(Try-Confirm-Cancel)和
事务消息。每一种实现都有其特定的使用场景,也有各自的问题,都不是完美的解决方案。
事务消息适用的场景主要是那些需要异步更新数据,并对数据实时性要求不太高的场景。比如在电商的案例里,在创建订单
后,如果出现短暂的几秒,购物车里的商品没有被及时清空,也不是完全不可接受的,只要最终购物车的数据和订单数据
保持一致就可以了。
事务消息需要消息队列提供相应的功能才能实现分布式事务(不完整的),Kafka和RocketMQ都提供了事务相关功能
首先,看上图,订单系统在消息队列上开启一个事务。然后订单系统给消息服务器发送一个"半消息",这个半消息不是说
消息内容不完整,它包含的内容就是完整的消息内容,半消息和普通消息的唯一区别是,在事务提交之前,对于消费者
来说,这个消息是不可见的。
半消息发送成功后,订单系统就可以执行本地事务了,在订单库中创建一条订单记录,并提交订单库的数据库事务。然后
根据本地事务的执行结果决定提交或者回滚事务消息。如果订单创建成功,那就提交事务消息,购物车系统就可以消费
这条消息继续后续的流程。如果订单创建失败,那就回滚事务消息,购物车系统就不会收到这条消息。这样就基本实现了
“要么都成功,要么都失败”的一致性要求。
如果第四步提交事务消息时失败了怎么办?
Kafka的解决方案比较简单粗暴,直接抛出异常,让用户自行处理。我们可以在业务代码中反复重试提交,直到提交成功,
或者删除之前创建的订单进行补偿。
RocketMQ的解决方案是增加了事务反查机制。我们定义Producer是订单系统,在提交或者回滚消息时发生网络异常,
RocketMQ的Broker没有收到提交或者回滚的请求,Broker会定期去Producer上反查这个事务对应的本地事务的状态,
然后根据反查结果决定提交或者回滚这个事务。
为了支撑这个事务反查机制,我们的业务代码需要实现一个反查本地事务状态的接口,告知RocketMQ本地事务时成功还是
失败。
在这里,反查本地事务的逻辑非常简单,我们只需要根据消息中的订单ID,在订单库中查询这个订单是否存在即可。
RocketMQ会自动根据事务反查的结果提交或者回滚事务消息。
这个反查本地事务的实现,并不依赖消息的发送方,也就是订单服务的某个实例节点上的任何数据。这种情况下,即使
是发送事务消息的那个订单服务节点宕机了,RocketMQ依然可以通过其他订单服务的节点来执行反查,确保事务的
完整性。