##### 該切麵功能適用場景 - 下單請求多次提交,導致生成多個相同的訂單 ##### 解決方案 - 前端解決:限制點擊下單按鈕為1次後失效。不足:用戶體驗下降,能繞過前端 - 後端解決:防重提交切麵解決,自定義註釋實現該功能(如下) - 步驟: - 自定義註釋類RepeatSubmit - 創建 ...
該切麵功能適用場景
- 下單請求多次提交,導致生成多個相同的訂單
解決方案
-
前端解決:限制點擊下單按鈕為1次後失效。不足:用戶體驗下降,能繞過前端
-
後端解決:防重提交切麵解決,自定義註釋實現該功能(如下)
- 步驟:
- 自定義註釋類RepeatSubmit
- 創建切麵並有該註釋綁定,在切麵類實現防重提交功能:
- 方式一:引入redission進行加鎖5秒,原理redis的setAbsent
- 方式二:將token存入redis中,下單成功刪除token,下單前需要調用獲取token介面才能成功下單(類似於加鎖,和方式一原理相同)
- 步驟:
-
RepeatSubmit
/**
* 自定義防重提交
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {
/**
* 防重提交類型。 方法、令牌
*/
enum Type {PARAM, TOKEN}
/**
* 預設防重提交,是方法參數
* @return
*/
Type limitType() default Type.PARAM;
/**
* 加鎖過期時間,預設5秒
* @return
*/
long lockTime() default 5;
}
- 自定義切麵類
/**
* 定義一個切麵類
*/
@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private RedissonClient redissonClient;
/**
* 定義 @Pointcut註解表達式,
* 方式一:@annotation:當執行的方法上擁有指定的註解時生效(我們採用這)
*/
@Pointcut("@annotation(repeatSubmit)")
public void pointCutNoRepeatSubmit(RepeatSubmit repeatSubmit) {
}
/**
* 環繞通知, 圍繞著方法執行
*
* @param joinPoint
* @param repeatSubmit
* @return
* @throws Throwable
* @Around 可以用來在調用一個具體方法前和調用後來完成一些具體的任務。
*/
@Around("pointCutNoRepeatSubmit(repeatSubmit)")
public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
// 記錄成功或者失敗
Boolean res = false;
// 防重提交類型
String type = repeatSubmit.limitType().name();
if (type.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())) {
//方式一,參數形式防重提交
long lockTime = repeatSubmit.lockTime();
String ipAddr = CommonUtil.getIpAddr(request);
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
String className = method.getDeclaringClass().getName();
String key = "order-server:repeat_submit"+CommonUtil.MD5(String.format("%s-%s-%s-%s", ipAddr, className, method, accountNo));
// 加鎖
//res = redisTemplate.opsForValue().setIfAbsent(key,"1",lockTime, TimeUnit.SECONDS);
RLock lock = redissonClient.getLock(key);
// 嘗試加鎖,最多等待2秒,上鎖以後5秒自動解鎖 [lockTime預設為5s, 可以自定義]
res = lock.tryLock(2, lockTime, TimeUnit.SECONDS);
} else if (type.equalsIgnoreCase(RepeatSubmit.Type.TOKEN.name())) {
//方式二,令牌形式防重提交
String requestToken = request.getHeader("request-token");
if (StringUtils.isBlank(requestToken)) {
throw new BizException(BizCodeEnum.ORDER_CONFIRM_TOKEN_EQUAL_FAIL);
}
String key = String.format(RedisKey.SUBMIT_ORDER_TOKEN_KEY, accountNo, requestToken);
/**
* 提交表單的token key
* key是 order:submit:accountNo:token,然後直接刪除成功則完成
*/
res = redisTemplate.delete(key);
}
if (!res) {
log.error("訂單請求重覆提交");
return null;
}
log.info("環繞通知執行前");
Object obj = joinPoint.proceed();
log.info("環繞通知執行後");
return obj;
}
}
- RedissionConfiguration配置類(用於加鎖)
@Configuration
public class RedissionConfiguration {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private String redisPort;
@Value("${spring.redis.password}")
private String redisPwd;
/**
* 配置分散式鎖的redisson
* @return
*/
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
//單機方式
config.useSingleServer().setPassword(redisPwd).setAddress("redis://"+redisHost+":"+redisPort);
//集群
//config.useClusterServers().addNodeAddress("redis://192.31.21.1:6379","redis://192.31.21.2:6379")
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
/**
* 集群模式
* 備註:可以用"rediss://"來啟用SSL連接
*/
/*@Bean
public RedissonClient redissonClusterClient() {
Config config = new Config();
config.useClusterServers().setScanInterval(2000) // 集群狀態掃描間隔時間,單位是毫秒
.addNodeAddress("redis://127.0.0.1:7000")
.addNodeAddress("redis://127.0.0.1:7002");
RedissonClient redisson = Redisson.create(config);
return redisson;
}*/
}
- 使用說明:在下單介面標註@RepeatSubmit(limitType = RepeatSubmit.Type.TOKEN)
- 或者@RepeatSubmit(limitType = RepeatSubmit.Type.PARAM)
/**
* 下單前獲取令牌,用於防重提交
* @return
*/
@GetMapping("token")
public JsonData getOrderToken() {
Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
String token = CommonUtil.getStringNumRandom(32);
String key = String.format(RedisKey.SUBMIT_ORDER_TOKEN_KEY, accountNo, token);
// token 過期時間30分鐘
redisTemplate.opsForValue().set(key, String.valueOf(Thread.currentThread().getId()), 30, TimeUnit.MINUTES);
return JsonData.buildSuccess(token);
}
@PostMapping("confirm")
@RepeatSubmit(limitType = RepeatSubmit.Type.TOKEN)
public void confirmOrder(@RequestBody ConfirmOrderRequest orderRequest, HttpServletResponse response) {
// TODO 下單業務
}