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 以及自定义注解的组合,我们实现了一个高效且灵活的流量控制方案。此方案不仅支持高并发场景,还具备高度的可配置性和可插拔性,能够应对各种业务需求。

相关推荐
Mr Aokey20 小时前
解决Redis数据丢失难题:深入理解RDB与AOF持久化机制
数据库·redis·缓存
柳贯一(逆流河版)21 小时前
Redis 分布式锁实战:解决马拉松报名并发冲突与 Lua 原子性优化
redis·分布式·lua
AAA修煤气灶刘哥1 天前
Spring AI 通关秘籍:从聊天到业务落地,Java 选手再也不用馋 Python 了!
后端·spring·openai
不良人天码星1 天前
谈谈redis的持久化
数据库·redis·缓存
zl9798991 天前
SpringBoot-配置文件yaml
java·spring boot·spring
_extraordinary_1 天前
Java Spring配置
java·开发语言·spring
工业甲酰苯胺1 天前
Java并发机制的底层实现原理:从CPU到JVM的全面解析
java·jvm·spring
重整旗鼓~1 天前
27.Redisson基本使用和可重入性
数据库·redis·缓存
SunsPlanter1 天前
苍穹外卖--04--Redis 缓存菜品信息、购物车
数据库·redis·缓存