前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >空谈分布式系统设计之幂等性

空谈分布式系统设计之幂等性

作者头像
Bruce Li
发布2020-10-14 17:47:16
9180
发布2020-10-14 17:47:16
举报
文章被收录于专栏:天马行空布鲁斯

这篇文章以两个典型的实际案例为基础,聊一聊分布式系统如何实现幂等性。

案例一:转账系统

在之前的文章,有多次提到转账系统这个案例,由于这个案例太典型了,很多大学教授数据库事务的时候就是用的这个案例。

对于一个单体应用版的转账系统,我们可以直接利用数据库的事务来保证整个转账操作的ACID。但是,随着用户量级的增加,单个数据库的瓶颈也随之出现,于是就出现了分库分表的设计,即:一部分用户信息存储在一个数据库,另一部分存储在另一个数据库。基于这样的设计,单个数据库的事务肯定就不可用了,我们需要采用跨数据库的分布式事务,比如基于XA协议的分布式事务,但是这种方式有一些自身的问题,并且有应用场景的局限性。所以,一般来说实际场景都是采用基于BASE的最终一致性解决方案。

如下则是一个简单的最终一致性方案设计:

Step 1:Application收到用户发出的一个转账请求之后,首先执行转出方的逻辑,如下:

代码语言:javascript
复制
begin transaction记账单 (包括:转账请求uuid+转账状态in progress)扣钱(转出方余额减少)commit/rollback

这段逻辑包含在一个transaction里面,由于只牵扯到一个数据库,可以利用单个数据库的事务保证。

Step 2:一个background job不断的抓取in progress的记账单,然后发送event(通知收款方收钱)到Kafka,发送成功之后,把账单状态改成success。

这段逻辑就是outbox pattern的实现,关于outbox pattern的具体介绍,可以参考我的另外一篇文章(空谈发件箱模式(outbox pattern))。

Step 3:转入方实现有个listener一直监听这个event,当监听到这个event时,执行如下逻辑:

代码语言:javascript
复制
begin transaction记账单(包括:转账请求uuid+转账状态success)加钱(转入方余额增加)commit/rollback

转入方的逻辑处理也是在一个transaction里面,可以通过单个数据库的事务保证。

但是,上面的设计可能有多个地方会出现event消息重发的情况,比如:background job发送event成功,但是修改账单状态失败;或者,转入方逻辑commit到数据库成功,但是发送ack给Kafka出问题,等等。那么,如何处理这样的重复消费消息的情况呢?因为如果处理不当,就可能会导致数据不一致。其实,这本质上就是一个幂等性问题,保证收到重复消息和收到一次消息的处理结果是一致的,就是幂等的。

对于上面的设计,要保证幂等性,可以在账单表中存一个request uuid,利用这个uuid达到去重的效果,具体是:转入方在收到重复转账event消息时,根据request uuid先去数据库里面检查有没有这个ID存在,有的话则表示这个转账已经处理过了,直接把这个event忽略掉;没有的话则表示需要处理这个event,执行转账。总体来讲,这样的处理逻辑就是幂等的。

当然,实际的转账系统还需要考虑各种错误情况,比如:转入方处理失败的话,可以发送一个反向的event,转出方把之前的扣钱revert回来。

案例二:数据迁移

在之前的文章,也有多次提到数据迁移这个案例。这个案例说的是需要把数据从老的数据库迁移到新的数据库,并且需要保证服务不停止(zero downtime),即不影响用户的正常使用。

对于老数据,可以直接使用一个background job不断的迁移;关键是对于新数据,应该如何“迁移”?一种办法是:双写,即在往老数据库写的同时也往新数据库写,这样来保证新数据在两边都有。

同时往两个数据库写,如何保证两边全成功全失败呢?这又是分布式事务的问题,当时提到了一种方案:best effort 1pc,使用的是Spring提供的ChainedTransactionManager。但是,这种方式在极限情况下也会出现不一致的情况,比如:数据库在特定的时间节点宕机。

下面介绍另外一种基于event方式的双写:在把数据往老数据库写之后,接着把数据本身作为event payload发到Kafka。(这里可以利用outbox pattern来保证at least once delivery)然后,新加一段逻辑,监听这个event,收到这个event之后,把数据写入到新的数据库。

同样的,在监听event这里,需要额外handle下面的情况以保证幂等性:

  1. 收到重复插入数据event(这个情况和上面转账的案例类似) 对于这种情况,如何实现幂等性处理? 类似的,可以依赖一个唯一的主键,先根据主键判断数据存不存在。
  2. 消息顺序变化 消息顺序产生变化,可能的情况有: - retry queue,两次连续更新同一条数据的event,第一个event处理失败放进retry queue,而第二个event处理成功。 - 流量切到新的数据库上时,Kafka里面还有更新数据的event,此时已经有更新数据的请求进来。 对于这种情况,如何保证幂等性呢? 关键点是老的event需要被忽略掉。实现层面可以依赖于一个时间戳,不管是迁移数据本身,或者是event对象本身,如果新的event已经处理,则老的event忽略;如果数据已经被更新,则老的event忽略。

上面提到的双写需要再额外增加一个event数据库表,如果可以,也可以采用cdc的方式,这种方式常常用于数据库的复制、备份等场景,利用这种方式,则不需要额外写一张表,而依赖数据库的事务日志,具体可以参考我的另一篇文章(空谈发件箱模式(outbox pattern))。

写在最后的话

通过上面两个案例,我们可以看到:在很多场景,都可以用最终一致性方案替代强一致性来实现分布式事务,这样应用系统可以更加容易地实现高可用(high availability)、可伸缩性(scaling)等特性;但同时也需要非常仔细地从具体业务角度处理幂等性等问题。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-10-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 天马行空布鲁斯 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档