如何避免并发情况下的重复提交

2019年10月11日21:30:44 评论 488

在业务开发中,我们常会面对防止重复请求的问题。当服务端对于请求的响应涉及数据的修改,或状态的变更时,可能会造成极大的危害。重复请求的后果在交易系统、售后维权,以及支付系统中尤其严重。

在传统的restful风格的项目中,防止重复提交,通常做法是:后端生成一个唯一的提交令牌(uuid),并存储在服务端。页面提交请求携带这个提交令牌,后端验证并在第一次验证后删除该令牌,保证提交请求的唯一性。

上述的思路其实没有问题的,但是需要前后端都稍加改动,如果在业务开发完在加这个的话,改动量未免有些大了,本节的实现方案无需前端配合,纯后端处理。

利用分布式锁

相同的请求在同一时间只能被处理一次,利用分布式锁可以非常方便地解决这个问题。

思路:

1、自定义注解 @NoRepeatSubmit 标记所有Controller中的提交请求。
2、通过拦截器对所有标记了 @NoRepeatSubmit 的方法拦截。
3、在业务方法执行前,获取当前用户的 token(或者JSessionId)+ 当前请求地址,作为一个唯一 KEY,去获取 Redis 分布式锁(如果此时并发获取,只有一个线程会成功获取锁)。
4、业务方法执行后,释放锁。

拦截器类

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
@Slf4j
public class ViolationInterceptor extends HandlerInterceptorAdapter {    
    @Autowired 
    private RedisLock redisLock;

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex) throws Exception {
    }
     
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object hadnler, ModelAndView ex) throws Exception {
    }
 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String key = request.getHeader("requestId");
            
        boolean isSuccess = redisLock.tryLock(key, clientId, lockSeconds); 
        if (isSuccess) { 
            log.info("tryLock success, key = [{}], clientId = [{}]", key, clientId); 
            // 获取锁成功, 执行进程 
            Object result; 
            try { 
                requestIds.add(requestId);
                return super.preHandle(request, response, handler);
            } finally { 
                // 释放锁 
                redisLock.releaseLock(key, clientId); 
                log.info("releaseLock success, key = [{}], clientId = [{}]", key, clientId); 
            }
        }else {
            throw new IllegalArgumentException("Violation Request; Reason requestId already registered");
        }
    }
}

利用数据库的唯一索引

如果设置了唯一约束,那么同一条数据再次插入数据库时,数据库会报唯一索引的错误,这个时候后台对这个异常进行处理并返回给前端提示。

思路:

接口A接收到请求之后,对请求信息hash运算,得到hash值hashCodeA;
保存hashCodeA 到数据库,并且对应的数据库的列(column)满足unique约束;
保存成功之后,才进行正常业务逻辑处理,比如提交订单;
服务器B接收到相同的请求后,也得到相同的hash值,hashCodeA,
服务器B保存hashCodeA 到数据库,肯定失败,因为相同的hash值已经存在;
因为保存失败,所以后面的业务逻辑不会执行。

缓存计数器
由于数据库的操作比较消耗性能,了解到redis的计数器也是原子性操作。果断采用计数器。既可以提高性能,还不用存储,而且能提升qps的峰值。

以支付为例子:

每次request进来则新建一个以orderId为key的计数器,然后+1。

如果>1(不能获得锁): 说明有操作在进行,删除。
如果=1(获得锁): 可以操作。
操作结束(删除锁):删除这个计数器。

本文已通过「原本」原创作品认证,未经作者授权请勿直接转载,负责将依法追究其法律责任。
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: