防重複送出方案
1.思路
因為服務端是使用Token來擷取使用者資訊,沒有對應的session管理機制,不能用sessionId加上使用者請求的URL作為唯一辨別。但是伺服器中的userId是用雪花算法進行計算,能確定唯一性,是以我們選擇用userId加上使用者請求的URL作為使用者請求的唯一辨別。當使用者請求POST、PUT、DELETE接口時,緩存使用者的userId加上請求的位址,設定逾時時間為2秒鐘,當這個使用者重複請求這個位址的時候,判斷是否在緩存中,如果在緩存中,則進行攔截,2秒後使用者才可以請求這個接口。
2.實作步驟
2.1編寫aop切面對所有請求到controller層的請求進行攔截
2.1在資料庫中擷取要攔截的路徑,為了防止多次請求資料庫導緻性能下降,但是又要定時的重新整理緩存,把查詢的資料加入緩存中,緩存的時間為1個小時。
2.2判斷使用者的請求是否在緩存中,如果在緩存中,則友好提示使用者不能頻繁操作。如果不在緩存中,則加入緩存,緩存失效時間為2秒鐘。
說明:這裡的緩存用的是Hazelcast,因為Hazelcast鎖和逾時時間不能在一行代碼中執行,如果并發請求實作不了防重複送出,建議替換成Redis。
/**
* 切入controller層,對配置的位址進行判斷,如果2秒内重複送出多次則進行攔截
* (攔截的位址自己進行編寫)
*/
@Before("within(com.*.*.web.controller.*)")
public void before(){
// 擷取請求路徑
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String path = request.getServletPath();
// 截取delete風格的servletPath
if (path.contains(InsuranceConstant.APL_NO)){
path = path.substring(0, path.lastIndexOf(InsuranceConstant.APL_NO));
}
String servletPath = path;
// 查找防重複路徑(查找資料庫擷取過來的資料,緩存在Hazelcast中,逾時時間為1個小時)
SysParameter sysParameter = findDuplicationPath();
if (sysParameter == null){
log.info("沒有配置防重複送出路徑,退出");
return;
}
// 判斷請求位址是否是防重複位址
String[] prmValue = sysParameter.getPrmValue().split(",");
long count = Arrays.stream(prmValue).filter(value -> value.equals(servletPath)).count();
if (count <= 0){
log.info("{}不是要檢測路徑,退出",servletPath);
return;
}
// 擷取登入的使用者Id
Long userId = JwtTokenUtils.decodeToken(request.getHeader(CommonConstant.TOKEN));
// 緩存Key
String uniqueKey = servletPath + userId;
// 檢查緩存中是否有對應的請求位址
Object result = hazelcastUtils.get(InsuranceConstant.PREVENTING_DUPLICATION_ASPECT, uniqueKey);
if (result == null){
// 設定逾時時間[2秒逾時時間]
hazelcastUtils.expire(InsuranceConstant.PREVENTING_DUPLICATION_ASPECT,uniqueKey,uniqueKey
,preDuplicationMillisecond);
log.info("請求路徑:{},進入防重複攔截,攔截時間:{}秒",servletPath,preDuplicationSecond);
}else {
log.info("請求路徑:{},{}秒内重複送出,友好提示",servletPath,preDuplicationSecond);
throw new BusinessException(InsuranceErrorCodeEnum.REQUESTS_ARE_TOO_FREQUENT);
}
}