Spring-Boot征服Spring

spring web 重复提交解决方案

2019-05-31  本文已影响10人  zolvces

场景演示

假设有一个录入学生信息的功能,为了便于演示,要求不能有重名的学生,并且数据库对应字段没有做唯一限制.

    @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个请求同时进来会发生什么呢

  1. 模拟同时100个请求


  1. 发现重复插入了很多条数据


如何解决

在网上有很多处理重复提交的方案,大部分的逻辑都是利用redis的key的过期时间,让请求在一点时间内重复进入方法,直达key过期.这种方法有一个缺点,需要固定一个时间,这个时间设置长了浪费性能,设置短了起不到防止重复提交的作用,我觉得有更好的方案

单服务

  1. 定义一个注解,拦截需要处理的方法
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Resubmit {
    }
  1. 拦截处理
@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);
    }
}

在请求进入方法前,加锁,往后的同一个请求(requestId相同)无法获取锁,就被判定为重复请求,抛出异常,等第一个请求调用完毕后再释放锁,这样一来,就不需要设定时间来限制访问

多服务/或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出现问题,导致后面的请求一直无法获取锁,从而所有请求都被判定为重复请求.
最后 如果发现本文有需要改进的地方,或者你有更好的方案,欢迎留言交流

上一篇下一篇

猜你喜欢

热点阅读