目录

引言:

为什么需要限流?

一、技术选型与核心思路

二、基础实现:Redis + AOP + SpEL

1. 自定义限流注解

2. AOP切面拦截处理

3. SpEL动态Key示例

三、性能优化:Lua脚本实现原子操作

问题分析:

Lua脚本解决方案

Java调用Lua脚本

四、方案对比与性能提升

五、扩展与注意事项

六、总结


引言:
为什么需要限流?

        在高并发场景下,系统可能因突发流量崩溃。限流是保护系统的关键手段,通过限制接口请求频率,防止资源耗尽。本文结合 Redis(高性能缓存)、AOP(切面编程)和 SpEL(动态表达式),实现灵活、高可用的分布式限流方案,并通过 Lua 脚本优化性能。


一、技术选型与核心思路
  1. Redis

    • 单线程原子性操作,适合计数器场景。

    • 过期时间特性可自然实现时间窗口限流。

  2. AOP(面向切面编程)

    • 无侵入式拦截方法,统一处理限流逻辑。

    • 结合自定义注解,灵活标记需要限流的接口。

  3. SpEL(Spring Expression Language)

    • 动态解析注解参数,支持从请求参数、Header等动态生成限流Key。

  4. Lua脚本优化

    • 减少网络开销:如果直接通过客户端与Redis进行多次交互来完成一个复杂的逻辑(如限流),那么每次交互都会产生网络延迟。而使用Lua脚本,我们可以将整个逻辑封装在一个脚本里,在一次请求中完成所有操作,大大减少了网络开销。

    • 原子性操作:Redis会将整个Lua脚本作为一个整体命令执行,这意味着在整个脚本执行期间,不会有其他命令插入进来打断这个过程,确保了操作的原子性。


二、基础实现:Redis + AOP + SpEL
1. 自定义限流注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimiter {
    String key();          // 限流唯一标识(支持SpEL)
    int time() default 60; // 时间窗口(秒)
    int count() default 10;// 允许请求次数
}
2. AOP切面拦截处理
@Aspect
@Component
public class RateLimiterAspect {
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private ExpressionParser parser;

    @Around("@annotation(rateLimiter)")
    public Object around(ProceedingJoinPoint joinPoint, RateLimiter rateLimiter) throws Throwable {
        // 解析SpEL动态生成Key(例如:user_123)
        String key = parseSpEL(joinPoint, rateLimiter.key());
        int time = rateLimiter.time();
        int count = rateLimiter.count();

        // Redis计数器自增
        Long current = redisTemplate.opsForValue().increment(key, 1);
        if (current == 1) {
            redisTemplate.expire(key, time, TimeUnit.SECONDS); // 首次设置过期时间
        }
        if (current > count) {
            throw new RateLimitException("请求过于频繁,请稍后再试!");
        }
        return joinPoint.proceed();
    }

    private String parseSpEL(ProceedingJoinPoint joinPoint, String spEL) {
        // 解析方法参数、注解等生成动态Key
        EvaluationContext context = new MethodBasedEvaluationContext(...);
        return parser.parseExpression(spEL).getValue(context, String.class);
    }
}
 
3. SpEL动态Key示例
@RateLimiter(key = "'rate_limit:' + #userId") // 从方法参数中提取userId
public void queryUser(String userId) { ... }
 

三、性能优化:Lua脚本实现原子操作
问题分析:
  • 网络开销:INCR + EXPIRE 需要两次Redis请求,高并发下延迟明显。

  • 原子性风险:若设置过期时间失败,可能导致Key永久存在。

Lua脚本解决方案
-- KEYS[1]: 限流Key
-- ARGV[1]: 时间窗口(秒)
-- ARGV[2]: 允许的最大请求数
local current = redis.call('GET', KEYS[1])
if current == false then
    redis.call('SET', KEYS[1], 1, 'EX', ARGV[1])
    return 1
else
    local newCount = redis.call('INCR', KEYS[1])
    if tonumber(newCount) > tonumber(ARGV[2]) then
        return -1  -- 超出限制
    else
        return newCount
    end
end
 
Java调用Lua脚本
private static final DefaultRedisScript<Long> RATE_LIMIT_SCRIPT = new DefaultRedisScript<>();
static {
    RATE_LIMIT_SCRIPT.setScriptSource(new ResourceScriptSource(new ClassPathResource("rate_limiter.lua")));
    RATE_LIMIT_SCRIPT.setResultType(Long.class);
}

public boolean tryAcquire(String key, int time, int count) {
    Long result = redisTemplate.execute(
        RATE_LIMIT_SCRIPT,
        Collections.singletonList(key),
        String.valueOf(time),
        String.valueOf(count)
    );
    return result != null && result != -1;
}
 

四、方案对比与性能提升
方案网络请求次数原子性性能
原生INCR + EXPIRE2次无保证低
Lua脚本1次强原子性高
  • 压测结果:单节点QPS从 2k 提升至 8k+,性能提升约300%。


五、扩展与注意事项
  1. 限流算法选择

    • 计数器:简单但存在临界问题。

    • 令牌桶:平滑突发流量,需更复杂实现。

    • 漏桶:恒定速率处理请求。

  2. 集群环境适配

    • 使用Redis集群时,需确保相同Key路由到同一节点。

  3. 降级策略

    • 返回缓存数据、排队等待或直接拒绝请求。

  4. 监控与动态配置

    • 集成Spring Cloud Config动态调整限流参数。


六、总结

        通过 Redis + AOP + SpEL,我们实现了灵活、无侵入的分布式限流,而Lua脚本的引入彻底解决了原子性和性能瓶颈。该方案可快速集成到Spring Boot项目中,适用于电商秒杀、API开放平台等高并发场景。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐