一、目标场景
-
提现 / 下单 / 行为记录接口
-
需要:
-
防重复点击
-
不同接口规则不同
-
Redis / 内存都可能用
-
二、整体设计
@NoRepeatSubmit
↓
AOP(装饰器)
↓
RepeatSubmitStrategy(策略)
↓
Redis / 内存 / Token
每一层职责非常单一
三、第一步:注解
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmit {
/** 策略类型 */
String strategy() default "REDIS";
/** 间隔时间(毫秒) */
long interval() default 1500;
}
四、第二步:策略模式
1 策略接口
java
public interface RepeatSubmitStrategy {
boolean isRepeat(String key, long interval);
}
2 Redis 实现
java
@Component("REDIS")
public class RedisRepeatSubmitStrategy implements RepeatSubmitStrategy {
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public boolean isRepeat(String key, long interval) {
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(key, "1", interval, TimeUnit.MILLISECONDS);
return Boolean.FALSE.equals(success);
}
}
3 内存实现(兜底 / 本地)
java
@Component("LOCAL")
public class LocalRepeatSubmitStrategy implements RepeatSubmitStrategy {
private final Map<String, Long> cache = new ConcurrentHashMap<>();
@Override
public boolean isRepeat(String key, long interval) {
long now = System.currentTimeMillis();
Long last = cache.put(key, now);
return last != null && now - last < interval;
}
}
五、第三步:AOP(装饰器模式)
java
@Aspect
@Component
public class NoRepeatSubmitAspect {
@Autowired
private Map<String, RepeatSubmitStrategy> strategyMap;
@Around("@annotation(noRepeatSubmit)")
public Object around(ProceedingJoinPoint pjp,
NoRepeatSubmit noRepeatSubmit) throws Throwable {
String key = buildKey(pjp);
String strategy = noRepeatSubmit.strategy();
long interval = noRepeatSubmit.interval();
RepeatSubmitStrategy handler = strategyMap.get(strategy);
if (handler.isRepeat(key, interval)) {
return AjaxResult.error("操作太频繁,请稍后再试");
}
return pjp.proceed();
}
private String buildKey(ProceedingJoinPoint pjp) {
// userId + method + 参数摘要
return pjp.getSignature().toShortString();
}
}
六、Controller 使用
java
@PostMapping("/withdraw")
@NoRepeatSubmit(strategy = "REDIS", interval = 1500)
public AjaxResult withdraw() {
// 业务逻辑非常干净
return AjaxResult.success();
}
通过策略模式+装饰器模式的防重复提交实现:
-
Controller 0 if / 0 try-catch
-
新加规则:
-
新加一个 Strategy
-
不动旧代码
-
-
Redis / 本地 / Token:
- 随时切