如何保证业务的幂等性

浅析幂等性及常用解决方案

什么是幂等性

在编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。用通俗的话讲:就是针对一个操作,不管做多少次,产生效果或返回的结果都是一样的。

举几个简单的例子:

  1. 前端对同一表单数据的重复提交,后台应该只会产生一个结果
  2. 我们发起一笔付款请求,应该只扣用户账户一次钱,当遇到网络重发或系统bug重发,也应该只扣一次钱
  3. 发送消息,也应该只发一次,同样的短信如果多次发给用户,用户会崩溃
  4. 创建业务订单,一次业务请求只能创建一个,不能出现创建多个订单

如何保证幂等性

幂等性需要通过唯一的业务单号来保证,也就是说相同的业务单号,认为是同一笔业务。使用这个唯一的业务单号来确保后面的多次相同的业务处理逻辑和执行效果是一致的。以支付为例,在不考虑并发的情况下,实现业务的幂等性分两个流程:

  1. 先查询订单是否已经支付过
  2. 如果支付过,则返回支付成功;如果没有支付,则进行支付流程,修改订单业务为已支付

这个方案是分成两步的,步骤 2 依赖于步骤 1 的结果,无法保证原子性。在并发环境下,假设现在请求 A 查询到订单没有支付,执行支付流程,执行完毕后,还没有修改订单状态时,由于重复提交,请求 B 查询订单状态得出未支付的结论,进行支付流程。这样对于同一个订单相当于支付了两次,是不符合幂等性的。

知道为什么破坏了幂等性,余下为问题也很简单:将查询和更改状态操作加锁,将并行操作更改为串行操作

幂等性实现方案

乐观锁

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

防重表

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

分布式锁

对于防重表可以用分布式锁代替,比如 Redis 和 Zookeeper

Redis

  1. 订单发起支付请求,支付系统会去 Redis 缓存中查询是否存在该订单号的 Key,如果不存在,则向 Redis 增加 Key 为订单号
  2. 查询订单支付状态,如果未支付,则进行支付流程,支付完成后删除该订单号的 key

Zookeeper

  1. 订单发起支付请求,支付系统会去 Zookeeper 中创建一个 node,如果创建失败,则表示订单已经被支付
  2. 如果创建成功,则进行支付流程,支付完成后删除 node

Token 机制

这种方式分成两个阶段:申请 Token 阶段和支付阶段。 第一阶段,在进入到提交订单页面之前,需要订单系统根据用户信息向支付系统发起一次申请 Token 的请求,支付系统将 Token 保存到 Redis 缓存中,为第二阶段支付使用。 第二阶段,订单系统拿着申请到的 Token 发起支付请求,支付系统会检查 Redis 中是否存在该 Token ,如果存在,表示第一次发起支付请求,删除缓存中 Token 后开始支付逻辑处理;如果缓存中不存在,表示非法请求。

消息队列缓冲

将订单的支付请求全部发送到消息队列中,然后使用异步任务处理队列中的数据,过滤掉重复的待支付订单,再进行支付流程。

参考

Search

    Table of Contents