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