美文网首页Spring-Boot征服Spring
spring web 重复提交解决方案

spring web 重复提交解决方案

作者: zolvces | 来源:发表于2019-05-31 13:28 被阅读10次

    场景演示

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

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

    相关文章

      网友评论

        本文标题:spring web 重复提交解决方案

        本文链接:https://www.haomeiwen.com/subject/xtmwtctx.html