zoukankan      html  css  js  c++  java
  • SpringBoot + Redis 解决重复提交问题(幂等)

    在开发中,一个对外暴露的接口可能会面临瞬间的大量重复请求,如果想过滤掉重复请求造成对业务的伤害,那就需要实现幂等

    幂等:

    • 任意多次执行所产生的影响均与一次执行的影响相同。最终的含义就是 对数据库的影响只能是一次性的,不能重复处理。

    解决方案:

    1. 数据库建立唯一性索引,可以保证最终插入数据库的只有一条数据
    2. token机制,每次接口请求前先获取一个token,然后再下次请求的时候在请求的header体中加上这个token,后台进行验证,如果验证通过删除token,下次请求再次判断token(本次案例使用)
    3. 悲观锁或者乐观锁,悲观锁可以保证每次for update的时候其他sql无法update数据(在数据库引擎是innodb的时候,select的条件必须是唯一索引,防止锁全表)
    4. 先查询后判断,首先通过查询数据库是否存在数据,如果存在证明已经请求过了,直接拒绝该请求,如果没有存在,就证明是第一次进来,直接放行

    一、搭建Redis服务

    package com.gamer.idempotence.service;
    
    /**
     * @author 47
     * @version 1.0
     * @date 2020/6/11 9:42
     * @description: redis工具类
     */
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.ValueOperations;
    import org.springframework.data.redis.serializer.RedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    import org.springframework.stereotype.Component;
    
    import java.io.Serializable;
    import java.util.concurrent.TimeUnit;
    
    /**
     * redis工具类
     */
    @Component
    public class RedisService {
    
        private RedisTemplate redisTemplate;
    
        @Autowired(required = false)
        public void setRedisTemplate(RedisTemplate redisTemplate) {
            RedisSerializer stringSerializer = new StringRedisSerializer();
            redisTemplate.setKeySerializer(stringSerializer);
            redisTemplate.setValueSerializer(stringSerializer);
            redisTemplate.setHashKeySerializer(stringSerializer);
            redisTemplate.setHashValueSerializer(stringSerializer);
            this.redisTemplate = redisTemplate;
        }
    
        /**
         * 写入缓存
         *
         * @param key
         * @param value
         * @return
         */
    
        public boolean set(final String key, Object value) {
            boolean result = false;
            try {
                ValueOperations operations = redisTemplate.opsForValue();
                operations.set(key, value);
                result = true;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return result;
        }
    
        /**
         * 写入缓存设置时效时间
         *
         * @param key
         * @param value
         * @return
         */
    
        public boolean setEx(final String key, Object value, Long expireTime) {
            boolean result = false;
            try {
                ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
                operations.set(key, value);
                redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
                result = true;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return result;
        }
    
        /**
         * 判断缓存中是否有对应的value
         *
         * @param key
         * @return
         */
        public boolean exists(final String key) {
            return redisTemplate.hasKey(key);
        }
    
    
        /**
         * 读取缓存
         * @param key
         * @return
         */
        public Object get(final String key) {
            Object o = null;
            ValueOperations valueOperations = redisTemplate.opsForValue();
            return valueOperations.get(key);
        }
    
    
        /**
         * 删除对应的value
         * @param key
         */
        public Boolean remove(final String key) {
            if(exists(key)){
                return redisTemplate.delete(key);
            }
            return false;
        }
    
    }

    二、自定义注解

    作用:拦截器拦截请求时,判断调用的地址对应的Controller方法是否有自定义注解,有的话说明该接口方法进行 幂等

    package com.gamer.idempotence.annotion;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * @author 47
     * @version 1.0
     * @date 2020/6/11 9:55
     * @description:
     */
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface AutoIdempotent {
    }

    三、Token创建和校验

    package com.gamer.idempotence.service;
    
    import com.gamer.idempotence.exectionhandler.BaseException;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.util.StringUtils;
    
    import javax.servlet.http.HttpServletRequest;
    import java.util.UUID;
    
    /**
     * @author 47
     * @version 1.0
     * @date 2020/6/11 9:56
     * @description: token服务
     */
    @Service
    public class TokenService {
    
        @Autowired RedisService redisService;
    
        //创建token
        public String createToken() {
            //使用UUID代表token
            UUID uuid = UUID.randomUUID();
            String token = uuid.toString();
            //存入redis
            boolean b = redisService.setEx(token, token, 10000L);
            return token;
        }
    
        //检验请求头或者请求参数中是否有token
        public boolean checkToken(HttpServletRequest request) {
    
            String token = request.getHeader("token");
    
            //如果header中是空的
            if(StringUtils.isEmpty(token)){
    
                //从request中拿
                token = request.getParameter("token");
                if(StringUtils.isEmpty(token)){
                   throw new BaseException(20001, "缺少参数token");
                }
    
            }
    
            //如果从header中拿到的token不正确
            if(!redisService.exists(token)){
                throw new BaseException(20001, "不能重复提交-------token不正确、空");
            }
    
            //token正确 移除token
            if(!redisService.remove(token)){
                throw new BaseException(20001, "token移除失败");
            }
    
            return true;
        }
    }

    这里用到了自定义异常和自定义响应体如下

    自定义异常:

    package com.gamer.idempotence.exectionhandler;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    /**
     * @author 47
     * @version 1.0
     * @date 2020/5/16 20:58
     * @description: 自定义异常类
     */
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class BaseException extends RuntimeException {
    
        private Integer code;
    
        private String msg;
    
    }

    设置统一异常处理:

    package com.gamer.idempotence.exectionhandler;
    
    
    import com.gamer.idempotence.utils.R;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    /**
     * @author 47
     * @version 1.0
     * @date 2020/5/16 20:45
     * @description: 统一异常处理器
     */
    @ControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(Exception.class)
        @ResponseBody
        public R error(Exception e){
            e.printStackTrace();
            return R.error();
        }
    
        @ExceptionHandler(BaseException.class)
        @ResponseBody
        public R error(BaseException e){
            e.printStackTrace();
            return R.error().message(e.getMsg()).code(e.getCode());
        }
    }

    自定义响应体:

    package com.gamer.idempotence.utils;
    
    import lombok.Data;
    
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * @author 47
     * @version 1.0
     * @date 2020/5/16 18:35
     * @description: 返回结果
     */
    @Data
    public class R {
    
        private Boolean success;
        private Integer code;
        private String message;
        private Map<String, Object> data = new HashMap<String, Object>();
    
        private R() {
        }
    
        //封装返回成功
        public static R ok(){
            R r = new R();
            r.setSuccess(true);
            r.setCode(ResultCode.SUCCESS);
            r.setMessage("成功");
            return r;
        }
    
        //封装返回失败
        public static R error(){
            R r = new R();
            r.setSuccess(false);
            r.setCode(ResultCode.ERROR);
            r.setMessage("失败");
            return r;
        }
    
        public R success(Boolean success){
            this.setSuccess(success);
            return this;
        }
        public R message(String message){
            this.setMessage(message);
            return this;
        }
        public R code(Integer code){
            this.setCode(code);
            return this;
        }
        public R data(String key, Object value){
            this.data.put(key, value);
            return this;
        }
        public R data(Map<String, Object> map){
            this.setData(map);
            return this;
        }
    }

    自定义响应码:

    package com.gamer.idempotence.utils;
    
    import lombok.Data;
    
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * @author 47
     * @version 1.0
     * @date 2020/5/16 18:35
     * @description: 返回结果
     */
    @Data
    public class R {
    
        private Boolean success;
        private Integer code;
        private String message;
        private Map<String, Object> data = new HashMap<String, Object>();
    
        private R() {
        }
    
        //封装返回成功
        public static R ok(){
            R r = new R();
            r.setSuccess(true);
            r.setCode(ResultCode.SUCCESS);
            r.setMessage("成功");
            return r;
        }
    
        //封装返回失败
        public static R error(){
            R r = new R();
            r.setSuccess(false);
            r.setCode(ResultCode.ERROR);
            r.setMessage("失败");
            return r;
        }
    
        public R success(Boolean success){
            this.setSuccess(success);
            return this;
        }
        public R message(String message){
            this.setMessage(message);
            return this;
        }
        public R code(Integer code){
            this.setCode(code);
            return this;
        }
        public R data(String key, Object value){
            this.data.put(key, value);
            return this;
        }
        public R data(Map<String, Object> map){
            this.setData(map);
            return this;
        }
    }

    四、拦截器配置

    1、拦截器配置类

    package com.gamer.idempotence.config;
    
    import com.gamer.idempotence.interceptor.AutoIdempotentInterceptor;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    /**
     * @author 47
     * @version 1.0
     * @date 2020/6/11 10:07
     * @description: 拦截器配置类
     */
    @Configuration
    public class WebConfiguration implements WebMvcConfigurer {
    
        @Autowired
        private AutoIdempotentInterceptor autoIdempotentInterceptor;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
    
            registry.addInterceptor(autoIdempotentInterceptor);
    
        }
    }

    2、拦截器类

    package com.gamer.idempotence.interceptor;
    
    import com.gamer.idempotence.annotion.AutoIdempotent;
    import com.gamer.idempotence.service.TokenService;
    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 javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.lang.reflect.Method;
    
    /**
     * @author 47
     * @version 1.0
     * @date 2020/6/11 10:11
     * @description: 拦截重复提交数据
     */
    @Component
    public class AutoIdempotentInterceptor implements HandlerInterceptor {
    
        @Autowired
        private TokenService tokenService;
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    
            if(!(handler instanceof HandlerMethod))
                return true;
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
    
            //拿到方法上面的自定义注解
            AutoIdempotent annotation = method.getAnnotation(AutoIdempotent.class);
            
            //如果不等于null说明该方法要进行幂等
            if(null != annotation){
                return tokenService.checkToken(request);
            }
    
            return true;
        }
    }

    五、正常Sevice类

    package com.gamer.idempotence.service;
    
    import org.springframework.stereotype.Service;
    
    /**
     * @author 47
     * @version 1.0
     * @date 2020/6/11 10:04
     * @description:
     */
    @Service
    public class TestService {
    
        public String testMethod(){
            return "正常业务逻辑";
        }
    
    }

    六、Controller类

    package com.gamer.idempotence.controller;
    
    import com.gamer.idempotence.annotion.AutoIdempotent;
    import com.gamer.idempotence.service.TestService;
    import com.gamer.idempotence.service.TokenService;
    import com.gamer.idempotence.utils.R;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.*;
    
    /**
     * @author 47
     * @version 1.0
     * @date 2020/6/11 9:58
     * @description:
     */
    @RestController
    @CrossOrigin
    @RequestMapping("/Idempotence")
    public class TestController {
    
        @Autowired
        private TokenService tokenService;
    
        @Autowired
        private TestService testService;
    
    
        @GetMapping("/getToken")
        public R getToken(){
            String token = tokenService.createToken();
            return R.ok().data("token",token);
        }
    
    
        //相当于添加数据接口(测试时 连续点击添加数据按钮  看结果是否是添加一条数据还是多条数据)
        @AutoIdempotent
        @PostMapping("/test/addData")
        public R addData(){
            String s = testService.testMethod();
            return R.ok().data("data",s);
        }
    
    
    
    
    
    }

    七、测试

    第一次点击:

     第二次点击:

  • 相关阅读:
    Java基础面试操作题: 线程问题,写一个死锁(原理:只有互相都等待对方放弃资源才会产生死锁)
    Java基础面试操作题: File IO 文件过滤器FileFilter 练习 把一个文件夹下的.java文件复制到另一个文件夹下的.txt文件
    Java基础面试操作题:Java代理工厂设计模式 ProxyFactory 有一个Baby类,有Cry行为,Baby可以配一个保姆 但是作为保姆必须遵守保姆协议:能够处理Baby类Cry的行为,如喂奶、哄睡觉。
    Java中 Character方法练习:字符串中英文字母个数 5435abc54abc3AHJ5 正则:matches("[a-zA-Z0-9]{1}")
    Java 练习:字符串反转
    集合类别
    JAVA 后台
    micro focus cobol vs mainframe cobol
    Java 编码
    关于java中char占几个字节,汉字占几个字节
  • 原文地址:https://www.cnblogs.com/47Gamer/p/14279345.html
Copyright © 2011-2022 走看看