随着微服务的越来越多,一致性问题也越来越被重视。纠结是怎样才能ACID呢?CAP还是Base呢?其实强一致性的方案也特别多,比如net的msdtc、java的atomikos...等。但他们这类基于2pc(两阶段提交协议)实现,基本上性能太差,根本不适合高并发的系统。而本地消息表、可靠消息最终一致性方案、最大努力通知方案都是不错的解决方案。
目录
一致性问题
解决一致性问题的模式和思路
ACID
CAP
BASE
分布式一致性协议
两阶段提交协议
三阶段提交协议
TCC
最终一致性的解决方案
1.查询模式
2.补偿模式
3.异步确保模式
4. 定期校对模式
5.可靠消息模式
1.本地消息
2. 可靠事务消息事务
3.消息处理器的幂等性
关系型数据库天生用于解决具有复杂事务场景的问题,完全满足ACID 的特性。
由于业务规则的限制,我们无法将相关数据分到同一个数据库分片,这时就需要实现最终一致性。
由于对系统或者数据进行了拆分,我们的系统不再是单机系统,而是分布式系统!
CAP 原理证明,任何分布式系统只可同时满足以上两点,无法三者兼顾。由于关系型数据库是单节点无复制的,因此不具有分区容忍性,但是具有一致性和可用性,而分布式的服务化系统都需要满足分区容忍性,那么我们必须在一致性和可用性之间进行权衡。
BASE 思想解决了CAP 提出的分布式系统的一致性和可用性不可兼得的问题,如果想全面地学习BASE思想!BASE 思想与ACID 原理截然不同,它满足CAP 原理,通过牺牲强一致性获得可用性,一般应用于服务化系统的应用层或者大数据处理系统中,通过达到最终一致性来尽量满足业务的绝大多数需求。
有了BASE 思想作为基础,我们对复杂的分布式事务进行拆解,对其中的每个步骤都记录其状态,有问题时可以根据记录的状态来继续执行任务,达到最终一致。
JEE 的XA 协议就是根据两阶段提交来保证事务的完整性,并实现分布式服务化的强一致性。两阶段提交协议把分布式事务分为两个阶段, 一个是准备阶段,另一个是提交阶段。准备阶段和提交阶段都是由事务管理器发起的, 为了接下来讲解方便,我们将事务管理器称为协调者, 将资源管理器称为参与者。
两阶段提交协议在准备阶段锁定资源,这是一个重量级的操作, 能保证强一致性!但是实现起来复杂、成本较高、不够灵活,更重要的是它有如下致命的问题。
上面的所有问题虽然很少发生,但都需要人工干预处理,没有自动化的解决方案,因此两阶段提交协议在正常情况下能保证系统的强一致性,但是在出现异常的情况下,当前处理的操作处于错误状态,需要管理员人工干预解决, 因此可用性不够好,这也符合CAP 协议的一致性和可用性不能兼得的原理。
三阶段提交协议是两阶段提交协议的改进版本。它通过超时机制解决了阻塞的问题, 井且把两个阶段增加为以下三个阶段。
三阶段提交协议与两阶段提交协议主要有以下两个不同点:
三阶段提交协议与两阶段提交协议相比,具有如上优点,但是一旦发生超时,系统仍然会发生不一致,只不过这种情况很少见,好处是至少不会阻塞和永远锁定资源。
两阶段及三阶段方案中都包含多个参与者、多个阶段实现一个事务,实现复杂,性能也是一个很大的问题,因此,在互联网的高并发系统中,鲜有使用两阶段提交和三阶段提交协议的场景。
TCC 协议将一个任务拆分成Try 、Confirm 、Cancel 三个步骤,正常的流程会先执行T可,如果执行没有问题,则再执行Confirm ,如果执行过程中出了问题,则执行操作的逆操作Cancel 。从正常的流程上讲,这仍然是一个两阶段提交协议,但是在执行出现问题时有一定的自我修复能力,如果任何参与者出现了问题,则协调者通过执行操作的逆操作来Cancel 之前的操作,达到最终的一致状态。
从时序上来说,如果遇到极端情况,则TCC 会有很多问题,例如,如果在取消时一些参与者收到指令,而另一些参与者没有收到指令,则整个系统仍然是不一致的。对于这种复杂的情况,系统首先会通过补偿的方式尝试自动修复,如果系统无法修复,则必须由人工参与解决。
从TCC 的逻辑上看,可以说TCC 是简化版的三阶段提交协议,解决了两阶段提交协议的阻塞问题,但是没有解决极端情况下会出现不一致和脑裂的问题。然而, TCC 通过自动化补偿手段,将需要人工处理的不一致情况降到最少,也是一种非常有用的解决方案。某著名的互联网公司在内部的一些中间件上实现了TCC 模式。
现实系统的底线是仅仅需要达到最终一致性,而不需要实现专业的、复杂的一致性协议。
任何服务操作都需要提供一个查询接口,用来向外部输出操作执行的状态。
对于服务化系统中同步调用的操作,若业务操作发起方还没有收到业务操作执行方的明确返回或者调用超时,有了上面的查询模式,我们便可以对处于不正常的状态进行修正操作(要么重新执行子操作,要么取消子操作),则可以通过补偿模式。
异步确保模式是补偿模式的一个典型案例,经常应用到使用方对响应时间要求不太高的场景中,通常把这类操作从主流程中摘除,充分利用离线处理能力,通过异步的方式进行处理,处理后把结果通过通知系统通知给使用方。这个方案的最大好处是能够对高并发流量进行消峰。
最大努力通知方案也属于此类
- 系统 A 本地事务执行完之后,发送个消息到 MQ;
- 这里会有个专门消费 MQ 的最大努力通知服务,这个服务会消费 MQ 然后写入数据库中记录下来,或者是放入个内存队列也可以,接着调用系统 B 的接口;
- 要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么最大努力通知服务就定时尝试重新调用系统 B,反复 N 次,最后还是不行就放弃。
在操作主流程中的系统间执行校对操作,可以在事后异步地批量校对操作的状态,如果发现不一致的操作,则进行补偿,补偿操作与补偿模式中的补偿操作是一致的。
定期校对模式多应用于金融系统中。金融系统由于涉及资金安全, 需要保证准确性, 所以需要多重的一致性保证机制,包括商户交易对账、系统间的一致性对账、现金对账、账务对账、手续费对账等,这些都属于定期校对模式。顺便说一下,金融系统与社交应用在技术上的本质区别为: 社交应用在于量大, 而金融系统在于数据的准确性。
对于主流程中优先级比较低的操作,大多采用异步的方式执行,也就是前面提到的异步确保模型,为了让异步操作的调用方和被调用方充分解耦。我们通常通过消息队列实现异步化。
消息的可靠发送又分两种
在发送消息之前将消息持久到数据库,状态标记为待发送, 然后发送消息,如果发送成功,则将消息改为发送成功。定时任务定时从数据库捞取在一定时间内未发送的消息并将消息发送。
本地消息表其实是国外的 ebay 搞出来的这么一套思想,它主要是利用本地消息表来保障可靠保存凭证(消息)。这个方案说实话最大的问题就在于严重依赖于数据库的消息表来管理事务啥的,如果是高并发场景咋办呢?咋扩展呢?所以一般确实很少用。
不同的是持久消息的数据库是独立的, 并不藕合在业务系统中。发送消息前,先发送一个预消息给某个第三方的消息管理器,消息管理器将其持久到数据库,并标记状态为待发送,在发送成功后,标记消息为发送成功。
阿里的 RocketMQ 就支持消息事务,解放了本地消息的局限性。这个还是比较合适的,目前国内互联网公司大都是这么玩儿的。
- A 系统先发送一个 prepared 消息到 mq,如果这个 prepared 消息发送失败那么就直接取消操作别执行了;
- 如果这个消息发送成功过了,那么接着执行本地事务,如果成功就告诉 mq 发送确认消息,如果失败就告诉 mq 回滚消息;
- 如果发送了确认消息,那么此时 B 系统会接收到确认消息,然后执行本地的事务;
- mq 会自动定时轮询所有 prepared 消息回调你的接口,问你,这个消息是不是本地事务处理失败了,所有没发送确认的消息,是继续重试还是回滚?一般来说这里你就可以查下数据库看之前本地事务是否执行,如果回滚了,那么这里也回滚吧。这个就是避免可能本地事务执行成功了,而确认消息却发送失败了。
- 这个方案里,要是系统 B 的事务失败了咋办?重试咯,自动不断重试直到成功,如果实在是不行,要么就是针对重要的资金类业务进行回滚,比如 B 系统本地回滚后,想办法通知系统 A 也回滚;或者是发送报警由人工来手工回滚和补偿。
- 这个还是比较合适的,目前国内互联网公司大都是这么玩儿的,要不你举用 RocketMQ 支持的,要不你就自己基于类似 ActiveMQ?RabbitMQ?自己封装一套类似的逻辑出来,总之思路就是这样子的。
在分布式系统中,保证一致性的解决方案非常多,要针对场景而定,在另一篇中将针对微服务同步 & 异步 & 超时 & 补偿 & 快速失败进行分析。