SpringBoot處理接口冪等性的兩種方法詳解
在上周發(fā)布的 TienChin 項(xiàng)目視頻中,我和大家一共梳理了六種冪等性解決方案,接口冪等性處理算是一個(gè)非常常見的需求了,我們?cè)诤芏囗?xiàng)目中其實(shí)都會(huì)遇到。今天我們來看看兩種比較簡(jiǎn)單的實(shí)現(xiàn)思路。
1. 接口冪等性實(shí)現(xiàn)方案梳理
其實(shí)接口冪等性的實(shí)現(xiàn)方案還是蠻多的,我這里和小伙伴們分享兩種比較常見的方案。
1.1 基于 Token
基于 Token 這種方案的實(shí)現(xiàn)思路很簡(jiǎn)單,整個(gè)流程分兩步:
- 客戶端發(fā)送請(qǐng)求,從服務(wù)端獲取一個(gè) Token 令牌,每次請(qǐng)求獲取到的都是一個(gè)全新的令牌。
- 客戶端發(fā)送請(qǐng)求的時(shí)候,攜帶上第一步的令牌,處理請(qǐng)求之前,先校驗(yàn)令牌是否存在,當(dāng)請(qǐng)求處理成功,就把令牌刪除掉。
大致的思路就是上面這樣,當(dāng)然具體的實(shí)現(xiàn)則會(huì)復(fù)雜很多,有很多細(xì)節(jié)需要注意
1.2 基于請(qǐng)求參數(shù)校驗(yàn)
最近在 TienChin 項(xiàng)目中使用的是另外一種方案,這種方案是基于請(qǐng)求參數(shù)來判斷的,如果在短時(shí)間內(nèi),同一個(gè)接口接收到的請(qǐng)求參數(shù)相同,那么就認(rèn)為這是重復(fù)的請(qǐng)求,拒絕處理,大致上就是這么個(gè)思路。
相比于第一種方案,第二種方案相對(duì)來說省事一些,因?yàn)橹挥幸淮握?qǐng)求,不需要專門去服務(wù)端拿令牌。在高并發(fā)環(huán)境下這種方案優(yōu)勢(shì)比較明顯。
所以今天我就來和大家聊聊第二種方案的實(shí)現(xiàn),后面在 TienChin 項(xiàng)目視頻中也會(huì)和大家細(xì)講。
2. 基于請(qǐng)求參數(shù)的校驗(yàn)
首先我們新建一個(gè) Spring Boot 項(xiàng)目,引入 Web 和 Redis 依賴,新建完成后,先來配置一下 Redis 的基本信息,如下:
spring.redis.host=localhost spring.redis.port=6379 spring.redis.password=123
為了后續(xù) Redis 操作方便,我們?cè)賮韺?duì) Redis 進(jìn)行一個(gè)簡(jiǎn)單封裝,如下:
@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); } }
這個(gè)比較簡(jiǎn)單,一個(gè)存數(shù)據(jù),一個(gè)讀數(shù)據(jù)。
接下來我們自定義一個(gè)注解,在需要進(jìn)行冪等性處理的接口上,添加該注解即可,將來這個(gè)接口就會(huì)自動(dòng)的進(jìn)行冪等性處理。
@Inherited @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RepeatSubmit { /** * 間隔時(shí)間(ms),小于此時(shí)間視為重復(fù)提交 */ public int interval() default 5000; /** * 提示消息 */ public String message() default "不允許重復(fù)提交,請(qǐng)稍候再試"; }
這個(gè)注解我們通過攔截器來進(jìn)行解析,解析代碼如下:
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; } } /** * 驗(yàn)證是否重復(fù)提交由子類實(shí)現(xiàn)具體的防重復(fù)提交的規(guī)則 * * @param request * @return * @throws Exception */ public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation); }
這個(gè)攔截器是一個(gè)抽象類,將接口方法攔截下來,然后找到接口上的 @RepeatSubmit 注解,調(diào)用 isRepeatSubmit 方法去判斷是否是重復(fù)提交的數(shù)據(jù),該方法在這里是一個(gè)抽象方法,我們需要再定義一個(gè)類繼承自這個(gè)抽象類,在新的子類中,可以有不同的冪等性判斷邏輯,這里我們就是根據(jù) URL 地址+參數(shù) 來判斷冪等性條件是否滿足:
@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參數(shù)為空,獲取Parameter的數(shù)據(jù) 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()); // 請(qǐng)求地址(作為存放cache的key值) String url = request.getRequestURI(); // 唯一值(沒有消息頭則使用請(qǐng)求地址) String submitKey = request.getHeader(header); // 唯一標(biāo)識(shí)(指定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; } /** * 判斷參數(shù)是否相同 */ 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); } /** * 判斷兩次間隔時(shí)間 */ 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; } }
我們來看下具體的實(shí)現(xiàn)邏輯:
- 首先判斷當(dāng)前的請(qǐng)求對(duì)象是不是 RepeatedlyRequestWrapper,如果是,說明當(dāng)前的請(qǐng)求參數(shù)是 JSON,那么就通過 IO 流將參數(shù)讀取出來,這塊小伙伴們要結(jié)合上篇文章共同來理解,否則可能會(huì)覺得云里霧里的,傳送門JSON 數(shù)據(jù)讀一次就沒了,怎么辦?。
- 如果在第一步中,并沒有拿到參數(shù),那么說明參數(shù)可能并不是 JSON 格式,而是 key-value 格式,那么就以 key-value 的方式讀取出來參數(shù),并將之轉(zhuǎn)為一個(gè) JSON 字符串。
- 接下來構(gòu)造一個(gè) Map,將前面讀取到的參數(shù)和當(dāng)前時(shí)間存入到 Map 中。
- 接下來構(gòu)造存到 Redis 中的數(shù)據(jù)的 key,這個(gè) key 由固定前綴 + 請(qǐng)求 URL 地址 + 請(qǐng)求頭的認(rèn)證令牌組成,這塊請(qǐng)求頭的令牌還是非常重要需要有的,只有這樣才能區(qū)分出來當(dāng)前用戶提交的數(shù)據(jù)(如果是 RESTful 風(fēng)格的接口,那么為了區(qū)分,也可以將接口的請(qǐng)求方法作為參數(shù)拼接到 key 中)。
- 接下來就去 Redis 中獲取數(shù)據(jù),獲取到之后,分別去比較參數(shù)是否相同以及時(shí)間是否過期。
- 如果判斷都沒問題,返回 true,表示這個(gè)請(qǐng)求重復(fù)了。
- 否則返回說明這是用戶對(duì)這個(gè)接口第一次提交數(shù)據(jù)或者是已經(jīng)過了時(shí)間窗口了,那么就把參數(shù)字符串重新緩存到 Redis 中,并返回 false,表示請(qǐng)求沒問題。
好啦,做完這一切,最后我們?cè)賮砼渲靡幌聰r截器即可:
@Configuration public class WebConfig implements WebMvcConfigurer { @Autowired RepeatSubmitInterceptor repeatSubmitInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(repeatSubmitInterceptor) .addPathPatterns("/**"); } }
如此,我們的接口冪等性就處理好啦~在需要的時(shí)候,就可以直接在接口上使用啦:
@RestController public class HelloController { @PostMapping("/hello") @RepeatSubmit(interval = 100000) public String hello(@RequestBody String msg) { System.out.println("msg = " + msg); return "hello"; } }
到此這篇關(guān)于SpringBoot處理接口冪等性的兩種方法詳解的文章就介紹到這了,更多相關(guān)SpringBoot處理接口冪等性內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java?Chassis3應(yīng)用視角的配置管理技術(shù)解密
這篇文章主要為大家介紹了Java?Chassis3應(yīng)用視角的配置管理相關(guān)的機(jī)制和背后故事,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01Java PriorityQueue優(yōu)點(diǎn)和缺點(diǎn)面試精講
這篇文章主要為大家介紹了Java面試中PriorityQueue的優(yōu)點(diǎn)和缺點(diǎn)及使用注意詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10SpringMVC 向jsp頁(yè)面?zhèn)鬟f數(shù)據(jù)庫(kù)讀取到的值方法
下面小編就為大家分享一篇SpringMVC 向jsp頁(yè)面?zhèn)鬟f數(shù)據(jù)庫(kù)讀取到的值方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-03-03springboot項(xiàng)目打成war包部署到tomcat遇到的一些問題
這篇文章主要介紹了springboot項(xiàng)目打成war包部署到tomcat遇到的一些問題,需要的朋友可以參考下2017-06-06Vue3源碼解讀effectScope API及實(shí)現(xiàn)原理
這篇文章主要為大家介紹了Vue3源碼解讀effectScope API及實(shí)現(xiàn)原理,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03mybatisplus添加真正的批量新增、批量更新的實(shí)現(xiàn)
這篇文章主要介紹了mybatisplus添加真正的批量新增、批量更新的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03IDEA Ultimate2020.2版本配置Tomcat詳細(xì)教程
這篇文章主要介紹了IDEA Ultimate2020.2版本配置Tomcat教程,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09