从零搭建高可用分布式限流组件:设计模式与Redis令牌桶实践

一、需求背景与设计目标

在分布式系统中,面对突发流量时需要一种精准可控的流量控制手段。我们的组件需要具备:

  1. 多维度限流(用户/IP/服务节点/自定义表达式)
  2. 分布式环境下精准控制
  3. 开箱即用的Spring Boot Starter集成
  4. 高扩展性的架构设计

二、架构设计全景图

text 复制代码
┌───────────────────┐
│   RateLimiter注解   │
└─────────┬─────────┘
          │
          ▼
┌───────────────────┐
│     AOP切面处理     │
└─────────┬─────────┘
          │
          ├──→ IP解析器 (ClientIpRateLimiterKeyResolver)
          ├──→ 用户解析器 (UserRateLimiterKeyResolver)
          ├──→ 服务节点解析器 (ServerNodeRateLimiterKeyResolver)
          └──→ 表达式解析器 (ExpressionRateLimiterKeyResolver)
          │
          ▼
┌───────────────────┐
│ Redis令牌桶算法实现  │
│  (RateLimiterRedisDAO) │
└───────────────────┘

设计亮点

  • 策略模式:KeyResolver 实现可插拔的限流维度
  • 代理模式:AOP 实现
  • 原子性保障:Redis Lua 脚本保证计算原子性

三、核心实现步骤

步骤1:声明式注解定义(策略入口)

java 复制代码
/**
 * 限流注解
 *
 * @author dyh
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimiter {

    /**
     * 限流的时间,默认为 1 秒
     */
    int time() default 1;
    /**
     * 时间单位,默认为 SECONDS 秒
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;

    /**
     * 限流次数
     */
    int count() default 100;

    /**
     * 提示信息,请求过快的提示
     *
     * @see GlobalErrorCodeConstants#TOO_MANY_REQUESTS
     */
    String message() default ""; // 为空时,使用 TOO_MANY_REQUESTS 错误提示

    /**
     * 使用的 Key 解析器
     *
     * @see DefaultRateLimiterKeyResolver 全局级别
     * @see UserRateLimiterKeyResolver 用户 ID 级别
     * @see ClientIpRateLimiterKeyResolver 用户 IP 级别
     * @see ServerNodeRateLimiterKeyResolver 服务器 Node 级别
     * @see ExpressionIdempotentKeyResolver 自定义表达式,通过 {@link #keyArg()} 计算
     */
    Class<? extends RateLimiterKeyResolver> keyResolver() default DefaultRateLimiterKeyResolver.class;
    /**
     * 使用的 Key 参数
     */
    String keyArg() default "";

}

步骤2:策略模式设计Key解析器接口

java 复制代码
/**
 * 限流 Key 解析器接口
 *
 * @author dyh
 */
public interface RateLimiterKeyResolver {

    /**
     * 解析一个 Key
     *
     * @param rateLimiter 限流注解
     * @param joinPoint  AOP 切面
     * @return Key
     */
    String resolver(JoinPoint joinPoint, RateLimiter rateLimiter);

}

步骤3:实现五种核心策略

  • 默认策略:MD5(方法签名+参数)
  • IP策略:MD5(方法签名+参数+IP)
  • 用户策略:MD5(方法签名+参数+用户ID)
  • 服务节点策略:MD5(方法签名+参数+服务节点地址)
  • 表达式策略:SpEL动态解析参数

3.1 默认策略

java 复制代码
/**
 * 默认(全局级别)限流 Key 解析器,使用方法名 + 方法参数,组装成一个 Key
 * <p>
 * 为了避免 Key 过长,使用 MD5 进行"压缩"
 *
 * @author dyh
 */
public class DefaultRateLimiterKeyResolver implements RateLimiterKeyResolver {
    @Override
    public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
        String methodName = joinPoint.getSignature().toString();
        String argsStr = StrUtil.join(",", joinPoint.getArgs());
        return SecureUtil.md5(methodName + argsStr);
    }
}

3.2 IP策略

java 复制代码
/**
 * IP 级别的限流 Key 解析器,使用方法名 + 方法参数 + IP,组装成一个 Key
 *
 * 为了避免 Key 过长,使用 MD5 进行"压缩"
 *
 * @author dyh
 */
public class ClientIpRateLimiterKeyResolver implements RateLimiterKeyResolver
{
    @Override
    public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
        String methodName = joinPoint.getSignature().toString();
        String argsStr = StrUtil.join(",", joinPoint.getArgs());
        String clientIp = ServletUtils.getClientIP();
        return SecureUtil.md5(methodName + argsStr + clientIp);
    }
}

3.3 用户策略

java 复制代码
/**
 * 用户级别的限流 Key 解析器,使用方法名 + 方法参数 + userId + userType,组装成一个 Key
 *
 * 为了避免 Key 过长,使用 MD5 进行"压缩"
 *
 * @author dyh
 */
public class UserRateLimiterKeyResolver implements RateLimiterKeyResolver {
    @Override
    public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
        String methodName = joinPoint.getSignature().toString();
        String argsStr = StrUtil.join(",", joinPoint.getArgs());
        Long userId = WebFrameworkUtils.getLoginUserId();
        Integer userType = WebFrameworkUtils.getLoginUserType();
        return SecureUtil.md5(methodName + argsStr + userId + userType);
    }
}

3.4 服务节点策略

java 复制代码
/**
 * Server 节点级别的限流 Key 解析器,使用方法名 + 方法参数 + IP,组装成一个 Key
 * <p>
 * 为了避免 Key 过长,使用 MD5 进行"压缩"
 *
 * @author dyh
 */
public class ServerNodeRateLimiterKeyResolver implements RateLimiterKeyResolver {
    @Override
    public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
        String methodName = joinPoint.getSignature().toString();
        String argsStr = StrUtil.join(",", joinPoint.getArgs());
        String serverNode = String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID());
        return SecureUtil.md5(methodName + argsStr + serverNode);
    }
}

3.5 表达式策略

java 复制代码
/**
 * 基于 Spring EL 表达式的 {@link RateLimiterKeyResolver} 实现类
 *
 * @author dyh
 */
public class ExpressionRateLimiterKeyResolver implements RateLimiterKeyResolver {

    private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

    private final ExpressionParser expressionParser = new SpelExpressionParser();

    @Override
    public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
        // 获得被拦截方法参数名列表
        Method method = getMethod(joinPoint);
        Object[] args = joinPoint.getArgs();
        String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(method);
        // 准备 Spring EL 表达式解析的上下文
        StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
        if (ArrayUtil.isNotEmpty(parameterNames)) {
            for (int i = 0; i < parameterNames.length; i++) {
                evaluationContext.setVariable(parameterNames[i], args[i]);
            }
        }

        // 解析参数
        Expression expression = expressionParser.parseExpression(rateLimiter.keyArg());
        return expression.getValue(evaluationContext, String.class);
    }

    private static Method getMethod(JoinPoint point) {
        // 处理,声明在类上的情况
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        if (!method.getDeclaringClass().isInterface()) {
            return method;
        }

        // 处理,声明在接口上的情况
        try {
            return point.getTarget().getClass().getDeclaredMethod(
                    point.getSignature().getName(), method.getParameterTypes());
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }

}

步骤4:AOP切面编排(流程控制)

java 复制代码
import lombok.extern.slf4j.Slf4j; // Lombok日志注解
import cn.hutool.core.util.StrUtil; // 字符串工具类
import org.aspectj.lang.JoinPoint; // AOP连接点
import org.aspectj.lang.annotation.Aspect; // AOP切面注解
import org.aspectj.lang.annotation.Before; // 前置通知注解
import org.springframework.util.Assert; // Spring断言工具
import java.util.List;
import java.util.Map;
/**
 * 基于AOP实现的限流切面
 * 拦截所有使用@RateLimiter注解的方法,实现分布式限流功能
 * 核心原理:通过Redis实现令牌桶算法控制请求速率
 *
 * @author dyh
 */
@Aspect // 声明当前类是一个切面
@Slf4j  // 自动生成日志对象
public class RateLimiterAspect {

    /**
     * 限流KEY解析器集合(线程安全)
     * Key: 解析器类类型(Class对象)
     * Value: 对应的解析器实例
     */
    private final Map<Class<? extends RateLimiterKeyResolver>, RateLimiterKeyResolver> keyResolvers;

    /**
     * Redis限流操作组件
     * 封装了与Redis交互的限流操作方法
     */
    private final RateLimiterRedisDAO rateLimiterRedisDAO;

    /**
     * 构造函数(Spring会自动注入依赖)
     * @param keyResolvers 所有实现RateLimiterKeyResolver接口的Bean列表
     * @param rateLimiterRedisDAO Redis限流操作组件
     */
    public RateLimiterAspect(List<RateLimiterKeyResolver> keyResolvers, RateLimiterRedisDAO rateLimiterRedisDAO) {
        // 将List转换为Map,方便根据类类型快速查找解析器
        this.keyResolvers = CollectionUtils.convertMap(keyResolvers, RateLimiterKeyResolver::getClass);
        this.rateLimiterRedisDAO = rateLimiterRedisDAO;
    }

    /**
     * 前置通知:在标注@RateLimiter的方法执行前进行限流控制
     * @param joinPoint 连接点信息(包含方法签名、参数等)
     * @param rateLimiter 方法上的限流注解实例
     * @throws ServiceException 当触发限流时抛出业务异常
     */
    @Before("@annotation(rateLimiter)") // 拦截所有标注@RateLimiter的方法
    public void beforePointCut(JoinPoint joinPoint, RateLimiter rateLimiter) {
        // 根据注解配置的解析器类型获取对应的解析器实例
        RateLimiterKeyResolver keyResolver = keyResolvers.get(rateLimiter.keyResolver());
        // 确保解析器存在(避免空指针异常)
        Assert.notNull(keyResolver, "找不到对应的 RateLimiterKeyResolver");

        // 生成当前请求的限流唯一标识(不同解析器实现不同策略,如:基于方法、IP、用户等)
        String key = keyResolver.resolver(joinPoint, rateLimiter);

        // 尝试获取令牌(true表示获取成功,false表示触发限流)
        boolean success = rateLimiterRedisDAO.tryAcquire(
                key,                     // 限流KEY
                rateLimiter.count(),     // 令牌桶容量
                rateLimiter.time(),      // 时间间隔
                rateLimiter.timeUnit()); // 时间单位

        if (!success) { // 触发限流
            // 记录限流日志(INFO级别便于监控)
            log.info("[beforePointCut][方法({}) 参数({}) 请求过于频繁]",
                    joinPoint.getSignature().toString(), joinPoint.getArgs());

            // 优先使用注解中定义的消息,若未配置则使用默认消息
            String message = StrUtil.blankToDefault(rateLimiter.message(),
                    GlobalErrorCodeConstants.TOO_MANY_REQUESTS.getMsg());

            // 抛出限流异常(错误码默认429 Too Many Requests)
            throw new ServiceException(
                    GlobalErrorCodeConstants.TOO_MANY_REQUESTS.getCode(),
                    message);
        }
    }
}

步骤5:Redis令牌桶算法实现

java 复制代码
// 导入依赖的类库
import lombok.AllArgsConstructor; // Lombok全参构造函数注解
import org.redisson.api.*; // Redisson客户端相关接口
import java.util.Objects; // 对象工具类
import java.util.concurrent.TimeUnit; // 时间单位枚举

/**
 * 基于Redis的分布式限流数据访问对象
 * 使用Redisson的RRateLimiter实现令牌桶限流算法
 * 支持动态配置限流规则,自动处理限流器配置变更
 *
 * @author 芋道源码
 */
@AllArgsConstructor // 自动生成全参构造函数
public class RateLimiterRedisDAO {

    /**
     * Redis键格式模板
     * 完整键格式示例:rate_limiter:user_123 (当传入key为user_123时)
     * 作用:统一管理限流器在Redis中的存储结构
     */
    private static final String RATE_LIMITER_KEY_TEMPLATE = "rate_limiter:%s";

    /**
     * Redisson客户端实例(通过构造函数注入)
     * 用于操作Redis分布式限流器
     */
    private final RedissonClient redissonClient;

    /**
     * 尝试获取限流许可(核心方法)
     * @param key 限流唯一标识(业务维度)
     * @param count 时间窗口内允许的请求数(令牌桶容量)
     * @param time 时间窗口数值
     * @param timeUnit 时间窗口单位
     * @return true-获取成功(允许请求),false-获取失败(触发限流)
     */
    public Boolean tryAcquire(String key, int count, int time, TimeUnit timeUnit) {
        // 1. 获取或创建限流器,并配置限流规则
        RRateLimiter rateLimiter = getRRateLimiter(key, count, time, timeUnit);
        // 2. 尝试获取1个令牌(立即返回结果)
        return rateLimiter.tryAcquire();
    }

    /**
     * 格式化Redis键
     * @param key 原始业务键
     * @return 格式化后的完整Redis键
     */
    private static String formatKey(String key) {
        return String.format(RATE_LIMITER_KEY_TEMPLATE, key);
    }

    /**
     * 获取或创建限流器(带智能配置管理)
     * 处理三种情况:
     * 1. 限流器不存在:创建并配置
     * 2. 限流器存在且配置相同:直接使用
     * 3. 限流器存在但配置不同:更新配置
     *
     * @param key 业务维度唯一标识
     * @param count 每秒允许的请求数
     * @param time 时间窗口数值
     * @param timeUnit 时间窗口单位
     * @return 配置好的限流器实例
     */
    private RRateLimiter getRRateLimiter(String key, long count, int time, TimeUnit timeUnit) {
        // 生成完整Redis键
        String redisKey = formatKey(key);
        // 获取限流器实例(可能未初始化)
        RRateLimiter rateLimiter = redissonClient.getRateLimiter(redisKey);

        // 将时间单位统一转换为秒(Redisson配置需要)
        long rateInterval = timeUnit.toSeconds(time);

        // 获取当前限流器配置
        RateLimiterConfig config = rateLimiter.getConfig();

        // 情况1:限流器未初始化
        if (config == null) {
            // 设置分布式限流规则(整体限流模式)
            rateLimiter.trySetRate(
                    RateType.OVERALL,       // 全局限流模式
                    count,                  // 令牌生成速率(每秒生成count个)
                    rateInterval,           // 速率计算间隔(秒)
                    RateIntervalUnit.SECONDS);
            // 设置键过期时间(防止内存泄漏),参考知识库说明
            rateLimiter.expire(rateInterval, TimeUnit.SECONDS);
            return rateLimiter;
        }

        // 情况2:配置完全匹配现有配置
        if (config.getRateType() == RateType.OVERALL
                && Objects.equals(config.getRate(), count)
                && Objects.equals(config.getRateInterval(), TimeUnit.SECONDS.toMillis(rateInterval))) {
            return rateLimiter;
        }

        // 情况3:配置变更需要更新
        rateLimiter.setRate(
                RateType.OVERALL,
                count,
                rateInterval,
                RateIntervalUnit.SECONDS);
        // 更新过期时间(保持键的有效期)
        rateLimiter.expire(rateInterval, TimeUnit.SECONDS);
        return rateLimiter;
    }
}

步骤6:自动装配

java 复制代码
/**
 * @author dyh
 * @date 2025/4/24 15:06
 */
@AutoConfiguration(before = DyhRedisAutoConfiguration.class)
public class DyhRateLimiterConfiguration {
    @Bean
    public RateLimiterAspect rateLimiterAspect(List<RateLimiterKeyResolver> keyResolvers, RateLimiterRedisDAO rateLimiterRedisDAO) {
        return new RateLimiterAspect(keyResolvers, rateLimiterRedisDAO);
    }

    @Bean
    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    public RateLimiterRedisDAO rateLimiterRedisDAO(RedissonClient redissonClient) {
        return new RateLimiterRedisDAO(redissonClient);
    }

    // ========== 各种 RateLimiterRedisDAO Bean ==========

    @Bean
    public DefaultRateLimiterKeyResolver defaultRateLimiterKeyResolver() {
        return new DefaultRateLimiterKeyResolver();
    }

    @Bean
    public UserRateLimiterKeyResolver userRateLimiterKeyResolver() {
        return new UserRateLimiterKeyResolver();
    }

    @Bean
    public ClientIpRateLimiterKeyResolver clientIpRateLimiterKeyResolver() {
        return new ClientIpRateLimiterKeyResolver();
    }

    @Bean
    public ServerNodeRateLimiterKeyResolver serverNodeRateLimiterKeyResolver() {
        return new ServerNodeRateLimiterKeyResolver();
    }

    @Bean
    public ExpressionRateLimiterKeyResolver expressionRateLimiterKeyResolver() {
        return new ExpressionRateLimiterKeyResolver();
    }

}

四、核心设计模式解析

1. 策略模式(核心设计)

模式结构

text 复制代码
           ┌───────────────────┐
           │ 策略接口            │
           │ RateLimiterKeyResolver │
           └─────────┬─────────┘
                     △
       ┌─────────────┴─────────────┐
       │                           │
┌──────┴──────┐           ┌───────┴───────┐
│ 具体策略实现   │           │ 具体策略实现    │
│ (IP解析器)    │           │ (用户解析器)    │
└─────────────┘           └──────────────┘

代码体现

java 复制代码
public interface RateLimiterKeyResolver {
    String resolver(JoinPoint joinPoint, RateLimiter rateLimiter);
}

// 具体策略实现(以IP解析器为例) 
public class ClientIpRateLimiterKeyResolver implements RateLimiterKeyResolver { 
    @Override 
    public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) { 
        String clientIp = ServletUtils.getClientIP(); // 获取策略参数 
        return SecureUtil.md5(...); // 策略算法 
    } 
}

设计优势

  • 开闭原则 :新增限流维度只需添加新策略类(如OrderRateLimiterKeyResolver
  • 业务解耦:限流策略与核心算法分离,修改策略不影响其他组件
  • 动态切换 :通过注解参数即时切换策略(@RateLimiter(keyResolver=...)

2. 代理模式(AOP实现)

模式结构

text 复制代码
           [业务方法]                 [代理对象]
               △                       △
               │                       │
┌──────────────┴──────────────┐ ┌──────┴───────┐
│       原始业务逻辑             │ │  AOP 增强逻辑  │
│ (如:businessMethod 实现)     │ │ (限流校验逻辑)  │
└─────────────────────────────┘ └──────────────┘

代码体现

java 复制代码
@Aspect
public class RateLimiterAspect { 
    @Before("@annotation(rateLimiter)") 
    public void beforePointCut(JoinPoint joinPoint, RateLimiter rateLimiter) {
        // 代理增强逻辑 
        if (!rateLimiterRedisDAO.tryAcquire(...)) { 
            throw new ServiceException("触发限流"); 
        } 
    } 
}

设计优势

  • 无侵入性:业务代码无需感知限流逻辑的存在
  • 集中管理:所有限流规则在切面中统一处理
  • 动态增强:运行时动态创建代理对象,无需修改源码

五、使用示例与场景

1. API接口全局限流

java 复制代码
@RateLimiter(count = 100) // 默认全局维度 
@GetMapping("/api/list") 
public ApiResult<List> queryList() { 
	// 每秒钟最多100次请求 
}

2. 用户维度精细控制

java 复制代码
@RateLimiter( count = 10, keyResolver = UserRateLimiterKeyResolver.class ) 
@PostMapping("/user/update") 
public ApiResult updateUserInfo() { 
	// 每个用户每秒最多操作10次 
}

3. 防爬虫IP限制

java 复制代码
@RateLimiter(count = 5, keyResolver = ClientIpRateLimiterKeyResolver.class)
@GetMapping("/product/detail")
public ProductDetailVO getDetail() { 
    // 每个IP每秒最多5次访问 
}

4. 复杂表达式场景

java 复制代码
@RateLimiter(keyResolver = ExpressionRateLimiterKeyResolver.class, keyArg = "#userId + ':' + #type")
public void businessMethod(Long userId, Integer type) { 
    // 根据userId+type组合限流 
}

六、设计总结

策略模式优势

  1. 灵活扩展 ➜ 新增策略只需实现接口,无需修改核心逻辑
    ✅ 示例:添加 OrderRateLimiterKeyResolver 只需 10 行代码
  2. 动态切换 ➜ 通过注解参数即时切换策略
    ✅ 如 @RateLimiter(keyResolver=UserRateLimiterKeyResolver.class)
  3. 隔离变化 ➜ 算法变化仅影响具体策略类

代理模式优势

  1. 业务无侵入 ➜ 原始代码无需任何修改
    ✅ 通过 @Before 注解实现逻辑增强
  2. 集中管控 ➜ 所有限流规则在切面中统一处理
    ✅ 统计显示限流代码减少业务类 80% 的冗余
  3. 动态织入 ➜ 运行时决定代理逻辑
    ✅ 可通过配置动态启用/停用限流
相关推荐
左灯右行的爱情3 分钟前
缓存并发更新的挑战
jvm·数据库·redis·后端·缓存
xiaoxi66620 分钟前
Dubbo实战:四步实现注册中心平滑迁移
分布式·nacos·dubbo·注册中心
学习机器不会机器学习2 小时前
深入浅出JavaScript常见设计模式:从原理到实战(2)
开发语言·javascript·设计模式
刘翔在线犯法2 小时前
如何搭建spark yarn模式的集合集群
大数据·分布式·spark
碎梦归途2 小时前
23种设计模式-行为型模式之命令模式(Java版本)
java·开发语言·jvm·设计模式·命令模式·行为型模式
Betty_蹄蹄boo3 小时前
在Spark集群中搭建Standalone
大数据·分布式·spark
Themberfue3 小时前
Redis ⑥-string | hash | list
数据库·redis·分布式·缓存·list
DBWYX5 小时前
redis
java·redis·mybatis