在分布式系统中,时刻要想着design for failure。接口调用失败(非业务异常)和超时是最常见的一种错误(fault),而幂等的设计是针对此类问题的容错手段。

再重复一下幂等的定义:

在编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。

简单的理解,就是如果f(f(x)) = f(x),则f函数是幂等的。概念很好理解,但在实战中,我们的接口不会是stateless的函数,所以实现上会复杂很多,场景的需要幂等保证的场景有这些:

  • 用户领取(单个)优惠,多次领取和一次领取的效果一致。
  • 付款请求,同一笔订单,不会出现多次付款。
  • 利用mq进行系统间解耦,因为mq不保证消息只投递一次,所以consumer需要考虑多次消费的场景。
  • 理论上所有更新操作接口都要考虑幂等性。

先凭印象回顾下常见的幂等实现方案。

常见的方案有哪些坑?

唯一索引

最容易想到的做法是借助数据库的唯一键的约束实现(实际上数据库的底层实现也是加锁)。具体的实现是:

  • 对具体的业务场景建一个执行记录表,以幂等号(和场景)建唯一键索引。
  • 在执行业务逻辑之前,先根据幂等号,尝试向这个记录表插入幂等记录。
  • 如果能成功插入,说明未被执行,可以进入业务逻辑。
  • 如果插入失败,说明已经执行,就拒绝请求。

[流程图]

这个做法的确可以达到分布式互斥的效果,保证对相同操作(相同幂等号),只执行一次。但这个方案比较大的问题在于:因为“插入幂等记录”和“执行业务事务”是两步操作,在极端情况下,需要考虑幂等记录插入成功,但业务事务执行失败(且重试可能成功)的情况:在这种情况下,实际上操作并没有执行成功,是需要调用方重试的,然而幂等号的记录已经落地,在重试时,请求会被拒绝。

那如果将两步放在同一个事务里呢?这样的设计避免了上面的问题,引入新的问题是,直到事务提交的时候,才知道这个请求是否已经执行,这样会增加大量无用的开销,当然有一些优化手段,比如说执行整个事务前先查幂等记录,如果存在则阻断,不需要等到事务提交。但在高并发的情况下,这样的实现依然会存在无用开销:多个线程同时执行事务,而实际能提交的只有一个。

乐观锁(只考虑了并发,没考虑重复请求)

利于业务状态机

  • 业务语义的幂等(但外围的操作,比如发消息这种,怎么避免?)

幂等要考虑的问题

幂等号 - 什么是“同一个操作”?

  • 并发的问题 - 分布式互斥
  • 非并发的多次执行

  • 我们不止希望多次调用的结果一致,还希望额外调用的开销降低

幂等号的生成

幂等的实现

  • 通用的
  • 最通用的不一定最好

参考

-