关于解决系统接口幂等性问题的解决

什么是幂等性?

对于同一个业务操作,不管调用多少次,在数据库的存储,或者得到的结果应该是一样的。

幂等性的设计思想

以支付宝或者微信的充值为例,在我们支付完之后,支付宝、微信会给我们回调,来通知我们的系统支付成功,而在这之前,我们系统中已经存储了这条订单信息,我们要做的事就是需要在支付宝或者微信给我们回调之后,不管回调多少次,我们接口应该针对于同一个订单得到的结果是一样的。

针对于我们系统中应该是有一个唯一的商品订单号:out_trade_no

支付宝中的回调给我们返回了:out_trade_no【商户订单号(自己定义)】,trade_no【支付宝交易号】

幂等性接口的实现方式

方式一(最常用的,最方便的方式,但是有问题):

为什么会说这是最常用的呢,因为这样实现是最简单的,并没有太多的思考,

过程如下面:

  1. 收到支付宝的回调,
  2. 根据trade_no和out_trade_no 查询数据库订单信息,该订单是否被处理
  3. 如果该订单已经处理,则直接返回,如果未处理继续向下执行
  4. 开启本地事务,
  5. 本地系统给用户账户加钱
  6. 将订单状态修改为交易成功
  7. 提交本地事务

思考:

看似该方式很合适,但是,如果,支付宝、微信给我们的通知多次,同时到达步骤二,会发生什么事情呢?显而易见,查询到订单都是未处理的,那么就会发生多次给账户加钱的情况,所以,我们最常用的这种方式是有问题的

方式二(加锁):

想一想,方式一中,是因为出现了并发问题,所以导致账户重复充值的情况,那么我们可不可以用加锁的方式来解决呢?当然是可以的

过程如下:

  1. 接收到支付宝的支付成功回调请求
  2. 调用java中的锁,
  3. 根据trade_no和out_trade_no 查询数据库订单信息,该订单是否被处理
  4. 如果订单已处理直接返回,若未处理,继续向下执行
  5. 开启本地事务
  6. 本地系统给用户加钱
  7. 将订单状态置为成功
  8. 提交本地事务
  9. 释放Lock锁

思考:

这样看来已经没什么问题了,是吧?其实并不然,想想看,如果我们的应用只是部署了一份,这样做是没有问题的,但是,如果我们的应用做了负载均衡,部署了多台机器,这样做是不是会出现问题,支付宝的回调过来之后,经过负载均衡服务,将请求分配到不同的机器上,这种方式是不是就不行了呢?此时相当于是无锁处理了,又会出现方式一的结果。

想一想nginx负载均衡中请求的策略中可以设置,同一ip的请求,都在同一台服务器上,采用这种配置,是否可以解决该问题呢?

方式三:(悲观锁方式)

使用数据库的悲观锁方式,其实跟方式二的加锁方式很像,只不过是依靠数据库来实现,数据库中悲观锁方式是使用 for update来实现的,过程如下:

  1. 接收到支付宝的支付成功请求。
  2. 打开本地事务
  3. 查询订单信息并加上悲观锁(select * from t_order where order_id = trade_no for update;)
  4. 判断订单是否已被处理,如果已处理,直接返回,如果未处理,继续向下执行
  5. 本地系统给用户账户加钱
  6. 将订单状态修改为已处理
  7. 提交本地事务

思考:

该方式其实主要用运了数据库的 for update,关于 for update 做出解释:

1.当线程A执行for update,数据会对当前记录加锁,其他线程执行到此行代码的时候,会等待线程A释放锁之后,才可以获取锁,继续后续操作。
2.事物提交时,for update获取的锁会自动释放。

如果我们的业务系统逻辑比较复杂,那么,在并发情况下,会导致后面的线程处于无效的等待状态,都在等待获取 for update悲观锁,这样不利于系统的并发操作

方式四:乐观锁

利用数据库中的乐观锁来实现,过程如下:

      1.接收到支付宝的成功支付回调请求

      2.查询订单信息(select * from t_order where order_id = trade_no;)

      3.判断订单信息是否已被处理,如果已被处理直接返回,如果未被处理继续执行

      4.打开本地事务

      5.本地系统给用户账号加钱

      6.使用类似下面的伪代码给修改订单状态为成功

update t_order set status = 1 where order_id = trade_no where status = 0;
//上面的update操作会返回影响的行数num
if(num==1){
 //表示更新成功
 提交事务;
}else{
 //表示更新失败
 回滚事务;
}

      思考:

update t_order set status = 1 where order_id = trade_no where status = 0;

是依靠乐观锁来实现的,执行这条sql,如果有多个线程同事到达这段代码,数据库内部会保证update同一条记录会排队,最终只有一条update会执行成功,其他未成功的返回的num是0,然后根据num来进行提交或者回滚操作

方式五:唯一约束

依赖数据库中的唯一约束来实现,其实也很简单,过程如下:

首先,我们需要建一张表,t_uq_dipose,这张表里面呢包含了一个业务类型字段,和该业务类型在系统中的唯一的订单号,业务来时,先查询该表中有没有对应的数据,如果没有,继续执行,如果要是有,直接返回

先创建一张表:

CREATE TABLE `t_uq_dipose` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `ref_type` varchar(32) NOT NULL DEFAULT '' COMMENT '关联对象类型',
  `ref_id` varchar(64) NOT NULL DEFAULT '' COMMENT '关联对象id',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_1` (`ref_type`,`ref_id`) COMMENT '保证业务唯一性'
);

可以看到,我们建了唯一性约束,ref_type和ref_id 这样的话可以保证插入到该表中的数据绝对是唯一的。

过程如下:

  1. 收到支付宝成功支付回调
  2. 查询 t_uq_dipose 表可以判断订单是否已处理
  3. 判断订单是否已处理,如果已处理直接返回,如果未处理,继续向下执行
  4. 打开本地事务
  5. 给本地系统中用户加钱
  6. 将订单状态修改为成功
  7. 向 t_uq_dipose 表中插入数据,插入成功提交本地事务,插入不成功,回滚本地事务。

总结

实现幂等性常见的方法有:悲观锁、乐观锁、唯一约束

几种方式,按照最优排序:乐观锁 > 唯一约束 > 悲观锁

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章