在傳統(tǒng)的web項目中,防止重復提交访得,通常做法是:后端生成一個唯一的提交令牌(uuid)龙亲,并存儲在服務端。頁面提交請求攜帶這個提交令牌悍抑,后端驗證并在第一次驗證后刪除該令牌鳄炉,保證提交請求的唯一性。
上述的思路其實沒有問題的搜骡,但是需要前后端都稍加改動拂盯,如果在業(yè)務開發(fā)完在加這個的話,改動量未免有些大了记靡,本節(jié)的實現(xiàn)方案無需前端配合谈竿,純后端處理。
思路
- 自定義注解
@NoRepeatSubmit
標記所有Controller中的提交請求 - 通過AOP 對所有標記了
@NoRepeatSubmit
的方法攔截 - 在業(yè)務方法執(zhí)行前摸吠,獲取當前用戶的 token(或者JSessionId)+ 當前請求地址空凸,作為一個唯一 KEY,去獲取 Redis 分布式鎖(如果此時并發(fā)獲取寸痢,只有一個線程會成功獲取鎖)
- 業(yè)務方法執(zhí)行后呀洲,釋放鎖
關于Redis 分布式鎖
- 不了解的同學戳這里 ==> Redis分布式鎖的正確實現(xiàn)方式
- 使用Redis 是為了在負載均衡部署,如果是單機的部署的項目可以使用一個線程安全的本地Cache 替代 Redis
Code
這里只貼出 AOP 類和測試類轿腺,完整代碼見 ==> Gitee
@Aspect
@Component
public class RepeatSubmitAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(RepeatSubmitAspect.class);
@Autowired
private RedisLock redisLock;
@Pointcut("@annotation(com.gitee.taven.aop.NoRepeatSubmit)")
public void pointCut() {}
@Around("pointCut()")
public Object before(ProceedingJoinPoint pjp) {
try {
HttpServletRequest request = RequestUtils.getRequest();
Assert.notNull(request, "request can not null");
// 此處可以用token或者JSessionId
String token = request.getHeader("Authorization");
String path = request.getServletPath();
String key = getKey(token, path);
String clientId = getClientId();
boolean isSuccess = redisLock.tryLock(key, clientId, 10);
LOGGER.info("tryLock key = [{}], clientId = [{}]", key, clientId);
if (isSuccess) {
LOGGER.info("tryLock success, key = [{}], clientId = [{}]", key, clientId);
// 獲取鎖成功, 執(zhí)行進程
Object result = pjp.proceed();
// 解鎖
redisLock.releaseLock(key, clientId);
LOGGER.info("releaseLock success, key = [{}], clientId = [{}]", key, clientId);
return result;
} else {
// 獲取鎖失敗两嘴,認為是重復提交的請求
LOGGER.info("tryLock fail, key = [{}]", key);
return new ApiResult(200, "重復請求丛楚,請稍后再試", null);
}
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return new ApiResult(500, "系統(tǒng)異常", null);
}
private String getKey(String token, String path) {
return token + path;
}
private String getClientId() {
return UUID.randomUUID().toString();
}
}
多線程測試
測試代碼如下族壳,模擬十個請求并發(fā)同時提交
@Component
public class RunTest implements ApplicationRunner {
private static final Logger LOGGER = LoggerFactory.getLogger(RunTest.class);
@Autowired
private RestTemplate restTemplate;
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("執(zhí)行多線程測試");
String url="http://localhost:8000/submit";
CountDownLatch countDownLatch = new CountDownLatch(1);
ExecutorService executorService = Executors.newFixedThreadPool(10);
for(int i=0; i<10; i++){
String userId = "userId" + i;
HttpEntity request = buildRequest(userId);
executorService.submit(() -> {
try {
countDownLatch.await();
System.out.println("Thread:"+Thread.currentThread().getName()+", time:"+System.currentTimeMillis());
ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
System.out.println("Thread:"+Thread.currentThread().getName() + "," + response.getBody());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
countDownLatch.countDown();
}
private HttpEntity buildRequest(String userId) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "yourToken");
Map<String, Object> body = new HashMap<>();
body.put("userId", userId);
return new HttpEntity<>(body, headers);
}
}
成功防止重復提交,控制臺日志如下趣些,可以看到十個線程的啟動時間幾乎同時發(fā)起仿荆,只有一個請求提交成功了
image
本節(jié)demo
戳這里 ==> Gitee
build項目之后,啟動本地redis,運行項目自動執(zhí)行測試方法
參考
http://www.reibang.com/p/09c6b05b670a
作者:殷天文
鏈接:http://www.reibang.com/p/09860b74658e
來源:簡書
簡書著作權歸作者所有拢操,任何形式的轉載都請聯(lián)系作者獲得授權并注明出處锦亦。