切面实现下单请求防重提交功能(自定义注释@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 下单业务
}
热门相关:流鱼无恙 拒嫁豪门,前妻太抢手 买妻种田:山野夫君,强势宠! 豪门情变,渣总裁滚远点! 性爱寄宿家庭:轮流性爱