spring web 重复提交解决方案
场景演示
假设有一个录入学生信息的功能,为了便于演示,要求不能有重名的学生,并且数据库对应字段没有做唯一限制.
@GetMapping("/student/{name}")
public Object reSubmitTest(@PathVariable String name){
List<Student> allByName = repository.findByName(name);
if (allByName != null && allByName.size() > 0) {
return "姓名重复";
}
//便于测试假设都是18岁
return repository.save(new Student(name, 18));
}
学生表
Field | Type | Comment |
---|---|---|
id | int(11) | 自增 |
name | varchar(20) | 姓名 |
age | int(5) | 年龄 |
上面这段代码,如果什么都不做,100个请求同时进来会发生什么呢
- 模拟同时100个请求
- 发现重复插入了很多条数据
如何解决
在网上有很多处理重复提交的方案,大部分的逻辑都是利用redis的key的过期时间,让请求在一点时间内重复进入方法,直达key过期.这种方法有一个缺点,需要固定一个时间,这个时间设置长了浪费性能,设置短了起不到防止重复提交的作用,我觉得有更好的方案
单服务
- 定义一个注解,拦截需要处理的方法
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Resubmit { }
- 拦截处理在请求进入方法前,加锁,往后的同一个请求(requestId相同)无法获取锁,就被判定为重复请求,抛出异常,等第一个请求调用完毕后再释放锁,这样一来,就不需要设定时间来限制访问
@Aspect @Component public class SubmissionAop { @Autowired private HttpServletRequest request; private static ConcurrentHashMap<String,Integer> concurrentHashMap = new ConcurrentHashMap<>(); @Pointcut("@annotation(Resubmit)") public void needCheckMethod() { } @Before("needCheckMethod()") public void checkMethod() { //requestId 的获取,确保同一个用户,同一个url String requestId = request.getMethod() + request.getServletPath() + request.getHeader("token"); try { check(requestId); request.setAttribute("__request_resubmit_need_release","need"); } catch (Exception e) { //抛出异常后被统一异常处理,转化为返回信息返回给前端 throw new RuntimeException("重复提交-上一个请求还未处理完"); } } @After("needCheckMethod()") public void release(){ if ("need".equals(String.valueOf(request.getAttribute("__request_resubmit_need_release")))) { String requestId = request.getMethod() + request.getServletPath() + request.getHeader("token"); concurrentHashMap.remove(requestId); } } /**同步保证查询和设置是原子操作 * @param requestId * @throws Exception */ private static synchronized void check(String requestId) throws Exception { if (concurrentHashMap.get(requestId) != null) { throw new Exception(); } concurrentHashMap.put(requestId, 1); } }
多服务/或redis
上面的aop方法,无法适用于多服务/集群,总体逻辑不变,适用redis来做分布式锁就行了,改造比较简单,如下@Autowired private StringRedisTemplate redisTemplate; @Autowired private HttpServletRequest request; @Pointcut("@annotation(Resubmit)") public void needCheckMethod() { } @Before("needCheckMethod()") public void checkMethod() { String requestId = request.getServletPath() + request.getHeader("token"); Boolean absent = redisTemplate.opsForValue() .setIfAbsent(requestId, "1",60, TimeUnit.SECONDS); Assert.notNull(absent,""); if (!absent) { throw new RuntimeException("重复提交-上一个请求还未处理完"); } request.setAttribute("__request_resubmit_need_release","need"); } @After("needCheckMethod()") public void release(){ if ("need".equals(String.valueOf(request.getAttribute("__request_resubmit_need_release")))) { String requestId = request.getServletPath() + request.getHeader("token"); redisTemplate.delete(requestId); } } }
上面的 redisTemplate.opsForValue().setIfAbsent(requestId, “1”,60, TimeUnit.SECONDS);是原子操作,这也是为什么使用它做分布式锁的原因, 其次这个方法需要redis版本在2.1以上,否则只能在同步方法中先setIfAbsent 再设置过期时间了. 这里设置过期时间的原因是,有可能第一个请求设置完锁后,redis出现问题,导致后面的请求一直无法获取锁,从而所有请求都被判定为重复请求.
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!