分布式开发(二)---接口幂等性(防止重复提交)

说明下:这里说的接口主要指非查询类接口,因为查询类接口天然具备幂等性。

一、背景

交易系统里用户下单提交订单时,由于用户连续快速点击,导致连续发送多次请求,分别命中到了不同的服务器, 那么就会生成多个内容完全相同的订单,只有订单号不同而已。


当然造成重复请求的原因,还有其他的可能:

  1.  网络波动,引起重复请求
  2. MQ重复消费
  3. Nginx重试
  4. 黑客拦截请求后重发
  5.  ......

重复请求的影响

  • 下单时,用户面对多个一模一样的订单,不知道应该支付哪个。
  • 这些重复数据,对系统来说也是脏数据,还会影响正常的校验。
  • 下单成功如果发送短信提醒用户,但发送了几个内容一样的短信。

所以接口支持幂等性非常重要,尤其是涉及交易的接口。

二、什么是幂等性?

所谓幂等性, 通俗点说就是一个接口, 无论执行几次所产生的影响与只执行一次产生的影响是一样的

举例子说明下:
• 支付接口, 重复支付同一笔订单,最终也只能成功扣款一次。
• 支付宝、微信支付成功回调接口, 可能会多次回调, 必须只能成功处理一次。
• 普通表单提交接口, 多次点击提交, 数据只能成功落地一次。


三、常见解决方案

1. 唯一索引: 防止新增脏数据,保证最终插入数据库的数据只有一条。
2. token机制: 防止页面重复提交。
3. 悲观锁 -- 获取数据的时候加锁(一般是行锁)。
4. 乐观锁 -- 基于版本号version实现, 在更新数据那一刻校验数据。
5. 分布式锁 -- 通过第三方的系统(redis或zookeeper),在业务系统插入或更新数据时,获取分布式锁,然后做操作,最后释放锁。

四、分布式锁实现方案详解

解决思路:同一用户在1秒内对同一URL的操作视为重复提交
具体步骤:

  1. 服务器A接收到请求之后,尝试获取锁,获取成功 。
  2. 服务器A进行业务处理,订单提交成功。
  3. 服务器B接收到相同的请求,尝试获取锁,失败。因为锁被服务器A获取了,并且未释放。B 获取锁失败之后,直接返回。
  4. 服务器A处理完成,释放锁。

代码实现

@PostMapping(value = "/submit-order")
public Response<Boolean> submitOrder(HttpServletRequest request, OrderParam,orderParam){
        
        String userId= "123456";//用户
        String path = request.getServletPath();
        String key = MD5(userId+path);
        
        //1. 获取redis锁
        String result = jedis.set(key, orderParam, "NX", "EX", 1000);
        boolean success = "OK".equals(result);
        try {
            if (success) {
                //2. 执行具体业务逻辑
                //...
            }else{
                //3. 获取锁失败
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //4. 业务逻辑执行完成之后,释放锁
            jedis.del(key);
        }
        
        return ResponseUtil.success(Boolean.TRUE);
    }

总结

其实思路很简单, 我们也可以通过拦截器(AOP)+注解的方式实现一个通用的解决方案, 就不用每次请求都写重复代码。
幂等性在设计系统时,是需要首要考虑的问题,尤其是在像银行,支付宝等互联网金融公司等涉及到钱的系统。

 

Guess you like

Origin blog.csdn.net/icanlove/article/details/117652662