在上周公布的 TienChin 我的项目视频中,我和大家一共梳理了六种幂等性解决方案,接口幂等性解决算是一个十分常见的需要了,咱们在很多我的项目中其实都会遇到。明天咱们来看看两种比较简单的实现思路。
1. 接口幂等性实现计划梳理
其实接口幂等性的实现计划还是蛮多的,我这里和小伙伴们分享两种比拟常见的计划。
1.1 基于 Token
基于 Token 这种计划的实现思路很简略,整个流程分两步:
- 客户端发送申请,从服务端获取一个 Token 令牌,每次申请获取到的都是一个全新的令牌。
- 客户端发送申请的时候,携带上第一步的令牌,解决申请之前,先校验令牌是否存在,当申请解决胜利,就把令牌删除掉。
大抵的思路就是下面这样,当然具体的实现则会简单很多,有很多细节须要留神,松哥之前也专门录过这种计划的视频,小伙伴们能够参考下,录了两个视频,一个是基于拦截器解决的,还有一个是基于 AOP 切面解决的:
基于拦截器解决(视频一):
基于 AOP 切面解决(视频二):
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";
}
}
好啦,公众号后盾回复 RepeatSubmit 能够下载本文源码哦。