前言
防止接口重復提交有跟多種方法,可以在前端做處理。同樣在后端也能處理,而且后端的處理也有很多中方法。最先能想到的就是加鎖,也可以直接在該接口的實現過程中進行處理(可以參考防止數據重復提交的6種方法(超簡單)!),本文主要介紹另一種借助AOP實現的方法。
AOP
關于AOP就不做過多贅述,可以參考我的另一篇文章Spring框架(下半部分 -AOP)。主要是借助它能增強方法的功能,對接口做以下處理,這個方法跟直接在接口種處理相似,話不多說,我們直接開始吧。
自定義注解
我們要靈活的使用AOP,注解是必不可少的,能幫我們更加便捷靈活的處理。我們先創建一個Submit注解,有該注解的接口就是我們要使用AOP處理的接口。
package com.blog.annotation;import java.lang.annotation.*;@Documented
@Retention(RetentionPolicy.RUNTIME) // 注解的存活時間
@Target(ElementType.METHOD) // 作用在方法上
public @interface Submit {/*** 提交的間隔時間* 默認是10s* @return*/long expire() default 10000;
}
AOP的實現
其實使用AOP都有一個很創建的模板,我先貼出來,然后解釋。
@Aspect
@Component
@Slf4j
public class SubmitAspect {@Pointcut("@annotation(com.blog.annotation.Submit)")public void pt() {}@Around("pt()")public Object around(ProceedingJoinPoint point) {}
}
@Pointcut("@annotation(com.blog.annotation.Submit)")
就是切入點表達式,它的參數就是指定我們要處理,@Around("pt()")
表明我們使用環繞通知來處理。具體的在我剛剛提到的另一篇博客中,感興趣的可以仔細的了解一下。
接下來我們就要考慮該如何實現,防止接口重復提交就是說如果該接口提交過了,再來一次提交我們就不讓他去執行,直接返回。現在就有一個問題了,我們該如何知道這個接口提交沒提交過?我們是不是可以把提交過的接口保存下來,如果來了一個提交我們就去查找,如果找到了我們就不如他提交。
if (接口 not in 接口集合) {return "請勿重復提交";
}
// 說明接口沒有提交,我們就執行該接口的方法
// 最重要的一點是把該接口存儲到接口集合中
...執行提交操作...
接口集合.insert(接口)
所以我們就需要考慮使用哪些集合?這個接口該怎么存儲?怎么執行原方法的操作?什么時候用戶還能再次提交代碼?等等,這些都是我們要考慮的問題。
關于集合的使用,我們首先能想到的是list、set、map等等,但是考慮到并發安全,我們應該使用線程安全的集合例如ConcurrentHashMap、CopyOnWriteArrayList等等。我們還要解決什么時候用戶還能再次提交代碼,我們可以設置一個實現,所以更加推薦ConcurrentHashMap,其key值就是我們為每一個接口構建的key(使用類名+方法名),value就是我們設置的時間。
想到這還有一個問題,我們為每一個接口構建key,如果有多個用戶那么他們的key就是一樣的,可事實上每個用戶的同一接口的key一定是不能一樣的,否則他提交了我提交不了,這憑什么?所以我們再構建每一個接口的key時加上當前用戶的唯一標識,使用該用戶的id就行。
那么又該如何獲取到當前用戶的id呢? 在這里我們ThreadLocal就可以,ThreadLocal也是很重要的,如果不是很了解,建議花點時間去認識它。在這里我們只需要知道,他是獨立于線程之外的,每一個線程又一個獨自的ThreadLocal ,也就是說,我們把每一個用戶都存儲在ThreadLocal 中。要的時候直接get就行。
到這里其實核心的問題都已經解決了,剩下的就是一些細節問題,在自己寫的時候就能注意到。這里給出我的實現。我使用的redis實現,因為它設置過期時間會自動清除,不需要我們手動去清除,再加上redis是天生支持高并發。
SUBMIT_KEY_PREFIX和NOT_SUBMIT_REPEATEDLY都是一個常量而已,不用過多注意。
package com.blog.aspect;import com.alibaba.fastjson.JSON;
import com.blog.annotation.Submit;
import com.blog.utils.JWTUtils;
import com.blog.utils.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;
import java.time.Duration;import static com.blog.domain.vo.ErrorCode.NOT_SUBMIT_REPEATEDLY;
import static com.blog.utils.ConstantValue.SUBMIT_KEY_PREFIX;@Aspect
@Component
@Slf4j
public class SubmitAspect {@Autowiredprivate RedisTemplate<String, String> redisTemplate;@Pointcut("@annotation(com.blog.annotation.Submit)")public void pt() {}@Around("pt()")public Object around(ProceedingJoinPoint point) {try {
// User user = UserThreadLocal.get();
// String UserId = user.getId();// 假設這里是從ThreadLocal獲取到的用戶id。String UserId = "123456";Signature signature = point.getSignature();// 獲取當前類名String className = point.getTarget().getClass().getSimpleName();// 獲取當前方法名String methodName = signature.getName();// 拿到該方法Method method = ((MethodSignature) signature).getMethod();// 獲取Submit注解Submit annotation = method.getAnnotation(Submit.class);// 獲取過期時間long expire = annotation.expire();// 設置key值,每個用戶對與每一個接口的key都是一樣的String key = SUBMIT_KEY_PREFIX + DigestUtils.md5Hex(UserId) + "::" + className + "::" + methodName;// 首先查看是否已經提交過String value = redisTemplate.opsForValue().get(key);if (StringUtils.isNoneEmpty(value)) {return Result.error(NOT_SUBMIT_REPEATEDLY.getCode(), NOT_SUBMIT_REPEATEDLY.getMsg());}// 沒有提交過就執行原方法Object proceed = point.proceed();redisTemplate.opsForValue().set(key, JSON.toJSONString(proceed), Duration.ofMillis(expire));return proceed;} catch (Throwable throwable) {throwable.printStackTrace();}return Result.error(-999, "系統異常");}
}
測試
接下來我們使用ApiPost進行測試,由于我們給定了id,所以我們只能測試單用戶的,如果想測試多用戶的,可以在請求路徑中加上一個id,來模擬多用戶。
間隔0ms,調用5次,只有一次成功,失敗的幾次,這里就不截圖了。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-e2fHCLrs-1720583474961)(https://i-blog.csdnimg.cn/direct/e0b09889def54e4494172c9edc1571e1.png)]
間隔11000ms,調用2次,每次都成功,這是因為我們的冷靜窗口是10000ms。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-zG3MVA4q-1720583474962)(https://i-blog.csdnimg.cn/direct/a0153a775a524fd0821df825fa3154ff.png)]