01
前言
限流 是高并发系统的保命符,Redis + Lua 则是业界公认的最轻量、最稳的组合。
今天手把手带你撸一个 可插拔、零侵入、秒级生效 的限流组件,10 分钟 就能上线!。
02
环境准备
2.1 依赖一把梭
<!-- web 容器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- AOP,用来切注解 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Redis 官方 starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.2 配置 Redis
spring:
redis:
host:127.0.0.1
port:6379
database:0
lettuce:
pool:
max-active:8
max-idle:8
min-idle: 0
03
定义限流模型
3.1 限流维度枚举
public enum LimitType {
IP("ip"), // 按 IP
USER("user"), // 按用户ID
CUSTOM("custom"); // 自定义
LimitType(String type) { this.type = type; }
public final String type;
}
3.2 注解,一行声明就能限流
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimiter {
LimitType limitType() default LimitType.IP; // 限流维度
String key() default ""; // 业务前缀
int time() default 60; // 周期,单位秒
int count() default 100; // 周期内最大次数
}
04
Lua 脚本 —— 原子操作就靠它
src/main/resources/lua/limit.lua
-- KEYS[1]:拼接好的 key
-- ARGV[1]:过期时间
-- ARGV[2]:最大次数
local key = KEYS[1]
localtime = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local current = redis.call('GET', key) or0 -- 已访问次数
current = tonumber(current)
if current >= limit then
return current -- 已超限,直接返回当前值
end
current = redis.call('INCR', key) -- 自增
if current == 1then
redis.call('EXPIRE', key, time) -- 首次访问设过期
end
return current
05
Redis 配置 —— 让模板认识 Lua
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
StringRedisSerializer serializer = new StringRedisSerializer(StandardCharsets.UTF_8);
template.setKeySerializer(serializer);
template.setValueSerializer(serializer);
template.setHashKeySerializer(serializer);
template.setHashValueSerializer(serializer);
return template;
}
@Bean
public DefaultRedisScript<Long> limitScript() {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setResultType(Long.class);
script.setLocation(new ClassPathResource("lua/limit.lua"));
return script;
}
}
注解:
-
1.
StringRedisSerializer
保证 Key/Value 都是 纯字符串,省去序列化烦恼。 -
2.
DefaultRedisScript<Long>
把 Lua 返回值直接映射成 Long,后续比较更方便。 -
3. 通过
@Bean
统一注入,切面里零硬编码。
06
切面 —— 真正的限流守门员
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class RateLimitAspect {
private final RedisTemplate<String, Object> redisTemplate;
private final DefaultRedisScript<Long> limitScript;
@Before("@annotation(rateLimiter)")
public void before(JoinPoint jp, RateLimiter rateLimiter) {
HttpServletRequest request = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getRequest();
String key = buildKey(rateLimiter, request);
Long current = redisTemplate.execute(
limitScript,
Collections.singletonList(key),
String.valueOf(rateLimiter.time()),
String.valueOf(rateLimiter.count())
);
if (current != null && current > rateLimiter.count()) {
log.warn("[限流] key={} 触发限流", key);
throw new BizException("请求太频繁,稍后再试");
}
}
private String buildKey(RateLimiter limit, HttpServletRequest request) {
StringBuilder sb = new StringBuilder("rate:");
if (!limit.key().isEmpty()) sb.append(limit.key()).append(":");
switch (limit.limitType()) {
case IP:
sb.append(IpUtil.getIpAddr(request));
break;
case USER:
sb.append(SecurityUtil.getUserId());
break;
default:
sb.append("custom");
}
return sb.toString();
}
}
注解:
-
1.
@Before
在 业务方法执行前 就拦截,失败立即返回,无性能损耗。 -
2.
buildKey
支持 多维度组合,想按 IP、用户、甚至接口名随意拼装。 -
3. 异常统一抛,由全局异常处理器转成 友好 JSON,前端无感知。
07
使用姿势 —— 一个注解完事
@RestController
@RequestMapping("/api/order")
public class OrderController {
@GetMapping("/list")
@RateLimiter(key = "orderList", time = 60, count = 10) // 每个 IP 60 秒 10 次
public List<Order> list() {
return orderService.list();
}
}
08
小结
-
• 零业务侵入:注解 + AOP,老代码无痛接入。
-
• 高性能:Lua + Redis 单线程原子操作,QPS 轻松破万。
-
• 可扩展:换一个
LimitType
就能玩出花,接口级、用户级、甚至参数级随心配。