防重复提交
大约 2 分钟解决方案
解决方案
- 前端JS控制点击次数,屏蔽点击按钮无法点击
- 数据库或者其他存储增加唯一索引约束
- 服务端token令牌方式。下单前先获取令牌-存储redis 下单时一并把token提交并检验和删除
实践
采用服务端token令牌方式,通过自定注解设置防重提交。
自定义注解
import java.lang.annotation.*; @Documented @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RepeatSubmit { /** * 加锁过期时间,默认是5秒 * @return */ long lockTime() default 5; /** * 默认限制类型,是方法参数 * @return */ Type limitType() default Type.PARAM; /** * 两种类型,token 或者 param */ enum Type{ PARAM , TOKEN}; }
配置redis
spring.redis.client-type=jedis spring.redis.host=120.79.150.146 spring.redis.password=xdclass.net spring.redis.port=6379 spring.redis.jedis.pool.max-active=100 spring.redis.jedis.pool.max-idle=100 spring.redis.jedis.pool.min-idle=100 spring.redis.jedis.pool.max-wait=60000
获取令牌接口
@Autowired private StringRedisTemplate redisTemplate; @GetMapping("token") public JsonData getToken(){ LoginUser loginUser = LoginInterceptor.threadLocal.get(); String token = CommonUtil.getStringNumRandom(32); //"order:submit:%s:%s" String key = String.format(RedisKey.SUBMIT_ORDER_TOKEN_KEY, loginUser.getAccountNo(),requestToken); redisTemplate.opsForValue().set(key, "1", 30, TimeUnit.MINUTES); return JsonData.buildSuccess(token); }
开发切面类
/** * 定义 @Pointcut注解表达式, * 方式一:@annotation:当执行的方法上拥有指定的注解时生效(我们采用这) * 方式二:execution:一般用于指定方法的执行 * * @param repeatSubmit */ @Pointcut("@annotation(repeatSubmit)") public void pointcutNoRepeatSubmit(RepeatSubmit repeatSubmit) { } /** * 环绕通知, 围绕着方法执行 * @Around 可以用来在调用一个具体方法前和调用后来完成一些具体的任务。 * * 方式一:单用 @Around("execution(* net.xdclass.controller.*.*(..))")可以 * 方式二:用@Pointcut和@Around联合注解也可以(我们采用这个) * * * 两种方式 * 方式一:加锁 固定时间内不能重复提交 * <p> * 方式二:先请求获取token,这边再删除token,删除成功则是第一次提交 * * @param joinPoint * @param noRepeatSubmit * @return * @throws Throwable */ @Around("pointcutNoRepeatSubmit(noRepeatSubmit)") public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit noRepeatSubmit) throws Throwable { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); boolean res; String type = noRepeatSubmit.limitType().name(); if (type.equals(RepeatSubmit.Type.PARAM.name())) { //方式一方法参数 TODO } else { //方式二,令牌形式 String requestToken = request.getHeader("request-token"); if (StringUtils.isBlank(requestToken)) { throw new BizException(BizCodeEnum.ORDER_CONFIRM_TOKEN_EQUAL_FAIL); } LoginUser loginUser = LoginInterceptor.threadLocal.get(); //"order:submit:%s:%s" String key = String.format(RedisKey.SUBMIT_ORDER_TOKEN_KEY, loginUser.getAccountNo(),requestToken); /** * 提交表单的token key * 方式一:不用lua脚本获取再判断,之前是因为 key组成是 order:submit:accountNo, value是对应的token,所以需要先获取值,再判断 * 方式二:可以直接key是 order:submit:accountNo:token,然后直接删除成功则完成 */ res = stringRedisTemplate.delete(key); } if (!res) { throw new BizException(BizCodeEnum.ORDER_CONFIRM_REPEAT); } System.out.println("目标方法执行前"); Object object = joinPoint.proceed(); System.out.println("目标方法执行后"); return object; }