关于分布式系统数据一致性的那些事

2019-08-15 17:27:11 浏览数 (1)

近些年,随着SOA、微服务架构的流行,分布式系统数据一致性问题也随之而来成为大家热门关注的一个问题。其实,这个问题在很早之前就存在,因为在现实生活中,很多系统都不可能是一个大而全的单机系统,都或多或少需要跟其他系统集成,这种情况就必须需要考虑分布式系统数据一致性。

记得之前做微服务的session时,很多同学也有问到在微服务架构下如何保证数据一致性的问题。这里把我对这个问题的理解记录下来,欢迎讨论。

传统单机系统

在讨论分布式系统之前,需要先知道传统单机系统是如何保证数据一致性的。

对于传统的单机系统,数据的一致性往往可以通过数据库的事务来保证。数据库的事务具有ACID特性,天然就是用来保证数据的一致性。ACID的含义如下:

  • A:原子性(Atomicity)
  • C:一致性(Consistency)
  • I:隔离性(Isolation)
  • D:持久性(Durability)

具体数据库的事务是如何实现的,有兴趣的童鞋可以网上搜索。

分布式系统

关于分布式系统的概念,个人觉得这篇文章(第一次有人把“分布式事务”讲的这么简单明了)解释的很好。简单来讲,可以分成如下两类:

  • 一个系统一个service关联多个数据库 这类系统包含一个业务处理service,但是一个service连接多个数据库。在分库分表的场景下,常可能会出现这类系统架构。之前做过一个不宕机实现数据迁移的案例,其中有一步是实现双写,就需要一个service同时连接两个数据库,也是属于这一类,可参考之前我写的一篇文章(如何不宕机实现数据库迁移)。
  • 一个系统多个service关联各自数据库 这类系统由多个service组成,而每个service连接各自独立的数据库。现在流行的SOA、微服务架构系统就属于这一类。

对于传统单机系统,可以简单的通过数据库的事务特性来保证数据一致性,那么对于分布式系统,则可以通过分布式事务来保证。

在讨论分布式事务之前,需要先了解分布式事务的两个基础理论:CAP和BASE。

CAP是分布式系统的三个基本特性,每个分布式系统至多只能同时满足其中两个:

  • C:一致性
  • A:可用性
  • P:分区容错性

BASE是对CAP的AP特性的一个扩展:

  • BA:Basically Available(基本可用)
  • S:Soft state(软状态)
  • E:Eventually consistent (最终一致性)

基于这个理论的分布式系统,对数据一致性的要求是最终一致性,不像传统的数据库事务对数据的强一致性保证。

分布式事务

网上有很多关于分布式事务方案的讨论,这里有一篇文章写的很好很全面(https://www.javaworld.com/article/2077963/distributed-transactions-in-spring--with-and-without-xa.html)。基于这篇文章,可以把分布式事务方案分为两类:用XA协议和不用XA协议。

一、XA分布式事务协议

XA是Oracle提出的一种分布式事务协议,主要有两阶段提交(2PC:2 Phase Commits),三阶段提交(3PC:3 Phase Commits)实现,具体原理解释,可以参考这篇文章(漫画:什么是分布式事务?)。

这种方式可以保证相对强一致性,但需要数据库支持,性能消耗比较大,并且并不能完全保证一致性,在一些情况也可能导致数据不一致问题。

对于XA分布式事务协议,Spring已经有很好的支持,可以参考JtaTransactionManager。

二、不用XA分布式事务协议

不用XA分布式事务协议,文中也提到了很多方式,这里只讨论Best Effort 1PC,其实现在很多互联网系统基本上都是基于这种方案,即是基于BASE理论,保证数据最终一致性。

下面分享几个基于这种方案实现的具体案例:

第一个案例,之前做过一个不宕机实现数据迁移的案例,具体可参考之前写的文章(如何不宕机实现数据库迁移)。其中关键一步是在service中实现双写,即需要service同时连接两个数据库。在这个案例中,当有一个写数据请求达到service,如何保证数据同时在两个数据库写成功呢?

其实这个场景就属于第一类的分布式系统:一个系统一个service关联多个数据库,如何保证这个分布式系统的数据一致性呢?

这里采用的方案就是Best Effort 1PC,具体实现用的就是java的ChainedTransactionManager。

第二个案例,之前做过一个项目,基于微服务架构构建。整个系统有几个微服务,而关键的几个微服务之间采用RabbitMQ消息中间件通讯。这里为了描述方便,简化系统原型为:一个电商网站,销售订单是一个微服务,发货订单是一个微服务,各个微服务连接自己专有的数据库,销售订单和发货订单两个微服务通过消息中间件通讯。当一个销售订单变成“已支付”状态时,销售订单微服务发送一个event给消息中间件,发货订单微服务收到event创建一个发货订单。在这个场景下,如何保证销售订单状态改变和发货订单创建的同时成功或者同时失败呢?

其实这个场景就属于第二类的分布式系统:一个系统多个service关联各自数据库,如何保证这个分布式系统的数据一致性呢?

这里的方案也是Best Effort 1PC,采用的原则是:at least once delivery idempotent auto automatic dead letter reprocessing。参见https://eng.uber.com/reliable-reprocessing/。

意思是说,对于销售订单微服务,至少保证一次发送event,这样可以保证原始数据在销售订单这个微服务不丢失。对于发货订单微服务,如果发生创建发货订单失败,则开启自动重试机制,比如说可以利用死信队列DLX;如果创建发货订单成功,但是event没有被确认commit,则当再次收到这个event做创建发货订单的时候,需要保证发货订单不会被再次创建,即保证业务处理的幂等性(idempotent)。

第三个案例,之前做过的一个项目,采用的是人工处理的方式来解决分布式系统数据不一致的情况。原型是:一个分布式的缓存系统,有一个主节点,一个从节点,写的数据先到主节点,然后通过消息中间件同步到从节点。记得在这个案例中,偶尔会发生主从数据不一致的情况。那么采取的办法是:有一个数据比对的monitoring工具,定期检查数据一致性,一旦发生不一致性的情况,发邮件给运维人员,人工处理。

延伸案例,我之前生活中遇到过一个真实场景,骑共享单车的时候,扫码开锁,app上显示开锁失败,但是实际上车的锁打开了,这其实就是一个分布式系统数据不一致性的案例。这个问题应该是app backend发出开锁消息到自行车上,但是因为某些原因没收到response,而自行车端收到开锁消息了,并且成功执行了开锁,但是成功开锁的ack因为某些原因没发到app backend。

这个问题从业务上来讲,对骑行的人没有影响,因为app上显示开锁失败,也不用付钱;但是对于单车运营方来说是个损失,从他们的角度怎么保证一致性呢?有童鞋建议,可以先在app backend更新锁状态(把状态设成开锁成功),然后发开锁的消息,并且在app中提供重新开锁的功能(开锁失败,让用户来触发重新开锁)来处理不一致的情况。但是这也会带来一些新的问题,比如backend不知道开锁是真失败(锁没打开)还是假失败(锁打开了,但是打开的ack没成功发送到backend),如果是真失败,app上一来就显示开锁成功,对用户来说,用户体验不是很好,用户只能通过申诉锁没打开来解除开锁成功状态。并且,如果是假失败,用户同样可以采取申诉把开锁成功解除掉。这样这种方案带来了一些新的问题。记得这好像就是最开始小黄人的解决方案。如果大家有其他好的idea,欢迎讨论。

总结

在讨论分布式数据一致性问题时,一定要根据业务需求来讨论方案架构,因为不同的业务需求对数据的一致性要求不一样,采用的系统方案架构也就不一样,进而需要的技术成本和带给用户的体验也不一样。

References

  • https://www.cnblogs.com/takumicx/p/9998844.html
  • https://eng.uber.com/reliable-reprocessing/
  • 图片来源:https://themint.ae/mintgateway/payment-solutions/

0 人点赞