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

相关推荐
编啊编程啊程10 分钟前
【029】智能停车计费系统
java·数据库·spring boot·spring·spring cloud·kafka
java1234_小锋1 小时前
Spring事件监听的核心机制是什么?
java·spring·面试
CodeBlossom5 小时前
Spring Cache快速入门
java·数据库·spring
tuokuac5 小时前
ps -ef | grep redis
数据库·redis·缓存
⑩-5 小时前
如何保证Redis和Mysql数据缓存一致性?
java·数据库·redis·mysql·spring·缓存·java-ee
刘一说7 小时前
深入理解 Spring Boot 中的 Redis 缓存集成:从基础配置到高可用实践
spring boot·redis·缓存
不见长安在8 小时前
redis集群下如何使用lua脚本
数据库·redis·lua
努力努力再努力wz9 小时前
【Linux进阶系列】:线程(上)
java·linux·运维·服务器·数据结构·c++·redis
虎子_layor10 小时前
实现异步最常用的方式@Async,快速上手
后端·spring
不光头强10 小时前
Spring整合单元测试
spring·单元测试·log4j