Spring Boot + Redis + Lua:30分钟搞定高并发接口限流的方案

图片

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. 1. StringRedisSerializer 保证 Key/Value 都是 纯字符串,省去序列化烦恼。

  2. 2. DefaultRedisScript<Long> 把 Lua 返回值直接映射成 Long,后续比较更方便。

  3. 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. 1. @Before 在 业务方法执行前 就拦截,失败立即返回,无性能损耗

  2. 2. buildKey 支持 多维度组合,想按 IP、用户、甚至接口名随意拼装。

  3. 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 就能玩出花,接口级、用户级、甚至参数级随心配。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值