我的编程空间,编程开发者的网络收藏夹
学习永远不晚

SpringBoot处理接口幂等性的两种方法详解

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

SpringBoot处理接口幂等性的两种方法详解

在上周发布的 TienChin 项目视频中,我和大家一共梳理了六种幂等性解决方案,接口幂等性处理算是一个非常常见的需求了,我们在很多项目中其实都会遇到。今天我们来看看两种比较简单的实现思路。

1. 接口幂等性实现方案梳理

其实接口幂等性的实现方案还是蛮多的,我这里和小伙伴们分享两种比较常见的方案。

1.1 基于 Token

基于 Token 这种方案的实现思路很简单,整个流程分两步:

  • 客户端发送请求,从服务端获取一个 Token 令牌,每次请求获取到的都是一个全新的令牌。
  • 客户端发送请求的时候,携带上第一步的令牌,处理请求之前,先校验令牌是否存在,当请求处理成功,就把令牌删除掉。

大致的思路就是上面这样,当然具体的实现则会复杂很多,有很多细节需要注意

1.2 基于请求参数校验

最近在 TienChin 项目中使用的是另外一种方案,这种方案是基于请求参数来判断的,如果在短时间内,同一个接口接收到的请求参数相同,那么就认为这是重复的请求,拒绝处理,大致上就是这么个思路。

相比于第一种方案,第二种方案相对来说省事一些,因为只有一次请求,不需要专门去服务端拿令牌。在高并发环境下这种方案优势比较明显。

所以今天我就来和大家聊聊第二种方案的实现,后面在 TienChin 项目视频中也会和大家细讲。

2. 基于请求参数的校验

首先我们新建一个 Spring Boot 项目,引入 Web 和 Redis 依赖,新建完成后,先来配置一下 Redis 的基本信息,如下:

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=123

为了后续 Redis 操作方便,我们再来对 Redis 进行一个简单封装,如下:

@Component
public class RedisCache {
    @Autowired
    public RedisTemplate redisTemplate;
    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }
    public <T> T getCacheObject(final String key) {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }
}

这个比较简单,一个存数据,一个读数据。

接下来我们自定义一个注解,在需要进行幂等性处理的接口上,添加该注解即可,将来这个接口就会自动的进行幂等性处理。

@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
    
    public int interval() default 5000;

    
    public String message() default "不允许重复提交,请稍候再试";
}

这个注解我们通过拦截器来进行解析,解析代码如下:

public abstract class RepeatSubmitInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
            if (annotation != null) {
                if (this.isRepeatSubmit(request, annotation)) {
                    Map<String, Object> map = new HashMap<>();
                    map.put("status", 500);
                    map.put("msg", annotation.message());
                    response.setContentType("application/json;charset=utf-8");
                    response.getWriter().write(new ObjectMapper().writeValueAsString(map));
                    return false;
                }
            }
            return true;
        } else {
            return true;
        }
    }

    
    public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation);
}

这个拦截器是一个抽象类,将接口方法拦截下来,然后找到接口上的 @RepeatSubmit 注解,调用 isRepeatSubmit 方法去判断是否是重复提交的数据,该方法在这里是一个抽象方法,我们需要再定义一个类继承自这个抽象类,在新的子类中,可以有不同的幂等性判断逻辑,这里我们就是根据 URL 地址+参数 来判断幂等性条件是否满足:

@Component
public class SameUrlDataInterceptor extends RepeatSubmitInterceptor {
    public final String REPEAT_PARAMS = "repeatParams";

    public final String REPEAT_TIME = "repeatTime";
    public final static String REPEAT_SUBMIT_KEY = "REPEAT_SUBMIT_KEY";

    private String header = "Authorization";

    @Autowired
    private RedisCache redisCache;

    @SuppressWarnings("unchecked")
    @Override
    public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) {
        String nowParams = "";
        if (request instanceof RepeatedlyRequestWrapper) {
            RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
            try {
                nowParams = repeatedlyRequest.getReader().readLine();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        // body参数为空,获取Parameter的数据
        if (StringUtils.isEmpty(nowParams)) {
            try {
                nowParams = new ObjectMapper().writeValueAsString(request.getParameterMap());
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }
        Map<String, Object> nowDataMap = new HashMap<String, Object>();
        nowDataMap.put(REPEAT_PARAMS, nowParams);
        nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());

        // 请求地址(作为存放cache的key值)
        String url = request.getRequestURI();

        // 唯一值(没有消息头则使用请求地址)
        String submitKey = request.getHeader(header);

        // 唯一标识(指定key + url + 消息头)
        String cacheRepeatKey = REPEAT_SUBMIT_KEY + url + submitKey;

        Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);
        if (sessionObj != null) {
            Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;
            if (compareParams(nowDataMap, sessionMap) && compareTime(nowDataMap, sessionMap, annotation.interval())) {
                return true;
            }
        }
        redisCache.setCacheObject(cacheRepeatKey, nowDataMap, annotation.interval(), TimeUnit.MILLISECONDS);
        return false;
    }

    
    private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap) {
        String nowParams = (String) nowMap.get(REPEAT_PARAMS);
        String preParams = (String) preMap.get(REPEAT_PARAMS);
        return nowParams.equals(preParams);
    }

    
    private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap, int interval) {
        long time1 = (Long) nowMap.get(REPEAT_TIME);
        long time2 = (Long) preMap.get(REPEAT_TIME);
        if ((time1 - time2) < interval) {
            return true;
        }
        return false;
    }
}

我们来看下具体的实现逻辑:

  1. 首先判断当前的请求对象是不是 RepeatedlyRequestWrapper,如果是,说明当前的请求参数是 JSON,那么就通过 IO 流将参数读取出来,这块小伙伴们要结合上篇文章共同来理解,否则可能会觉得云里雾里的,传送门JSON 数据读一次就没了,怎么办?。
  2. 如果在第一步中,并没有拿到参数,那么说明参数可能并不是 JSON 格式,而是 key-value 格式,那么就以 key-value 的方式读取出来参数,并将之转为一个 JSON 字符串。
  3. 接下来构造一个 Map,将前面读取到的参数和当前时间存入到 Map 中。
  4. 接下来构造存到 Redis 中的数据的 key,这个 key 由固定前缀 + 请求 URL 地址 + 请求头的认证令牌组成,这块请求头的令牌还是非常重要需要有的,只有这样才能区分出来当前用户提交的数据(如果是 RESTful 风格的接口,那么为了区分,也可以将接口的请求方法作为参数拼接到 key 中)。
  5. 接下来就去 Redis 中获取数据,获取到之后,分别去比较参数是否相同以及时间是否过期。
  6. 如果判断都没问题,返回 true,表示这个请求重复了。
  7. 否则返回说明这是用户对这个接口第一次提交数据或者是已经过了时间窗口了,那么就把参数字符串重新缓存到 Redis 中,并返回 false,表示请求没问题。

好啦,做完这一切,最后我们再来配置一下拦截器即可:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    RepeatSubmitInterceptor repeatSubmitInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(repeatSubmitInterceptor)
                .addPathPatterns("/**");
    }
}

如此,我们的接口幂等性就处理好啦~在需要的时候,就可以直接在接口上使用啦:

@RestController
public class HelloController {

    @PostMapping("/hello")
    @RepeatSubmit(interval = 100000)
    public String hello(@RequestBody String msg) {
        System.out.println("msg = " + msg);
        return "hello";
    }
}

到此这篇关于SpringBoot处理接口幂等性的两种方法详解的文章就介绍到这了,更多相关SpringBoot处理接口幂等性内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

SpringBoot处理接口幂等性的两种方法详解

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

浅谈Redis处理接口幂等性的两种方案

目录一、接口幂等性1.1、什么是接口幂等性1.2、为什么需要实现幂等性1.3、引入幂等性后对系统的影响二、如何设计幂等2.1、全局的唯一性ID2.2、幂等设计的基本流程三、接口幂等性常见解决方案3.1、下游传递唯一请求编号3.2、防重 To
2022-08-15

使用SpringBoot实现接口幂等性的方法有哪些

使用SpringBoot实现接口幂等性的方法有哪些?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。一、什么是幂等性幂等是一个数学与计算机学概念,在数学中某一元运算为幂等时,其
2023-06-07

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录