在上周發(fā)布的 TienChin 項目視頻中,我和大家一共梳理了六種冪等性解決方案,接口冪等性處理算是一個非常常見的需求了,我們在很多項目中其實都會遇到。今天我們來看看兩種比較簡單的實現(xiàn)思路。
1. 接口冪等性實現(xiàn)方案梳理
其實接口冪等性的實現(xiàn)方案還是蠻多的,我這里和小伙伴們分享兩種比較常見的方案。
1.1 基于 Token
基于 Token 這種方案的實現(xiàn)思路很簡單,整個流程分兩步:
大致的思路就是上面這樣,當然具體的實現(xiàn)則會復雜很多,有很多細節(jié)需要注意,松哥之前也專門錄過這種方案的視頻,小伙伴們可以參考下,錄了兩個視頻,一個是基于攔截器處理的,還有一個是基于 AOP 切面處理的:
基于攔截器處理(視頻一):
基于 AOP 切面處理(視頻二):
1.2 基于請求參數(shù)校驗
最近在 TienChin 項目中使用的是另外一種方案,這種方案是基于請求參數(shù)來判斷的,如果在短時間內(nèi),同一個接口接收到的請求參數(shù)相同,那么就認為這是重復的請求,拒絕處理,大致上就是這么個思路。
相比于第一種方案,第二種方案相對來說省事一些,因為只有一次請求,不需要專門去服務(wù)端拿令牌。在高并發(fā)環(huán)境下這種方案優(yōu)勢比較明顯。
所以今天我就來和大家聊聊第二種方案的實現(xiàn),后面在 TienChin 項目視頻中也會和大家細講。
2. 基于請求參數(shù)的校驗
首先我們新建一個 Spring Boot 項目,引入 Web 和 Redis 依賴,新建完成后,先來配置一下 Redis 的基本信息,如下:
spring.redis.host=localhostspring.redis.port=6379spring.redis.password=123
為了后續(xù) Redis 操作方便,我們再來對 Redis 進行一個簡單封裝,如下:
@Componentpublic class RedisCache { @Autowired public RedisTemplate redisTemplate; public void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) { redisTemplate.opsForValue().set(key, value, timeout, timeUnit); } public T getCacheObject(final String key) { ValueOperations operation = redisTemplate.opsForValue(); return operation.get(key); }}
這個比較簡單,一個存數(shù)據(jù),一個讀數(shù)據(jù)。
接下來我們自定義一個注解,在需要進行冪等性處理的接口上,添加該注解即可,將來這個接口就會自動的進行冪等性處理。
@Inherited@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @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 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; } } /** * 驗證是否重復提交由子類實現(xiàn)具體的防重復提交的規(guī)則 * * @param request * @return * @throws Exception */ public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation);}
這個攔截器是一個抽象類,將接口方法攔截下來,然后找到接口上的 @RepeatSubmit 注解,調(diào)用 isRepeatSubmit 方法去判斷是否是重復提交的數(shù)據(jù),該方法在這里是一個抽象方法,我們需要再定義一個類繼承自這個抽象類,在新的子類中,可以有不同的冪等性判斷邏輯,這里我們就是根據(jù) URL 地址+參數(shù) 來判斷冪等性條件是否滿足:
@Componentpublic 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參數(shù)為空,獲取Parameter的數(shù)據(jù) if (StringUtils.isEmpty(nowParams)) { try { nowParams = new ObjectMapper().writeValueAsString(request.getParameterMap()); } catch (JsonProcessingException e) { e.printStackTrace(); } } Map nowDataMap = new HashMap(); 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 sessionMap = (Map) sessionObj; if (compareParams(nowDataMap, sessionMap) && compareTime(nowDataMap, sessionMap, annotation.interval())) { return true; } } redisCache.setCacheObject(cacheRepeatKey, nowDataMap, annotation.interval(), TimeUnit.MILLISECONDS); return false; } /** * 判斷參數(shù)是否相同 */ private boolean compareParams(Map nowMap, Map preMap) { String nowParams = (String) nowMap.get(REPEAT_PARAMS); String preParams = (String) preMap.get(REPEAT_PARAMS); return nowParams.equals(preParams); } /** * 判斷兩次間隔時間 */ private boolean compareTime(Map nowMap, Map 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; }}
我們來看下具體的實現(xiàn)邏輯:
好啦,做完這一切,最后我們再來配置一下攔截器即可:
@Configurationpublic class WebConfig implements WebMvcConfigurer { @Autowired RepeatSubmitInterceptor repeatSubmitInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(repeatSubmitInterceptor) .addPathPatterns(“/**”); }}
如此,我們的接口冪等性就處理好啦 在需要的時候,就可以直接在接口上使用啦:
@RestControllerpublic class HelloController { @PostMapping(“/hello”) @RepeatSubmit(interval = 100000) public String hello(@RequestBody String msg) { System.out.println(“msg = ” + msg); return “hello”; }}
好啦,公眾號后臺回復 RepeatSubmit 可以下載本文源碼哦。