6

高并发下的幂等策略分析

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzA5OTI2MTE3NA%3D%3D&%3Bmid=2658338108&%3Bidx=5&%3Bsn=a5298c227631abfc5d4300cb9e23ca69
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

a6FbYzz.gif

双十一,零点刚开始,小明就迫不及待地点击提交订单按钮,1秒,2秒,3秒,没反应,小明有点心慌,又快速地点击了两下,提示下单成功。随后小明到我的订单列表中一看,发现有三个相同的订单,小明一脸黑线。

什么是幂等性

HTTP/1.1中对幂等性的定义是:

Methods can also have the property of “idempotence” in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request.

这里不讨论学术上如何定义幂等性,而是重点在于如何在分布式环境中提供对外幂等性的接口。对外提供的接口承诺幂等性,其要表达的含义是:只要调用接口成功,外部对接口的多次调用得到的结果是相同的。即执行多次和一次的效果是一样的。

为什么需要幂等

上面小明遇到的问题,就是在防止重复提交的情况上没有做好控制。业务开发中,经常会遇到重复提交的情况,无论是由于网络问题无法收到请求结果而重新发起请求,或是前端的操作抖动而造成重复提交情况。

在交易系统,支付系统这种重复提交造成的问题有尤其明显,比如:

  1. 用户在APP上连续点击了多次提交订单,后台应该只产生一个订单;(下多个相同的单,我有病啊)

  2. 向支付宝发起支付请求,由于网络问题或系统BUG重发,支付宝应该只扣一次钱。(多付N笔钱,我真有钱啊)

    很显然,幂等接口认为,外部调用者会存在多次调用的场景,为了防止重试对数据状态的改变,需要将接口的设计为幂等的。

什么情况下需要保证幂等性

以SQL为例,有下面三种场景,只有第三种场景需要开发人员使用其他策略保证幂等性:

SELECT col1 FROM tab1 WHER col2=2
UPDATE tab1 SET col1=1 WHERE col2=2
UPDATE tab1 SET col1=col1+1 WHERE col2=2

保证幂等策略

幂等需要通过唯一的业务单号来保证。也就是说相同的业务单号,认为是同一笔业务。使用这个唯一的业务单号来确保,后面多次的相同的业务单号的处理逻辑和执行效果是一致的。

下面以支付为例,在不考虑并发的情况下,实现幂等很简单:①先查询一下订单是否已经支付过,②如果已经支付过,则返回支付成功;如果没有支付,进行支付流程,修改订单状态为‘已支付’。

防重复提交策略

上述的保证幂等方案是分成两步的,第②步依赖第①步的查询结果,无法保证原子性的。在高并发下就会出现下面的情况:第二次请求在第一次请求第②步订单状态还没有修改为‘已支付状态’的情况下到来。既然得出了这个结论,余下的问题也就变得简单:把查询和变更状态操作加锁,将并行操作改为串行操作。

乐观锁

如果只是更新已有的数据,没有必要对业务进行加锁,设计表结构时使用乐观锁,一般通过version来做乐观锁,这样既能保证执行效率,又能保证幂等。例如:

UPDATE tab1 SET col1=1,version=version+1 WHERE version=#version#

不过,乐观锁存在失效的情况,就是常说的ABA问题,不过如果version版本一直是自增的就不会出现ABA的情况。

防重表

使用订单号orderNo做为去重表的唯一索引,每次请求都根据订单号向去重表中插入一条数据。第一次请求查询订单支付状态,当然订单没有支付,进行支付操作,无论成功与否,执行完后更新订单状态为成功或失败,删除去重表中的数据。后续的订单因为表中唯一索引而插入失败,则返回操作失败,直到第一次的请求完成(成功或失败)。可以看出防重表作用是加锁的功能。

分布式锁

这里使用的防重表可以使用分布式锁代替,比如Redis。订单发起支付请求,支付系统会去Redis缓存中查询是否存在该订单号的Key,如果不存在,则向Redis增加Key为订单号。查询订单支付已经支付,如果没有则进行支付,支付完成后删除该订单号的Key。通过Redis做到了分布式锁,只有这次订单订单支付请求完成,下次请求才能进来。相比去重表,将放并发做到了缓存中,较为高效。思路相同,同一时间只能完成一次支付请求。

token令牌

这种方式分成两个阶段:申请token阶段和支付阶段。

第一阶段,在进入到提交订单页面之前,需要订单系统根据用户信息向支付系统发起一次申请token的请求,支付系统将token保存到Redis缓存中,为第二阶段支付使用。

第二阶段,订单系统拿着申请到的token发起支付请求,支付系统会检查Redis中是否存在该token,如果存在,表示第一次发起支付请求,删除缓存中token后开始支付逻辑处理;如果缓存中不存在,表示非法请求。

实际上这里的token是一个信物,支付系统根据token确认,你是你妈的孩子。不足是需要系统间交互两次,流程较上述方法复杂。

支付缓冲区

把订单的支付请求都快速地接下来,一个快速接单的缓冲管道。后续使用异步任务处理管道中的数据,过滤掉重复的待支付订单。

优点是同步转异步,高吞吐。不足是不能及时地返回支付结果,需要后续监听支付结果的异步返回。

幂等性接口的不足

  1. 增加了额外控制幂等的业务逻辑,复杂化了业务功能;

  2. 把并行执行的功能改为串行执行,降低了执行效率。

    因此除了业务上的特殊要求外,尽量不提供幂等的接口。

FzYZV3b.jpg!web

Kotlin 开发者社区

国内第一Kotlin 开发者社区公众号,主要分享、交流 Kotlin 编程语言、Spring Boot、Android、React.js/Node.js、函数式编程、编程思想等相关主题。

IF3MZfm.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK