在微服务架构中,我们都会遇到事务一致性的问题。CAP理论要求必须要在可用性(Availability)和一致性(Consistency)中作出选择,如果选择了强一致性,必然导致可用性的降低。微服务的一大特性就是高可用,因此,我们只能通过弱化一致性来达到高可用,而最佳的选择是——数据最终一致性。
微服务架构实现最终一致性有三种模式:可靠事件模式、业务补偿模式、TCC模式。
可靠事件模式
可靠事件模式即是利用异步消息机制来达到数据最终一致性。这种实现方式将面临几个挑战:
- 如何保证事件的可靠投递?包括了事件可靠投递到事件代理(消息中间件),也包括了从事件代理中可靠获取事件。
- 如何保证事件消费端不会重复消费事件?
- 如何保证事件消费端能按事件生产顺序来消费?
- 如何处理事件消费异常?
- 如何解决长时处理问题?
保证事件可靠投递
由于大部分的消息中间件都实现了最少一次投递(at least once),因此消息中间件已经保证了事件是能可靠消费的。我们要重点讨论的是如何保证事件可靠投递到事件代理中去。
当在一个业务操作中,既要修改数据库,也要发送事件通知,这时就会产生分布式事务问题(两个存储,一个数据库,一个消息中间件)。要保证事件可靠投递,实际上就是要保证数据库和事件存储的最终一致性。一般有两种方式解决这个问题:本地事件表和外部事件表。
本地事件表
本地事件表实现方式是利用了数据库的ACID特性,把业务实体和事件都存储到同一个数据库实例中,最终把事件投递到消息中间件中去,从而保证事件可靠投递。
本地事件表的缺点是业务系统与事件系统紧耦合在一起,额外的事件数据库操作会给数据库带来额外的负担,从而导致瓶颈。
外部事件表
外部事件表的实现方式是事件在业务系统与事件系统中分离,事件独立存储在事件系统中,但与本地事件表方式不同的是,事件需要经历多个状态:准备、已确认、已提交或已撤消。下面的协作图与状态图描述了外部事件表的过程。

- 发生业务操作,向事件系统请求记录事件
- 事件系统记录事件,事件状态为准备中
- 提交业务数据,成功提交业务跳到步骤4,业务处理异常跳到步骤5,业务超时/网络超时/非业务异常跳到步骤6
- 向事件系统确认事件,事件状态为已确认。跳至步骤7
- 向事件系统撤消事件,事件状态为已撤消。
- 事件系统询问业务系统,更新事件状态为已确认或已撤消
- 事件系统检查事件状态,若为已确认则发送事件至事件代理,并更新事件状态为已提交。

重复消费问题
事件生产方保证事件的可幂等,即事件本身应表示某个时刻的状态,而非转换行为,如余额为100,而非新增余额10。事件消费方保证消费幂等。
消费顺序问题
消费顺序问题一般的解决办法是在事件中添加事件产生的时间戳或全局递增ID,消费方在消费完成后也需要记录事件产生时间或全局递增ID。
事件消费异常
事件消费异常会影响最终一致性,此时,可在消费方添加异常日志或报警机制,减少和尽快发现问题并修改。
长时处理
事件类型可以分为通知类事件和命令类事件。一旦发出通知类事件,事件生产方完全不需要理会消费方的消费结果。但对于命令类事件,事件生产方需要知道事件消费方的消费结果。当一个业务操作需要发送一个或多个命令类事件时,这样的事件处理过程称为长时处理。
长时处理一个关键的问题在于事件消费方可能由于消费异常(一般为业务异常,如果是系统异常,会由消息中间件重发消息进行再次消费)而导致最终没有把消费结果通知事件生产方,导致事件生产方业务不完整。另外,当一个业务操作需要等待多个命令类事件通知后,才能进入下一步操作时,也需要一种机制解决这类问题。此时,可在事件生产方添加一个跟踪器或状态机,当发生超时时,将以告警的形式通知开发人员,当命令类事件消费通知都到达之后,进入下一步处理。
补偿模式
补偿模式使用一个额外的协调服务来协调各个需要保证一致性的微服务,协调服务按顺序调用各个微服务,如果某个微服务调用异常(包括业务异常和技术异常)就取消之前所有已经调用成功的微服务。
假设行程预订的业务场景,需要同时预订航班、酒店、火车。使用协调框架的协作图如下:

上图中协调框架在收到1.预定请求后,创建一个协调流水,此时协调流水的状态为执行中状态。协调流水号由业务请求方生成,协调流水号很重要的一个作用是防止重复请求,而且在预订航班、酒店、火车流水中都要保存这个协调流水号,方便用于请求跟踪。当创建好协调流水后,将顺序调用各个依赖服务(航班预订、酒店预订、火车预订),调用每个依赖服务前都要在协调框架中记录相应调用流水,调用流水需要记录所有调用数据以作补偿过程使用,此时调用流水状态为执行中状态,当调用成功时,更新调用流水状态为已执行,当所有依赖调用都成功调用后,更新协调流水状态为已执行,并返回调用结果。当调用失败时,将按重试策略重试直到重试超时,进入补偿过程。
补偿过程应创建一条协调失败日志,并根据调用失败原因执行补偿操作:
- 失败原因为业务异常,调用之前成功调用过的依赖服务的补偿接口
- 失败原因为其他异常,对当前调用异常接口及之前成功调用过的依赖服务调用补偿接口。
当补偿接口调用成功之后,更新调用流水为已撤消状态,当所有补偿接口调用成功之后,更新协调流水为已撤消状态,并返回客户端调用失败结果。
由上面的执行过程中可以看出,协调框架将面临以下几个关键问题:
- 协调过程的中间状态如何恢复?—— 流水中有状态及所有的业务请求/补偿所需要的数据,这些数据为恢复执行提供了支持。
- 如何防止重复调用?—— 协调流水号作为调用的唯一标识,服务提供方/补偿接口提供方必须能按协调流水号支持幂等操作。
- 协调框架应具备服务编排和自动补偿能力。
重试策略
在服务调用和补偿接口调用都可以按下图所示的重试策略进行重试:

TCC模式(Try-Confirm-Cancel)
TCC模式是由业务服务向所有工作服务调用Try操作,如果所有Try操作成功,则执行Confirm操作,否则执行Cancel操作。Try操作完成所有业务检查 预留必须业务资源,Confirm操作真正执行业务 不作任何业务检查 只使用Try阶段预留的业务资源,Cancel操作释放Try阶段预留的业务资源。