Redis 限流解决方案:结合 Lua 脚本、AOP 和自定义注解的实现

目录

一、概述

为了保护系统的稳定性,避免在高并发情况下系统崩溃,我们设计并实现了一个基于 RedisLua 脚本AOP反射自定义注解的限流组件。该组件能灵活配置并支持高可用、高并发的流量控制。

二、技术栈

  • Redis:作为流量控制的基础存储,用于计数和过期控制。
  • Lua 脚本:通过 Lua 脚本来保证限流操作的原子性,避免因并发请求引起的竞争条件。
  • AOP:通过切面编程将流量控制逻辑与业务代码解耦,做到高效且优雅的控制。
  • 自定义注解:为不同的业务方法灵活添加限流控制,支持高度可配置性。

三、配置项

1、配置文件

首先是 Redis 配置文件(application.properties):

properties 复制代码
# Redis 配置
spring.data.redis.database=0
spring.data.redis.host=192.168.121.140
spring.data.redis.port=6379
spring.data.redis.password=
spring.data.redis.lettuce.pool.max-active=8
spring.data.redis.lettuce.pool.max-wait=-1ms
spring.data.redis.lettuce.pool.max-idle=8
spring.data.redis.lettuce.pool.min-idle=0

2、Redis 配置类

RedisConfig 配置类主要完成 Redis 连接池及序列化方式的设置:

java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

四、流量控制功能设计

1、需求分析

  1. 可配置:支持随时调整限流的时间和次数。
  2. 可插拔:支持按业务需求选择不同的限流策略。
  3. 可通用:不和代码业务逻辑写死,可单独配置
  4. 高可用:在高并发下仍能保证流量控制的稳定性。

2、设计思路

  1. 自定义注解 :通过 @RedisLimitAnnotation 注解来标识需要限流的方法,并配置限流的时间窗口、请求次数及提示消息。
  2. Lua 脚本:通过 Redis Lua 脚本实现原子操作,确保限流操作的可靠性。
  3. AOP 切面:利用 AOP 切面来拦截带有限流注解的方法,进行限流操作。

3、自定义注解

RedisLimitAnnotation 注解用于标识需要限流的方法,提供了可配置的属性。

java 复制代码
import java.lang.annotation.*;

/**
 * 限流注解,用于标识需要限流的方法
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface RedisLimitAnnotation {

    /**
     * 资源的唯一 key
     */
    String key() default "";

    /**
     * 最大访问次数
     */
    long permitsPerSecond() default 2;

    /**
     * 过期时间,单位秒,默认60
     */
    long expire() default 60;

    /**
     * 限流时的提示消息
     */
    String msg() default "系统繁忙,请稍后再试!";
}

4、AOP 切面实现

RedisLimitAop 类实现了基于 AOP 的限流逻辑,拦截带有 @RedisLimitAnnotation 注解的方法,执行 Redis 限流操作。

java 复制代码
@Slf4j
@Aspect
@Component
public class RedisLimitAop {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private DefaultRedisScript<Long> redisLuaScript;

    @PostConstruct
    public void init() {
        redisLuaScript = new DefaultRedisScript<>();
        redisLuaScript.setResultType(Long.class);
        redisLuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimiter.lua")));
    }

    @Around("@annotation(com.donglin.interview2.annotations.RedisLimitAnnotation)")
    public Object around(ProceedingJoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        RedisLimitAnnotation redisLimitAnnotation = method.getAnnotation(RedisLimitAnnotation.class);

        if (redisLimitAnnotation != null) {
            String key = redisLimitAnnotation.key();
            long limit = redisLimitAnnotation.permitsPerSecond();
            long expire = redisLimitAnnotation.expire();

            List<String> keys = new ArrayList<>();
            keys.add(key);

            Long count = stringRedisTemplate.execute(redisLuaScript, keys, String.valueOf(limit), String.valueOf(expire));

            if (count != null && count == 0) {
                return redisLimitAnnotation.msg();
            }
        }

        try {
            return joinPoint.proceed();
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
}

5、Lua 脚本

rateLimiter.lua 脚本用于实现 Redis 限流逻辑。它会检查当前访问次数,如果超过限流次数,则返回 0,否则自增访问计数并设置过期时间。

lua 复制代码
-- 获取当前 key
local key = KEYS[1]
-- 获取最大访问次数
local limit = tonumber(ARGV[1])
-- 获取当前访问次数
local currentLimit = tonumber(redis.call('get', key) or "0")

-- 超过访问次数限制,返回 0
if currentLimit + 1 > limit then
    return 0
else
    -- 自增访问次数
    redis.call('INCRBY', key, 1)
    -- 设置过期时间
    redis.call('EXPIRE', key, ARGV[2])
    return currentLimit + 1
end

6、示例 Controller

通过在 Controller 方法上添加 @RedisLimitAnnotation 注解,控制请求的频率。

java 复制代码
@Slf4j
@RestController
public class RedisLimitController {

    @GetMapping("/redis/limit/test")
    @RedisLimitAnnotation(key = "redisLimit", permitsPerSecond = 3, expire = 10, msg = "当前排队人数较多,请稍后再试!")
    public String redisLimit() {
        return "正常业务返回,订单流水:" + IdUtil.fastUUID();
    }
}

五、总结

通过 Redis、Lua 脚本、AOP 以及自定义注解的组合,我们实现了一个高效且灵活的流量控制方案。此方案不仅支持高并发场景,还具备高度的可配置性和可插拔性,能够应对各种业务需求。

相关推荐
heartbeat..4 小时前
Spring AOP 全面详解(通俗易懂 + 核心知识点 + 完整案例)
java·数据库·spring·aop
Jing_jing_X5 小时前
AI分析不同阶层思维 二:Spring 的事务在什么情况下会失效?
java·spring·架构·提升·薪资
程序猿零零漆9 小时前
Spring之旅 - 记录学习 Spring 框架的过程和经验(十一)基于XML方式、注解的声明式事务控制、Spring整合Web环境
xml·学习·spring
短剑重铸之日11 小时前
《SpringBoot4.0初识》第五篇:实战代码
java·后端·spring·springboot4.0
heartbeat..11 小时前
Spring MVC 全面详解(Java 主流 Web 开发框架)
java·网络·spring·mvc·web
CUIYD_198911 小时前
Freemarker 无法转译 & 字符
java·开发语言·spring
天意pt12 小时前
Blog-SSR 系统操作手册(v1.0.0)
前端·vue.js·redis·mysql·docker·node.js·express
柒.梧.13 小时前
SSM常见核心面试问题深度解析
java·spring·面试·职场和发展·mybatis
DemonAvenger16 小时前
Redis慢查询分析与优化:性能瓶颈排查实战指南
数据库·redis·性能优化
Li_yizYa16 小时前
Redis-常见数据类型及应用场景
java·数据库·redis