微服务架构下的数据一致性保证

可靠事件

补偿

TCC

传统单机应用 ACID 事务和2PC

传统单机应用一般用关系型数据库,好处是应用可以使用 ACID transactions

保证一致性只需要:开始事务,改变(插入,删除,更新)很多行,然后提交或回滚事务

借助数据访问技术和框架(如Spring),需要做的事情更少,只需要关注数据本身的改变

数据量庞大后,对应用和数据库拆分,应用需要同时访问两个或以上数据库

开始用分布式事务保证一致性(常说的两阶段提交协议(2PC))

微服务架构中不能选择分布式事务的原因

  1. 数据是微服务私有的,唯一可访问方式是API。这种打包数据访问方式使微服务之间松耦合,彼此独立容易性能扩展

  2. 不同微服务经常使用不同的数据库,产生各种不同类型的数据,关系型数据库并不一定是最佳选择(SQL和NoSQL结合)

redis,Elasticsearch,图数据库Neo4j等

非关系型数据大多数并不支持2PC

微服务架构中应满足数据最终一致性原则

可用性一般是更好的选择,但在服务和数据库之间维护数据一致性是非常根本的需求,微服务架构中选择满足最终一致性

微服务架构实现最终一致性的三种模式

业务模式决定了选择

可靠事件

订单服务发布“创建订单”事件

支付服务消费“创建订单”事件,支付完成后发布“支付完成”事件

订单服务消费“支付完成”事件,订单状态更新为待出库

可能不一致的地方:

  • 微服务更DB后发布事件失败

微服务发布事件成功,但消息代理未正确推送事件到订阅的微服务

  • 接受事件的微服务重复消费了事件

可靠事件模式在于保证可靠事件投递避免重复消费

可靠事件投递定义为

(a)每个服务原子性的业务操作和发布事件

(b)消息代理确保事件传递至少一次

避免重复消费要求服务实现幂等性,如支付服务不能因为重复收到事件而多次支付

补偿

业务异常:业务逻辑错误,账户余额不足、商品库存不足等 技术异常:非业务逻辑产生的异常,网络连接异常、网络超时等

使用一个额外的协调服务来协调各个需要保证一致性的微服务,协调服务按顺序调用各个微服务,如果某个微服务调用异常(包括业务异常和技术异常)就取消之前所有已经调用成功的微服务

补偿模式建议仅用于不能避免出现业务异常的情况,如果有可能应该优化业务模式,以避免要求补偿事务

余额不足业务异常可通过预先冻结金额方式避免,商品库存不足可要求商家准备额外的库存等

实例

预订行程

(1)上海-北京航班 (2)酒店住3晚 (3)北京-上海火车

客户提交行程后,按顺序串行调用航班预订、酒店预订、火车预订服务

最后的火车预订服务成功后整个预订业务才算完成

火车预订没有调用成功,之前预订的航班、酒店都得取消。取消之前预订的酒店、航班即为补偿过程

需要注意的是酒店的取消预订、航班的取消预订同样不能保证一定成功,所以补偿过程往往也同样需要实现最终一致性,需要保证取消服务至少被调用一次和取消服务必须实现幂等性

应该尽可能通过设计避免采用补偿方式

TCC(Try-Confirm-Cancel)

  1. Try:完成所有业务检查

预留必须业务资源

  1. Confirm:真正执行业务

不作任何业务检查

只使用Try阶段预留的业务资源

Confirm操作满足幂等性

  1. Cancel:

释放Try阶段预留的业务资源

Cancel操作满足幂等性

整个TCC业务分成两个阶段完成

需要注意的是第二阶段confirm或cancel操作本身也是满足最终一致性的过程,在调用confirm或cancel的时候也可能因为某种原因(比如网络)导致调用失败,所以需要活动管理支持重试的能力,同时这也就要求confirm和cancel操作具有幂等性

对账是最后的终极防线

瞬时的网络故障或调用超时等问题,通过3种模式一般都能得到很好的解决

但依赖外部系统的可用性情况,在一些重要的业务场景下还需要周期性的对账来保证真实的一致性。比如支付系统和银行之间每天日终是都会有对账过程

补偿要点

为降低开发的复杂性和提高效率,协调服务实现为一个通用的补偿框架。补偿框架提供服务编排和自动完成补偿的能力

第一部分

实现补偿模式的关键在于业务流水的记录

首先,确定失败的步骤和状态,从而确定需要补偿的范围,补偿范围和异常原因有关

不光要知道第3个步骤(预订火车)失败,还要知道失败的原因

订火车服务返回无票,补偿过程只需要取消前两个步骤就可以了;如果失败的原因是因为网络超时,那么补偿过程除前两个步骤之外还需要包括第3个步骤

其次,要能提供补偿操作使用到的业务数据

支付微服务的补偿操作要求参数包括支付时的业务流水id、账号和金额

理论上实际完成补偿操作根据唯一的业务流水id就可以,但是提供更多的要素有益于微服务的健壮性,收到补偿操作的时候可以做业务的检查,比如检查账户是否相等,金额是否一致等等

上面两点的实现,通过记录完整的业务流水,可以通过业务流水的状态来确定需要补偿的步骤,同时业务流水为补偿操作提供需要的业务数据

第二部分

通过重试来保证补偿过程的完整,从而满足最终一致性,要求补偿操作本身具备幂等性

只是一味的失败就立即重试会给工作服务造成不必要的压力,要根据服务执行失败的原因来选择不同的重试策略

  • 失败原因不是暂时性的,业务因素导致(如业务要素检查失败),不是重发就能自动恢复的,应立即终止重试

  • 罕见的异常,网络传输过程出现数据丢失或者错误,应该立即再次重试,类似的错误一般很少会再次发生

  • 系统繁忙(http 500或者另外约定的返回码)或者超时,等待一些时间再重试

重试次数上线,达到上限就不再进行重试了,通过手段通知相关人员处理

等待重试的策略如果重试时仍然错误,可逐渐增加等待的时间,直到达到一个上限后,以上限作为等待时间

某个时刻聚集了大量需要重试的操作,补偿框架需要限流

第三部分

TCC模式是优化的补偿模式

补偿模式没有隔离性,第一个工作服务步骤开始一直到所有工作服务完成(或者补偿过程完成),不一致是对其他服务可见的

另外最终一致性的保证还充分的依赖了协调服务的健壮性,如果协调服务异常,就没法达到一致性

TCC模式在一定程度上弥补了上述的缺陷,在TCC模式中直到明确的confirm动作,所有的业务操作都是隔离的(由业务层面保证)。另外工作服务可以通过指定try操作的超时时间,主动的cancel预留的业务资源,从而实现自治的微服务

TCC模式和补偿模式一样需要需要有协调服务和工作服务,协调服务也可以作为通用服务一般实现为框架。与补偿模式不同的是TCC服务框架不需要记录详细的业务流水,完成confirm和cancel操作的业务要素由业务服务提供