本文节选自《软件架构设计:大型网站技术架构与业务架构融合之道》一书,余春龙著,由电子工业出版社博文视点出版,已获得授权。架构是一种综合能力,而不是某一方面的技能。也正因为如此,本书提供的是一个全面的解决方案、方法论、成体系的设计思维。从基础技术谈起,之后到高层技术,再到业务、管理,提供一个架构能力的全局视图,从而诠释一个架构师的完整能力模型。
本文从实践角度总结了解决分布式事务问题,比较可靠的七种方法:两种最终一致性的方案,两种妥协办法,两种基于状态 + 重试 + 幂等的方法(TCC,状态机+重试+幂等),还有一种对账方法。
在实现层面,妥协和对账的办法最容易,最终一致性次之,TCC最复杂。
<以下为正文>
数据一致性问题非常多样,下面举一些常见例子。比如在更新数据的时候,先更新了数据库,后更新了缓存,一旦缓存更新失败,此时数据库和缓存数据会不一致。反过来,如果先更新缓存,再更新数据库,一旦缓存更新成功,数据库更新失败,数据还是不一致;
比如数据库中的参照完整性,从表引用了主表的主键,对从表来说,也就是外键。当主表的记录删除后,从表是字段置空,还是级联删除。同样,当要创建从表记录时,主表记录是否要先创建,还是可以直接创建从表的记录;
比如数据库中的原子性:同时修改两条记录,一条记录修改成功了,一条记录没有修改成功,数据就会不一致,此时必须回滚,否则会出现脏数据。
比如数据库的Master-Slave异步复制,Master宕机切换到Slave,导致部分数据丢失,数据会不一致。
发送方发送了消息1、2、3、4、5,因为消息中间件的不稳定,导致丢了消息4,接收方只收到了消息1、2、3、5,发送方和接收方数据会不一致。
从以上案例可以看出,数据一致性问题几乎无处不在。本书把一致性问题分为了两大类:事务一致性和多副本一致性。这两类一致性问题基本涵盖了实践中所遇到的绝大部分场景,本章和下一章将分别针对这两类一致性问题进行详细探讨。
随处可见的分布式事务问题
在“集中式”的架构中,很多系统用的是Oracle这种大型数据库,把整个业务数据放在这样一个强大的数据库里面,利用数据库的参照完整性机制、事务机制,避免出现数据一致性问题。这正是数据库之所以叫“数据库”而不是“存储”的一个重要原因,就是数据库强大的数据一致性保证。
但到了分布式时代,人们对数据库进行了分库分表,同时在上面架起一个个的服务。到了微服务时代,服务的粒度拆得更细,导致一个无法避免的问题:数据库的事务机制不管用了,因为数据库本身只能保证单机事务,对于分布式事务,只能靠业务系统解决。
例如做一个服务,最初底下只有一个数据库,用数据库本身的事务来保证数据一致性。随着数据量增长到一定规模,进行了分库,这时数据库的事务就不管用了,如何保证多个库之间的数据一致性呢?
再以电商系统为例,比如有两个服务,一个是订单服务,背后是订单数据库;一个是库存服务,背后是库存数据库,下订单的时候需要扣库存。无论先创建订单,后扣库存,还是先扣库存,后创建订单,都无法保证两个服务一定会调用成功,如何保证两个服务之间的数据一致性呢?
这样的案例在微服务架构中随处可见:凡是一个业务操作,需要调用多个服务,并且都是写操作的时候,就可能会出现有的服务调用成功,有的服务调用失败,导致只部分数据写入成功,也就出现了服务之间的数据不一致性。
分布式事务解决方案汇总
接下来,以一个典型的分布式事务问题——“转账”为例,详细探讨分布式事务的各种解决方案。
以支付宝为例,要把一笔钱从支付宝的余额转账到余额宝,支付宝的余额在系统A,背后有对应的DB1;余额宝在系统B,背后有对应的DB2;蚂蚁借呗在系统C,背后有对应的DB3,这些系统之间都要支持相关转账。所谓“转账”,就是转出方的系统里面账号要扣钱,转入方的系统里面账号要加钱,如何保证两个操作在两个系统中同时成功呢?
1. 2PC
(1)2PC理论。在讲MySQL Binlog和Redo Log的一致性问题时,已经用到了2PC。当然,那个场景只是内部的分布式事务问题,只涉及单机的两个日志文件之间的数据一致性;2PC是应用在两个数据库或两个系统之间。
2PC有两个角色:事务协调者和事务参与者。具体到数据库的实现来说,每一个数据库就是一个参与者,调用方也就是协调者。2PC是指事务的提交分为两个阶段,如图10-1所示。
阶段1:准备阶段。协调者向各个参与者发起询问,说要执行一个事务,各参与者可能回复YES、NO或超时。
阶段2:提交阶段。如果所有参与者都回复的是YES,则事务协调者向所有参与者发起事务提交操作,即Commit操作,所有参与者各自执行事务,然后发送ACK。
如果有一个参与者回复的是NO,或者超时了,则事务协调者向所有参与者发起事务回滚操作,所有参与者各自回滚事务,然后发送ACK,如图10-2所示。
图10-1 2PC事务提交示意图
图10-2 事务回滚示意图
所以,无论事务提交,还是事务回滚,都是两个阶段。
(2)2PC的实现。通过分析可以发现,要实现2PC,所有参与者都要实现三个接口:Prepare、Commit、Rollback,这也就是XA协议,在Java中对应的接口是javax.transaction.xa.XAResource,通常的数据库也都实现了这个协议。开源的Atomikos也基于该协议提供了2PC的解决方案,有兴趣的读者可以进一步研究。
(3)2PC的问题。2PC在数据库领域非常常见,但它存在几个问题:
问题1:性能问题。在阶段1,锁定资源之后,要等所有节点返回,然后才能一起进入阶段2,不能很好地应对高并发场景。
问题2:阶段1完成之后,如果在阶段2事务协调者宕机,则所有的参与者接收不到Commit或Rollback指令,将处于“悬而不决”状态。
问题3:阶段1完成之后,在阶段2,事务协调者向所有的参与者发送了Commit指令,但其中一个参与者超时或出错了(没有正确返回ACK),则其他参与者提交还是回滚呢? 也不能确定。
为了解决2PC的问题,又引入了3PC。3PC存在类似宕机如何解决的问题,因此还是没能彻底解决问题,此处不再详述。
2PC除本身的算法局限外,还有一个使用上的限制,就是它主要用在两个数据库之间(数据库实现了XA协议&#x