背景
在大型互联网系统中,基于成本的考虑,普遍会使用MySQL数据库;同时由于业务量很大,通常会按照用户维度对数据做垂直拆分,即大家常说的分库分表。
在阿里巴巴的红包系统中,红包的发放操作会涉及两个数据库的事务操作,一个数据库进行预算的扣减,另一个进行用户红包数据的写入,那么如何保证这两个事务操作的一致性呢?
问题及原因分析
开发人员首先想到的就是使用MySQL 的XA 协议,它使用的是两阶段提交协议,如图1所示。由事务协调者来保证所有的事务参与者都完成了第一阶段的准备工作,如果所有参与者都准备好了,那么就通知所有的参与者进行提交。MySQL 数据库仅能扮演参与者的角色,协调者(事务管理器)需要由另外的应用担任。
图1 两阶段提交协议
两阶段提交协议对于红包系统来说,在XA 事务中的第一阶段,预算端需要做的事情是冻结要扣除的预算,红包数据端需要插入一条不可用的红包数据。在XA 事务的第二阶段,如果事务需要提交,那么预算端需要将第一阶段的冻结的预算转化为实际的扣除,红包数据端需要将刚才插入的那一条不可用的红包数据变为可用;如果事务需要回滚,那么预处端需要将第一阶段的冻结的预算进行撤销操作,红包数据端需要删除刚才插入的不可用的红包数据。
两阶段提交协议对于红包系统来说,在XA 事务中的第一阶段,预算端需要做的事情是冻结要扣除的预算,红包数据端需要插入一条不可用的红包数据。在XA 事务的第二阶段,如果事务需要提交,那么预算端需要将第一阶段的冻结的预算转化为实际的扣除,红包数据端需要将刚才插入的那一条不可用的红包数据变为可用;如果事务需要回滚,那么预处端需要将第一阶段的冻结的预算进行撤销操作,红包数据端需要删除刚才插入的不可用的红包数据。
对于任何一次不论是成功还是失败的发放操作,预算端都会涉及两次针对于预算纪录的事务操作。众所周知的是,该条记录的操作会存在热点写的问题,使用XA 事务的方案会将热点写问题放大一倍。在业务场景中,使用XA 事务的方案看起来有些过于“悲观”了,事实上红包发放的成功与否几乎完全取决于预算是否扣减成功。回到业务的根本需求上来,有以下两个关键点。
(1)希望预算扣减成功与产生用户红包数据这两件事要么同时成功,要么同时失败,只要预算足够,那么理论上是肯定成功的。
(2)不可能将每一个发放操作变成一个一个顺序的独立事务,这样不但性能非常差,同时后面的事务也并不关心预算的具体剩余金额。所以实际上在发放操作中,很有可能看到一个是中间态的预算剩余金额,这是允许的。
如果能存在一起机制,它能保证预算扣减成功后100%会通知到我们,这样就可以利用这个机会去产生用户红包数据,如果产生失败,那么我们可以对已经扣减的预算进行一笔反向操作。
解决方案
最终的解决方案是开发人员设计了一个轻型的一致性消息组件,把预算扣减成功等诸多业务操作事件写入数据库中,该消息组件保证事件与业务操作处在同一个数据库中,所以仅仅是一次简单的本地事务操作。该消息组件会对写入的事件进行分发,通知每一个事件订阅者(比如在当前的场景中就是进行用户红包数据写入),然后对事件的处理结果进行记录。
在一个成功的红包发放操作中,对于预算端来说,进行了一次预算的扣减,一次事件写入,可能会进行一次事件读取,一次事件状态更新;对于用户红包数据端,仅需要进行一次用户红包数据的写入。可以看到,对于预算扣减的热点数据操作,新的方案并没有放大它。同时,失败的红包发放操作就更简单了,仅仅是一次失败的预算扣减,没有事件,没有用户红包端的任何操作。
另外,在实际使用中开发人员发现,使用这样的一套方案还可以降低系统的编码以及运维的复杂度,不再需要为预算端设计冻结操作以及用户红包数据端设计不可用数据,也不需要引入另外一套独立的事务协调系统。
这套系统命名为MiniBus,即微型总线,如图2所示。
图2 微型消息总线系统架构
它包括以下三部分。
(1)事件发布者Publisher。由业务逻辑在业务事务中调用,将事件写入到与业务相同的数据库中。
(2)事件配置Metadata。管理事件的基本信息,订阅关系等。
(3)事件调试器Scheduler。读取待分发的事件,根据订阅关系进行分发,调用相应的事件处理器,然后记录事件分发的结果。
该方案并不仅仅解决了红包发放操作中的事务一致性问题,事实上,只要业务上满足如下的两个要求,它都适用。
(1)短暂的中间状态是可以接受的。
(2)要求最终一终性。
同时,还有如下4 个方面的优点。
(1)较XA 方案更易于编码和理解,已有代码的改造成本很低。
(2)数据库消耗增加较少。
(3)系统简单,外部依赖少,易于维护。
(4)兼容数据库上的其他他SQL 优化。
小结
好的技术方案都是对于业务场景的深刻理解和权衡之后获得的,虽然通用的方案是首选,但它并不是唯一的好的选择。
原文发布时间为:2017-12-8
本文作者:逆流而上书摘