新聞中心
1. 接口冪等性實(shí)現(xiàn)方案梳理
其實(shí)接口冪等性的實(shí)現(xiàn)方案還是蠻多的,我這里和小伙伴們分享兩種比較常見(jià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é)需要注意,松哥之前也專門錄過(guò)這種方案的視頻,小伙伴們可以參考下,錄了兩個(gè)視頻,一個(gè)是基于攔截器處理的,還有一個(gè)是基于 AOP 切面處理的:
基于攔截器處理(視頻一):
基于 AOP 切面處理(視頻二):
1.2 基于請(qǐng)求參數(shù)校驗(yàn)
最近在 TienChin 項(xiàng)目中使用的是另外一種方案,這種方案是基于請(qǐng)求參數(shù)來(lái)判斷的,如果在短時(shí)間內(nèi),同一個(gè)接口接收到的請(qǐng)求參數(shù)相同,那么就認(rèn)為這是重復(fù)的請(qǐng)求,拒絕處理,大致上就是這么個(gè)思路。
相比于第一種方案,第二種方案相對(duì)來(lái)說(shuō)省事一些,因?yàn)橹挥幸淮握?qǐng)求,不需要專門去服務(wù)端拿令牌。在高并發(fā)環(huán)境下這種方案優(yōu)勢(shì)比較明顯。
所以今天我就來(lái)和大家聊聊第二種方案的實(shí)現(xiàn),后面在 TienChin 項(xiàng)目視頻中也會(huì)和大家細(xì)講。
2. 基于請(qǐng)求參數(shù)的校驗(yàn)
首先我們新建一個(gè) Spring Boot 項(xiàng)目,引入 Web 和 Redis 依賴,新建完成后,先來(lái)配置一下 Redis 的基本信息,如下:
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=123
為了后續(xù) Redis 操作方便,我們?cè)賮?lái)對(duì) Redis 進(jìn)行一個(gè)簡(jiǎn)單封裝,如下:
@Component
public class RedisCache {
@Autowired
public RedisTemplate redisTemplate;
publicvoid setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
publicT getCacheObject(final String key) {
ValueOperationsoperation = redisTemplate.opsForValue();
return operation.get(key);
}
}
這個(gè)比較簡(jiǎn)單,一個(gè)存數(shù)據(jù),一個(gè)讀數(shù)據(jù)。
接下來(lái)我們自定義一個(gè)注解,在需要進(jìn)行冪等性處理的接口上,添加該注解即可,將來(lái)這個(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è)注解我們通過(guò)攔截器來(lái)進(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 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è)抽象類,將接口方法攔截下來(lái),然后找到接口上的 @RepeatSubmit 注解,調(diào)用 isRepeatSubmit 方法去判斷是否是重復(fù)提交的數(shù)據(jù),該方法在這里是一個(gè)抽象方法,我們需要再定義一個(gè)類繼承自這個(gè)抽象類,在新的子類中,可以有不同的冪等性判斷邏輯,這里我們就是根據(jù) URL 地址+參數(shù) 來(lái)判斷冪等性條件是否滿足:
@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();
}
}
MapnowDataMap = new HashMap ();
nowDataMap.put(REPEAT_PARAMS, nowParams);
nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());
// 請(qǐng)求地址(作為存放cache的key值)
String url = request.getRequestURI();
// 唯一值(沒(méi)有消息頭則使用請(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) {
MapsessionMap = (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(MapnowMap, Map preMap) {
String nowParams = (String) nowMap.get(REPEAT_PARAMS);
String preParams = (String) preMap.get(REPEAT_PARAMS);
return nowParams.equals(preParams);
}
/**
* 判斷兩次間隔時(shí)間
*/
private boolean compareTime(MapnowMap, 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;
}
}
我們來(lái)看下具體的實(shí)現(xiàn)邏輯:
- 首先判斷當(dāng)前的請(qǐng)求對(duì)象是不是 RepeatedlyRequestWrapper,如果是,說(shuō)明當(dāng)前的請(qǐng)求參數(shù)是 JSON,那么就通過(guò) IO 流將參數(shù)讀取出來(lái),這塊小伙伴們要結(jié)合上篇文章共同來(lái)理解,否則可能會(huì)覺(jué)得云里霧里的,傳送門JSON 數(shù)據(jù)讀一次就沒(méi)了,怎么辦?。
- 如果在第一步中,并沒(méi)有拿到參數(shù),那么說(shuō)明參數(shù)可能并不是 JSON 格式,而是 key-value 格式,那么就以 key-value 的方式讀取出來(lái)參數(shù),并將之轉(zhuǎn)為一個(gè) JSON 字符串。
- 接下來(lái)構(gòu)造一個(gè) Map,將前面讀取到的參數(shù)和當(dāng)前時(shí)間存入到 Map 中。
- 接下來(lái)構(gòu)造存到 Redis 中的數(shù)據(jù)的 key,這個(gè) key 由固定前綴 + 請(qǐng)求 URL 地址 + 請(qǐng)求頭的認(rèn)證令牌組成,這塊請(qǐng)求頭的令牌還是非常重要需要有的,只有這樣才能區(qū)分出來(lái)當(dāng)前用戶提交的數(shù)據(jù)(如果是 RESTful 風(fēng)格的接口,那么為了區(qū)分,也可以將接口的請(qǐng)求方法作為參數(shù)拼接到 key 中)。
- 接下來(lái)就去 Redis 中獲取數(shù)據(jù),獲取到之后,分別去比較參數(shù)是否相同以及時(shí)間是否過(guò)期。
- 如果判斷都沒(méi)問(wèn)題,返回 true,表示這個(gè)請(qǐng)求重復(fù)了。
- 否則返回說(shuō)明這是用戶對(duì)這個(gè)接口第一次提交數(shù)據(jù)或者是已經(jīng)過(guò)了時(shí)間窗口了,那么就把參數(shù)字符串重新緩存到 Redis 中,并返回 false,表示請(qǐng)求沒(méi)問(wèn)題。
好啦,做完這一切,最后我們?cè)賮?lái)配置一下攔截器即可:
@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";
}
}
文章題目:處理接口冪等性的兩種常見(jiàn)方案
URL網(wǎng)址:http://m.fisionsoft.com.cn/article/dpppgej.html


咨詢
建站咨詢
