什么是幂等性

在同一笔数据处理操作中,不管执行多少次,它所造成的后果都一样。

幂等性的设计

创建订单的设计

创建订单是用户通过表单或接口传递过来一系列支付所需要的信息,后台服务器根据这些信息创建一条订单记录。

这是一般的用法,但是在一些网络不好,或者客户端条件不确定的一些情况下,用户会连续重复的传递多条一样的数据过来,如果不做处理,就会给该用户创建多条订单记录,这显然是不合理的。

要解决这种问题,可以参考下面的思路:

  1. 在用户确认订单页面时,服务器端就生成一个订单编号传递给前端;
  2. 在生成订单的接口处把该订单编号带上,订单编号可以用统一的id生成器或者自定义的规则生成;
  3. 数据库表订单编号设置唯一索引;
  4. 后端在校验使用前端传过来的订单编号后,就将该编号记录下数据库中。当用户重复提交通过订单信息时,可以利用数据库唯一索引的特性来避免产生多个订单。

伪代码:

1
2
3
4
5
6
try:
insert into t_order (order_no, other_field) values (out_trade_no, '...');
# 其余操作...
# 提交事务
except:
# 回滚事务操作

支付回调的设计

在接入第三方支付,例如微信,支付宝支付时,处理回调结果是必须的操作。
以支付宝为例,在支付宝回调中,会返回 out_trade_no【商户订单号】,trade_no【支付宝交易号】,out_trade_no在我们自己的系统中唯一,trade_no在支付宝中唯一。

在处理回调时,一般有以下处理方式:

普通方式

  1. 接收到支付宝的回调请求,在校验结果成功后,拿到out_trade_no和trade_no
  2. 根据trade_no查询系统中该订单是否以及处理过
    1
    select * from t_order where order_no = out_trade_no;
  3. 已处理,直接返回;未处理,继续下面操作
  4. 开启事务
  5. 处理订单信息,修改订单状态
  6. 提交事务

上面的操作在正常情况下不会出现问题,但是由于网络或其他问题,支付宝发送的多条通知,这是就会出现问题。当两条通知同时到达第2步,同时查到该订单未处理过,这样这两个通知就会继续向下执行,造成的不好的结果。

为了解决上面的问题,有下面的处理方式:

悲观锁方式

  1. 接收到支付宝的回调请求
  2. 开启事务
  3. 查询订单并加悲观锁
    1
    select * from t_order where order_no = out_trade_no for update;
  4. 判断该订单是否以及处理过
  5. 已处理,直接返回;未处理,继续下面操作
  6. 处理订单信息,修改订单状态
  7. 提交事务

这里和上面方式的区别在于for update
select … for update 是常用的手动加锁语句,当执行for update时,数据库会对当前记录加锁。在其他线程执行到这行代码时,会等待上一个线程释放锁,才会重新获取锁,才能够继续执行。而在事务结束后,获取的锁会自动释放。

该方法能够解决上面一个方法产生的问题,但是在并发条件下会产生一些问题:
当多个线程同时触达时,只有第一个会获取锁,其他线程只能处于等待的过程中,这些等待线程就相当于浪费了,消耗了线程资源,不利于并发的场景。

乐观锁方式

  1. 接收到支付宝的回调请求
  2. 判断该订单是否以及处理过
  3. 已处理,直接返回;未处理,继续下面操作
  4. 开启事务
  5. 处理订单信息,修改订单状态
    这里的修改订单状态是重点,采用乐观锁来判断
    1
    2
    3
    4
    5
    6
    7
    update t_order set status = 1 where order_no = out_trade_no where status = 0;
    # 上面的update操作会返回影响的行数num
    if num==1:
    # 成功修改
    commit
    else:
    rollback
  6. 提交事务

这里采用了乐观锁的方式,在update时,mysql会对该记录产生行锁,当多个线程同时到达时,update语句会排队执行,因此最终只有一条update执行成功,返回更新条数,其他的更新条数为0。然后就根据更新条数来判断是否成功更新订单状态。

唯一约束方式

唯一约束的方式可以采用mysql数据库来实现
要依赖数据库来实现唯一性,首先要创建一个表

1
2
3
4
5
6
CREATE TABLE `t_unique` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`trade_no` varchar(32) NOT NULL DEFAULT '' COMMENT '支付宝交易号',
PRIMARY KEY (`id`),
UNIQUE KEY `trade_no` (`trade_no`)
) ENGINE=InnoDB;
  1. 接收到支付宝的回调请求
  2. 查询t_unique表,根据trade_no查询是否已经处理过该订单
  3. 已处理,直接返回;未处理,继续下面操作
  4. 开启事务
  5. 处理订单信息,修改订单状态
  6. t_unique表插入数据,插入成功则提交事务;插入失败则回滚事务
    1
    2
    3
    4
    5
    try:
    insert into t_uq_dipose (trade_no) values (trade_no)
    commit
    except:
    rollback
    同一请求时,由于支付宝支付号一致,由于数据库唯一索引的特性,插入数据只有一条数据成功;插入错误时回滚操作,保持了幂等性。
    这种方式在业务量大,并发高的情况下,插入数据会成为瓶颈。

总结

上面几种方式中,乐观锁 > 唯一约束 > 悲观锁,可以根据实际情况选择使用方式。