
在分布式系统中,接口的幂等性保障和访问频率限制是两个常见且重要的需求。本文介绍一套轻量级、基于 Redis 计数器的实现方案,通过自定义注解、拦截器和缓存模板,为 Spring MVC 应用提供声明式的幂等控制与限流能力。
本方案提供两个核心能力:
两者均基于“计数”逻辑:幂等要求最大计数为 1(首次成功,后续拒绝),限流则允许配置更大的上限(例如 10 次/分钟)。计数器的存储依托 Redis,实现高性能与分布式一致性。
三个核心类构成计数器体系:
AbstractCounterCacheTemplate:抽象模板,封装 Redis 计数器的通用逻辑(自增、上限校验、键值处理等)。IdempotentCacheTemplate:专用于幂等性,构造时 maxTimes = 1。AccessLimitedCacheTemplate:专用于访问限制,maxTimes 由注解动态指定。这样的继承层次使得通用逻辑只需实现一次,不同业务仅需调整上限参数。
@Idempotent:标记需要幂等校验的方法。@AccessLimited:标记需要限流的方法,可通过 maxTimes() 指定最大访问次数。两个拦截器分别处理上述注解,在 preHandle 阶段生成请求的唯一标识,并调用对应的计数器模板进行计数。若计数失败(超过上限),则抛出特定业务异常,由全局异常处理器返回适当响应。
SessionUtils.generateRequestKey(request) 负责生成标识一个请求的字符串。典型的组成方式包括:
该键保证了不同用户、不同接口、不同参数的请求能够被正确区分。
AbstractCounterCacheTemplate.counting 提供了一个 useMd5 参数。当原始键可能过长或含有特殊字符时,可使用 MD5 缩短并规范化键名,节省 Redis 内存。
public abstract class AbstractCounterCacheTemplate extends AbstractRedisCacheTemplate<String, Integer> {
private int maxTimes = 1;
public int counting(String key, int maxTimes, boolean useMd5) {
// 1. 校验与键值转换
String newKey = useMd5 ? SecureUtil.md5(key) : key;
Integer current = get(newKey);
if (current == null) current = 0;
// 2. 首次访问则创建缓存值为1
if (current == 0) {
create(newKey);
} else {
// 3. 已达上限则抛异常
if (current >= maxTimes) {
throw new CounterOverflowException(maxTimes);
}
put(newKey, current + 1);
}
return current + 1;
}
}注意:create(newKey) 会调用 valueGenerator(key) 返回初始值 1,因此首次计数后缓存值为 1。后续每次调用加 1,直到达到 maxTimes。
public class IdempotentHandlerInterceptor implements HandlerInterceptor {
private final IdempotentCacheTemplate idempotentCacheTemplate;
@Override
public boolean preHandle(HttpServletRequest request, ...) {
// 获取方法上的 @Idempotent 注解
Idempotent idempotent = method.getAnnotation(Idempotent.class);
if (idempotent != null) {
String key = SessionUtils.generateRequestKey(request);
idempotentCacheTemplate.counting(key); // maxTimes 固定为1
}
return true;
}
}与之类似,但传入注解中配置的 maxTimes:
AccessLimited limited = method.getAnnotation(AccessLimited.class);
if (limited != null) {
accessLimitedCacheTemplate.counting(key, limited.maxTimes());
}SecurityConfiguration 实现 WebMvcConfigurer,将两个拦截器注册到 Spring MVC 的拦截器链中。由于使用了 @Configuration(proxyBeanMethods = false),该配置会被 Spring Boot 自动扫描加载(通常通过 META-INF/spring.factories 或 @Import 控制)。
确保项目中已引入 Redis 相关依赖以及本框架。在 application.yml 中配置 Redis 连接信息(继承自 AbstractRedisCacheTemplate 的底层配置即可)。
@RestController
public class OrderController {
@PostMapping("/order")
@Idempotent // 防止重复下单
public Result createOrder(@RequestBody OrderReq req) {
// 业务逻辑...
}
@GetMapping("/search")
@AccessLimited(maxTimes = 5) // 每分钟最多5次
public Result search(String keyword) {
// 搜索逻辑...
}
}当重复请求或超出限流阈值时,拦截器会分别抛出 IdempotentException 和 AccessLimitedException。可定义 @RestControllerAdvice 统一返回错误码与提示信息,例如:
@ExceptionHandler(IdempotentException.class)
public ResponseEntity<String> handleIdempotent() {
return ResponseEntity.status(409).body("重复请求,请勿重试");
}当前的计数缓存没有设置过期时间。这意味着一旦计数达到上限,将永久拒绝后续请求。实际生产环境中,通常需要配合过期策略:
AbstractRedisCacheTemplate 的 expire 方法支持。可以在创建缓存时调用 redisTemplate.expire(key, timeout, unit) 来添加 TTL。
AbstractCounterCacheTemplate 中的 get、create、put 操作并非严格原子。极端并发下可能造成计数不准确。改进方案是使用 Redis 的 INCR 命令,该命令原生支持原子递增,并且可以检查返回值是否超过上限。当前实现适合并发不高的场景,若需高并发精确限流,建议改用 RedisAtomicLong 或 Lua 脚本。
SessionUtils.generateRequestKey 的实现决定了安全性与覆盖面。例如:
开发者应根据业务需要重写该方法,或通过配置声明键的组成规则。
本方案拦截器默认对所有请求路径生效。可以通过配置 addInterceptor 时指定 addPathPatterns 和 excludePathPatterns 来缩小范围,避免不必要的性能损耗。
本文介绍的基于计数器的幂等与限流方案,具有以下优点:
同时也存在改进空间:原子计数、过期策略的完善以及更灵活的限流算法(令牌桶、漏桶)。开发者可根据实际场景在此基础上进行二次开发。
该方案已经应用于多个内部项目,稳定地保障了接口的正确性与系统的可用性。希望本文能为您的分布式接口治理提供参考。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。