一、什么是一致性问题
水平拆分和垂直拆分
- 水平拆分是指由于单一节点无法满足性能需求,需要扩展为多个节点,多个节点具有一致的功能,组成一个服务池,一个节点服一部分的请求量,所有节点共同处理大规模高并发的请求量。
- 垂直拆分是指按照功能拆分,秉着“专业的人干专业的事”的原则,把一个复杂的功能拆分为多个单一、简单的功能,不同的单一功能组合在一起,和未拆分前完成的功能是一样的。由于每个功能职责单一、简单,使得维护和变更都变得更简单、容易、安全,所以更易于产品版本的迭代,还能够快速地进行敏捷发布和上线。
拆分后的系统或者服务化饿系统的最大问题就是一致性问题:对于这么多具有单一功能的模块,或者同一功能池中的多个节点,如何保证它们的信息、工作进度、状态一致并且协调有序地工作。
二、一致性问题
1、下订单和扣库存
2、同步调用超时
3、异步回调超时
4、掉单
5、系统间状态不一致
6、缓存和数据库不一致
7、本地缓存节点间不一致
8、缓存数据结构不一致
三、解决一致性问题的模式和思路
1、ACID
A: Atomicity,原子性
C: Consistency,一致性
I: Isolation,隔离性
D: Durability,持久性
2、CAP
C:Consistency,一致性。在分布式系统中的所有数据备份,在同一时刻具有同样的值,所有节点在同一时刻读取的数据都是最新的数据副本。
A:Availability,可用性,好的响应性能。完全的可用性指的是在任何故障模型下,服务都会在有限的时间内处理完成并进行相应。
P:Partition tolerance,分区容忍性。尽管网络上有部分消息丢失,但系统仍然可继续工作。
CAP原理证明,任何分布式系统只同同时满足以上亮点,无法三者兼顾。
3、BASE
BASE思想与ACID原理截然不同,满足CAP原理,通过牺牲强一致性获得可用性,一般应用与服务化系统的应用层或者大数据处理系统中,通过达到最终一致性来尽量满足业务的绝大多数需求。
三、分布式一致性协议
1、两阶段提交协议
JEE的XA协议就是根据两阶段提交来保证事务的完整性,并实现分布式服务化的强一致性。
两阶段提交协议把分布式事务分为两个阶段,一个是准备阶段,另一个是提交阶段。准备阶段和提交阶段都是由事务管理器发起的。我们将事务管理器称为协调者,将资源管理器称为参与者。
两阶段提交协议的流程如下所述:
准备阶段
提交阶段
两阶段提交协议在准备阶段锁定资源,这是一个重量级操作,能保证强一致性,但实现复杂,成本较高,不够灵活,且有如下致命问题:
- 阻塞:对于任何一次指令都必须收到明确的响应,才会继续进行下一步,否则处于阻塞状态,占用的资源一直锁定,不会被释放
- 单点故障:如果协调者宕机,参与者没有协调者指挥,则会一直阻塞,尽管可以通过选举新 协调者替代原有协调者,但是如果协调者在发送一个提交指令后宕机,而提交指令仅仅被一个参与者接收,并且参与者接收后也宕机,则新上任的协调者无法处理这种情况。
- 脑裂:协调者发送提交指令,有的参与者接收到并执行了事务,有的参与者没有接收到事务就没有执行事务,多个参与者之间是不一致的。
2、三阶段提交协议
是两阶段提交协议的改进版本,通过超时机制解决了阻塞的问题
- 询问阶段:协调者询问参与者是否可以完成指令,参与者只需要回答是或不是,而不需要做真正的操作,这个阶段超时会导致中止
- 准备阶段:如果在询问阶段所有参与者都返回可以执行操作,则协调者向参与者发送预执行请求,然后参与者写redo和undo日志,执行操作但是不提交操作;如果在询问阶段任意参与者返回不能执行操作的结果,则协调者向参与者发送中止请求。
- 提交阶段:如果每个参与者在准备阶段返回准备成功,也就是说预留资源和执行操作成功,则协调者向参与者发起提交指令,参与者提交资源变更的事务,释放锁定的资源,如果任何参与者返回准备失败,也就是说预留资源或者执行操作失败,则协调者向参与者发起中止指令,参与者取消已经变更的事务,执行undo日志,释放锁定的资源
与两阶段提交协议主要有以下两个不同点:
- 增加了一个询问阶段,询问阶段可以确保尽可能早的发现无法执行操作而需要中止执行的行为,但是它并不能发现所有这种行为,只会减少这种情况的发生。
- 在准备阶段以后,协调者和参与者执行的任务中都增加了超时,一旦超时,则协调者和参与者都会继续提交事务,默认为成功,这也是根据概率统计超时后默认为成功的正确性最大。
三阶段提交协议与两阶段提交协议相比,具有如上优点,但是一旦发生超时,系统仍然会发生不一致,只不过这种情况很少见,好处是至少不会阻塞和永远锁定资源。
两阶段提交协议和三阶段提交协议,在极端情况下,系统会产生阻塞或者不一致的问题,需要阴影或者技术人员解决,两阶段及三阶段方案中都包含多个参与者、多个阶段实现一个事务,实现复杂,性能也是一个很大的问题,因此,在互联网的高并发系统中,鲜有使用两阶段提交和三阶段提交协议的场景。
3、TCC
TCC协议将一个任务拆分成Try、Confirn、Cancel三个步骤,正常的流程会先执行Try,如果执行没有问题,则再执行Confirm,如果执行过程中出了问题,则执行操作的逆操作Cancel。从正常的流程上讲,这仍然是一个两阶段提交协议,但是在执行出现问题时有一定的自我修复能力,如果任何参与者出现了问题,则协调者通过执行操作的逆操作来Cancel之前的操作,达到最终的一致状态。
从时序上来说,如果遇到极端情况,则TCC会有很多问题,例如,如果在取消时一些参与者收到指令,而另一些参与者没有收到指令,则整个系统仍然是不一致的。对于这种复杂的情况,系统首先会通过补偿的方式尝试自动修复,如果系统无法修复,则必须由人工参与解决。
从TCC的逻辑上看,可以说TCC是简化版的三阶段提交协议,解决了两阶段提交协议的阻塞问题,但是没有解决极端情况下会出现不一致和脑裂的问题。然而,TCC通过自动化补偿手段,将需要人工处理的不一致情况降到最少,也是一种非常有用的解决方案。
四、保证最终一致性的模式
1、查询模式
任何服务操作都需要提供一个查询接口,用来向外部输出操作执行的状态。
2、补偿模式
根据查询模式,在任何情况下,都能得知具体的操作所处的状态,如果整个操作都处于不正常的状态,则我们需要修正操作中有问题的子操作,可能需要重新执行未完成的自操作,或者取消已经完成的子操作,通过修复使整个分布式系统达到一致。为了让系统最终达到一致状态而做的努力都叫做补偿。
补偿操作根据发起形式分为以下几种。
- 自动恢复:程序根据发生不一致的环境,通过继续进行未完成的操作,或者回滚已经完成的操作,才自动达到一致状态
- 通知运营:如果程序无法自动恢复,并且设计时考虑到了不一致的场景,则可以提高运营功能,通过运营手工进行补偿。
- 技术运营:如果系统无法自动恢复,又没有运营功能,那么必须通过技术手段来解决,技术手段包括数据库变更或者代码变更,这是最糟的一种场景,也是在生产中尽量避免的场景。
3、异步确保模式
异步确保模式是补偿模式的一个典型案例,经常应用到使用方对响应时间要求不太高的场景中,通常把这类操作从主流程中摘除,通过异步的方式进行处理,处理后把结果通过通知系统通知给使用方。这个方案最大好处是能够对高并发流量进行消峰,例如:电商系统中的物流、配送,以及支付系统中的计费、入账等。
在实践中将要执行的异步操作封装后持久入库,然后通过定时捞取未完成的任务进行补偿操作来实现异步确保模式,只要定时系统足够健壮,则任何任务最终都会被成功执行。
4、定期校对模式
在操作主流程中的系统间执行校对操作,可以在事后异步地批量校对操作的状态,如果发生不一致的操作,则进行补偿,补偿操作与补偿模式中中的补偿操作是一致的。
实现定期校对的一个关键就是分布式系统中需要有一个自始至终唯一的ID,生成全局唯一ID有以下两种方法:
持久性:使用数据库表自增字段或者Sequence生成,为了提高效率,每个应用节点可以缓存一个批次的ID。
时间型:一般由机器号、业务号、时间、单节点内自增ID组成,由于时间一般精确到秒或者毫秒,因此不需要持久就能保证在分布式系统中全局唯一,粗略递增等。
(唯一ID:发号器项目Vesta,项目地址为http://vesta.cloudate.net/vesta/doc/Vesta.html)
5、可靠消息模式
1)消息的可靠发送
1、在发送消息之前将消息持久到数据库,状态标记为待发送,然后发送消息,如果发送成功,则将消息改为发送成功。定时任务定时从数据库捞取在一定时间内未发送的消息并将消息发送。
2、与第一种类似,不同的是持久消息的数据库是独立的,并不耦合在业务系统中。发送消息前,先发送一个预消息给某个第三方的消息管理器,消息管理器将其持久到数据库,并标记状态在待发送,在发送成功后,标记消息为发送成功。定时任务定时从数据库中捞取一定时间内未发送的消息,查询业务系统是否要继续发送,根据查询结果来确定消息的状态。
一些公司把消息的可靠发送实现在了中间件里,通过Spring的注入,在消息发送时自动持久消息记录,如果有消息记录没有发送成功,则定时补偿发送。
2)消息处理器的幂等性
要保证消息一定发送除去,那么需要有重试机制,有重试机制后,消息就一定会重复,那么就要保证操作的幂等性。
保证操作的幂等性的常用方法如下:
- 使用数据库表的唯一键进行滤重,拒绝重复的请求
- 使用分布式表对请求进行滤重
- 使用状态流转的方向性来滤重,通常使用数据库的行级锁来实现
- 根据业务的特点,操作本身就是幂等的,例如:删除一个资源,增加一个资源,获得一个资源等
6、缓存一致性模式
使用缓存来保证一致性的最佳实践:
- 如果性能要求不是非常高,则尽量使用分布式缓存,而不是使用本地缓存。
- 写缓存时数据一定要完整,如果缓存数据的一部分有效,另一部分无效,则宁河在需要时回源数据库,也不要把部分数据放入缓存中。
- 使用缓存牺牲了一致性,为了提高性能,数据库与缓存只需要保持弱一致性,而不需要保持强一致性,否则违背了使用缓存的初衷。
- 读的顺序是先读缓存,后读数据库,写的顺序要先写数据库,后写缓存。
五、超时处理模式
1、微服务的交互模式
- 同步调用模式
- 接口异步调用模式
- 消息队列异步处理模式
2、同步与异步的抉择
- 尽量使用异步来替换同步操作
- 能用同步解决的问题,不要引入异步
第一条原则是从业务功能的角度出发的,也就是从与用户或者使用方的交互模式出发的,如果业务逻辑允许,用户对产品的交互形态没有异议,则我们可以将一些耗时较长的、用户对响应时间没有特别要求的操作异步化,以此来减少核心链路的层级,释放系统的压力
第二条原则是从技术和架构的角度出发的,这条原则应用的前提是同步能够解决问题,这隐含了一个含义:如果性能不是问题,或者所处理的操作是短小的轻量级处理逻辑,那么同步调用方式是最理想不过的,因为这样不需要引入异步化的复杂处理流程。