SpringBoot处理接口幂等性的两种方法详解
作者:_江南一点雨 发布时间:2021-12-23 10:32:32
在上周发布的 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 {
/**
* 间隔时间(ms),小于此时间视为重复提交
*/
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;
}
}
/**
* 验证是否重复提交由子类实现具体的防重复提交的规则
*
* @param request
* @return
* @throws Exception
*/
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;
}
}
我们来看下具体的实现逻辑:
首先判断当前的请求对象是不是 RepeatedlyRequestWrapper,如果是,说明当前的请求参数是 JSON,那么就通过 IO 流将参数读取出来,这块小伙伴们要结合上篇文章共同来理解,否则可能会觉得云里雾里的,传送门JSON 数据读一次就没了,怎么办?。
如果在第一步中,并没有拿到参数,那么说明参数可能并不是 JSON 格式,而是 key-value 格式,那么就以 key-value 的方式读取出来参数,并将之转为一个 JSON 字符串。
接下来构造一个 Map,将前面读取到的参数和当前时间存入到 Map 中。
接下来构造存到 Redis 中的数据的 key,这个 key 由固定前缀 + 请求 URL 地址 + 请求头的认证令牌组成,这块请求头的令牌还是非常重要需要有的,只有这样才能区分出来当前用户提交的数据(如果是 RESTful 风格的接口,那么为了区分,也可以将接口的请求方法作为参数拼接到 key 中)。
接下来就去 Redis 中获取数据,获取到之后,分别去比较参数是否相同以及时间是否过期。
如果判断都没问题,返回 true,表示这个请求重复了。
否则返回说明这是用户对这个接口第一次提交数据或者是已经过了时间窗口了,那么就把参数字符串重新缓存到 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";
}
}
来源:https://blog.csdn.net/u012702547/article/details/125376668


猜你喜欢
- 普通商户分账功能分账比例:目前只有”低比例分账“小于等于30%分账,分账金额需要减去(千6)手续费.每一张订单只能分发,当前订单总额的百分之
- 1.常见字符串编码常见的字符串编码有:LATIN1 只能保存ASCII字符,又称ISO-8859-1。UTF-8 变长字节编码,一个字符需要
- 1. 对象的创建对象创建的主要流程:1.类加载检查虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引
- 1.添加加载更多布局1_初始化和隐藏代码在RefreshListView构造方法中调用private void initFooterView
- 1、首先 当然是启动genymotion2、然后Tomcat ,启动tomcat。。如图将请求的URL地址变为10.0.3.2 ,比如在电脑
- 1.修改主站点的elasticsearch.yml添加一下行:xpack.security.enabled: true2.生成安全秘钥切到E
- 我们知道,当我们按返回或Home键退出应用程序的界面时,应用程序会在后台被挂起。这么设计的好处是,由于应用被系统缓存在内存中,那么在用户打开
- 本文实例为大家分享了Unity2D游戏回旋镖实现的具体代码,供大家参考,具体内容如下以下我举出2种同使用情况的回旋镖那么回旋镖需要怎么做呢?
- 用于字符串替换,你还在用以下的这种方法吗?String.format(String format, Object... args)这是Str
- 一、简介当我们没有在子类构造函数中写上 base(),默认会先调用父类中无参的构造函数,再调用子类。当在有参构造函数后写上base时,只调用
- C# windows语音识别与朗读示例,供大家参考,具体内容如下本示例通过windows语音识别功能进行语音识别和文本朗读。打开window
- jmap:Java内存映像工具jmap(Memory Map for Java)命令用于生成堆转储快照(一般称为heapdump或dump文
- 1、用ASCII码判断在 ASCII码表中,英文的范围是0-127,而汉字则是大于127,具体代码如下:string text = &quo
- 效果图界面绘制操作private Point? _startPoint = null; private void Contain
- 实践过程效果代码public partial class Form1 : Form{ public Form1()
- 背景介绍你刚从学校毕业后,到新公司实习,试用期又被毕业,然后你又不得不出来面试,好在面试的时候碰到个美女面试官!面试官: 小伙子,
- java实现字符串匹配暴力匹配/** * 暴力匹配 * * @param str1 需要找的总字符串 * @param str2 需要找到的
- 1.定义指向非法的内存地址指针叫作野指针(Wild Pointer),也叫悬挂指针(Dangling Pointer),意为无法正常使用的指
- 前言有时候我们想克隆一个List去做别的事,而不影响原来的List,我们直接在list后面加上小点点,发现并没有Clone这样的扩展函数。这
- 前期准备首先要先明确有个大体的思路,要实现什么样的功能,了解完成整个模块要运用到哪些方面的知识,以及从做的过程中去发现自己的不足。技术方面的