在业务开发中,我们常会面对防止重复请求的问题。当服务端对于请求的响应涉及数据的修改,或状态的变更时,可能会造成极大的危害。重复请求的后果在交易系统、售后维权,以及支付系统中尤其严重。
在传统的restful风格的项目中,防止重复提交,通常做法是:后端生成一个唯一的提交令牌(uuid),并存储在服务端。页面提交请求携带这个提交令牌,后端验证并在第一次验证后删除该令牌,保证提交请求的唯一性。
上述的思路其实没有问题的,但是需要前后端都稍加改动,如果在业务开发完在加这个的话,改动量未免有些大了,本节的实现方案无需前端配合,纯后端处理。
利用分布式锁
相同的请求在同一时间只能被处理一次,利用分布式锁可以非常方便地解决这个问题。
思路:
[mark_a]
1、自定义注解 @NoRepeatSubmit 标记所有Controller中的提交请求。
2、通过拦截器对所有标记了 @NoRepeatSubmit 的方法拦截。
3、在业务方法执行前,获取当前用户的 token(或者JSessionId)+ 当前请求地址,作为一个唯一 KEY,去获取 Redis 分布式锁(如果此时并发获取,只有一个线程会成功获取锁)。
4、业务方法执行后,释放锁。
[/mark_a]
拦截器类
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"); } } }
利用数据库的唯一索引
如果设置了唯一约束,那么同一条数据再次插入数据库时,数据库会报唯一索引的错误,这个时候后台对这个异常进行处理并返回给前端提示。
思路:
[mark_a]接口A接收到请求之后,对请求信息hash运算,得到hash值hashCodeA;
保存hashCodeA 到数据库,并且对应的数据库的列(column)满足unique约束;
保存成功之后,才进行正常业务逻辑处理,比如提交订单;
服务器B接收到相同的请求后,也得到相同的hash值,hashCodeA,
服务器B保存hashCodeA 到数据库,肯定失败,因为相同的hash值已经存在;
因为保存失败,所以后面的业务逻辑不会执行。[/mark_a]
缓存计数器
由于数据库的操作比较消耗性能,了解到redis的计数器也是原子性操作。果断采用计数器。既可以提高性能,还不用存储,而且能提升qps的峰值。
以支付为例子:
[mark_a]
每次request进来则新建一个以orderId为key的计数器,然后+1。
如果>1(不能获得锁): 说明有操作在进行,删除。
如果=1(获得锁): 可以操作。
操作结束(删除锁):删除这个计数器。
[/mark_a]