工程实践 Redis

Redis-Cell 限流模块:令牌桶的原子实现

详解 Redis 4.0 新增的 Redis-Cell 模块(CL.THROTTLE 命令)、令牌桶 vs 漏桶算法对比,以及在银行 API 网关、支付接口限流中的实战。

发布于 2026/03/21 更新于 2026/03/21 1 分钟

“Redis-Cell 是 Redis 4.0 带来的原生限流命令,比 Lua 脚本更快、比滑动窗口更精确。银行 API 网关的限流,CL.THROTTLE 是首选。“

前言

Redis 实现限流有三种方案:Lua 脚本、滑动窗口、Redis-Cell。Redis-Cell(cl.throttle 命令)是最简单的令牌桶实现,Redis 原生支持,原子操作,无需编写 Lua 脚本。在银行 API 网关中,限流是保障系统稳定性的第一道防线。

1. 限流算法对比

算法原理优点缺点
计数器固定窗口内计数简单边界突发问题
滑动窗口移动时间窗口精度较高实现复杂
漏桶恒定速率消费流量平滑不适合突发
令牌桶固定速率放令牌允许突发 + 平滑实现较复杂
Redis-Cell原生命令桶原子、简单、高性能需要安装模块

2. CL.THROTTLE 命令详解

Redis-Cell 模块是 Redis 4.0+ 的可选模块,需要单独安装:

# 安装(Linux/macOS)
wget https://github.com/brandt/redis-cell/releases/download/v0.3.0/redis-cell-v0.3.0-x86_64-unknown-linux-gnu.tar.gz
tar xzf redis-cell-v0.3.0-x86_64-unknown-linux-gnu.tar.gz
cp libredis_cell.so /path/to/redis/modules/
redis-server --loadmodule /path/to/redis/modules/libredis_cell.so

# Docker 方式(推荐)
docker run -d --name redis-cell \
  redis:7 redis-server --loadmodule /usr/local/lib/redis/modules/libredis_cell.so

2.1 命令格式

# CL.THROTTLE key capacity count period [quota] [block]
#
# 参数:
#   key       - 限流 key(用户 ID、IP、API 路径)
#   capacity  - 漏斗容量(即最大突发量)
#   count     - 补充速率(period 时间内补充的令牌数)
#   period    - 时间窗口(秒)
#   quota     - 可选,刷新周期内的配额(默认 = count)
#   block     - 可选,阻塞等待时间(秒,0 = 不阻塞)

# 示例:每 60 秒最多 30 次请求,容量 15(即前 15 次可以突发通过)
CL.THROTTLE user:1000:api 15 30 60

2.2 返回值解析

CL.THROTTLE user:1000:api 15 30 60

# 返回 5 个值:
# 1) 0           → 0=允许,1=拒绝
# 2) 15          → 容量(capacity)
# 3) 14          → 剩余配额(quota left)
# 4) 0           → 如果被拒绝,下次重试需等多少秒(0 表示立即可重试)
# 5) 2           → 多长时间后配额完全恢复(秒)
  quota = 0(已耗尽)


  ┌─────────────────────────────────────────┐
  │ capacity = 15(漏斗容量,最大突发量)      │
  │ ▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░ ← quota left = 12   │
  │ ▓ = 已使用的配额                          │
  │ ░ = 剩余配额                             │
  └─────────────────────────────────────────┘
    ↑ 每秒补充 count/period 个配额
    ↑ quota 归零后,新请求被拒绝

3. 常见限流场景配置

# 场景 1:普通 API 限流(每用户每分钟 60 次)
CL.THROTTLE rate:user:1000:api 60 60 60
# 容量 60 + 速率 60/60秒 = 每秒 1 次

# 场景 2:支付接口严格限流(每用户每分钟 5 次)
CL.THROTTLE rate:user:1000:payment 5 5 60
# 容量 5 + 5次/60秒 = 支付接口极为严格

# 场景 3:突发限流(允许短时突发,但长期不超过速率)
# 用户平时请求少,突然来 20 个请求,容量 20 允许突发通过
CL.THROTTLE rate:user:1000:api 20 10 60
# 容量 20 + 10次/60秒

# 场景 4:全局限流(IP 级别)
CL.THROTTLE rate:ip:192.168.1.100:api 100 200 60

4. Java 实现:Spring Boot 整合

4.1 依赖与配置

<dependency>
    <groupId>io.github.resdq0x</groupId>
    <artifactId>redis-cell</artifactId>
    <version>0.3.0</version>
</dependency>
@Configuration
public class RedisCellConfig {
    @Bean
    public RedisClient redisClient(RedisConnectionFactory factory) {
        return RedisClient.create(factory.getReactiveConnection());
    }
}

4.2 限流服务

@Service
@Slf4j
public class RateLimitService {
    private final RedisTemplate<String, String> redis;

    /**
     * 执行限流检查
     * @param key      限流 key(通常 = rate:user:{userId}:{apiPath})
     * @param capacity 漏斗容量(最大突发量)
     * @param count    速率(每 period 秒内的配额数)
     * @param period   时间窗口(秒)
     * @return RateLimitResult 包含是否允许、剩余配额、重试时间
     */
    public RateLimitResult tryAcquire(String key, long capacity,
                                      long count, long period) {
        String[] args = {
            String.valueOf(capacity),
            String.valueOf(count),
            String.valueOf(period)
        };

        // 使用反射调用 CL.THROTTLE(Spring Data Redis 未直接封装)
        Long[] result = redis.execute(
            new RedisCallback<Long[]>() {
                @Override
                public Long[] doInRedis(RedisConnection connection)
                        throws DataAccessException {
                    connection.commands(
                        (Commands) connection.getNativeConnection()
                    );
                    // 调用原生 CL.THROTTLE
                    return executeThrottle(connection, key, args);
                }
            }
        );

        if (result == null || result.length < 5) {
            log.warn("CL.THROTTLE 调用失败,假设允许通过");
            return RateLimitResult.allowed(capacity, capacity);
        }

        boolean allowed = result[0] == 0;
        long quotaLeft = result[2];
        long retryAfter = result[3];
        long resetIn = result[4];

        return new RateLimitResult(allowed, quotaLeft, retryAfter, resetIn, capacity);
    }

    // 反射调用 CL.THROTTLE(Redis-Cell 模块命令)
    private Long[] executeThrottle(RedisConnection conn,
                                   String key, String[] args) {
        try {
            Commands commands = (Commands) conn;
            CommandResponse<Long[], ?> resp =
                commands.execute("CL.THROTTLE",
                    key, args[0], args[1], args[2]);

            // 解析 Redis 数组响应
            // 返回格式:[0, capacity, quota_left, retry_after, reset_in]
            return parseArrayResponse(resp);
        } catch (Exception e) {
            log.error("限流命令执行失败: key={}", key, e);
            return null;
        }
    }
}

@Data
@AllArgsConstructor
public class RateLimitResult {
    private boolean allowed;     // 是否允许通过
    private long quotaLeft;      // 剩余配额
    private long retryAfter;    // 拒绝后重试等待秒数
    private long resetIn;        // 完全恢复所需秒数
    private long capacity;      // 容量

    public static RateLimitResult allowed(long quotaLeft, long capacity) {
        return new RateLimitResult(true, quotaLeft, 0, 0, capacity);
    }
}

4.3 限流注解 + AOP

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
    String key();                       // 限流 key 模板
    long capacity() default 60;         // 容量(突发量)
    long count() default 60;           // 速率
    long period() default 60;          // 时间窗口(秒)
    String message() default "请求过于频繁,请稍后重试";
}

@Aspect
@Component
@Slf4j
public class RateLimitAspect {

    private final RateLimitService rateLimitService;

    @Around("@annotation(rateLimit)")
    public Object around(ProceedingJoinPoint joinPoint,
                         RateLimit rateLimit) throws Throwable {

        // 从请求上下文提取 key
        String key = resolveKey(rateLimit.key(), joinPoint);

        RateLimitResult result = rateLimitService.tryAcquire(
            key,
            rateLimit.capacity(),
            rateLimit.count(),
            rateLimit.period()
        );

        if (!result.isAllowed()) {
            log.warn("限流触发: key={}, quotaLeft={}", key, result.getQuotaLeft());
            throw new RateLimitExceededException(
                rateLimit.message(),
                result.getRetryAfter()
            );
        }

        return joinPoint.proceed();
    }

    private String resolveKey(String keyTemplate, ProceedingJoinPoint point) {
        // 替换 {userId}、{ip} 等占位符
        HttpServletRequest request = getRequest();
        return keyTemplate
            .replace("{userId}", getCurrentUserId())
            .replace("{ip}", getClientIp())
            .replace("{path}", request.getRequestURI());
    }
}

4.4 Controller 使用

@RestController
@RequestMapping("/api/v1/payments")
@RequiredArgsConstructor
@Slf4j
public class PaymentController {

    @RateLimit(
        key = "rate:user:{userId}:payment",
        capacity = 10,
        count = 10,
        period = 60,
        message = "支付接口限流,每分钟最多 10 次"
    )
    @PostMapping
    public ResponseEntity<ApiResponse<PaymentResponse>> createPayment(
            @Valid @RequestBody PaymentRequest request) {
        // 业务逻辑
        return ResponseEntity.ok(ApiResponse.success(response));
    }

    @RateLimit(
        key = "rate:user:{userId}:api",
        capacity = 120,
        count = 120,
        period = 60,
        message = "API 调用超限,每分钟最多 120 次"
    )
    @GetMapping("/balance")
    public ResponseEntity<ApiResponse<BigDecimal>> getBalance(
            @RequestParam String accountId) {
        return ResponseEntity.ok(ApiResponse.success(balance));
    }
}

5. 银行系统限流策略

# 银行 API 网关分层限流:

# 第一层:全局限流(Redis Cluster)
CL.THROTTLE rate:global:api 10000 10000 1
# 1 秒内全局最多 10000 请求

# 第二层:服务级别限流
CL.THROTTLE rate:service:payment 1000 1000 1
# payment 服务每秒最多 1000 请求

# 第三层:用户级别限流
CL.THROTTLE rate:user:123456:payment 5 5 60
# 每个用户每分钟最多 5 次支付

# 第四层:IP 级别防爬
CL.THROTTLE rate:ip:203.0.113.50:api 50 100 60
# 每个 IP 每分钟最多 100 次
@Service
@RequiredArgsConstructor
@Slf4j
public class TieredRateLimitService {
    private final RateLimitService rateLimitService;

    public void checkAllTiers(String userId, String ip, String service) {
        // 1. 全局限流
        RateLimitResult global = rateLimitService.tryAcquire(
            "rate:global:api", 5000, 5000, 1);
        if (!global.isAllowed()) {
            throw new RateLimitExceededException("系统繁忙,请稍后重试", 1);
        }

        // 2. 服务级限流
        RateLimitResult serviceLimit = rateLimitService.tryAcquire(
            "rate:service:" + service, 500, 500, 1);
        if (!serviceLimit.isAllowed()) {
            throw new RateLimitExceededException(service + " 服务限流,请稍后重试", 1);
        }

        // 3. 用户级限流
        RateLimitResult userLimit = rateLimitService.tryAcquire(
            "rate:user:" + userId + ":" + service, 60, 60, 60);
        if (!userLimit.isAllowed()) {
            throw new RateLimitExceededException("请求过于频繁", userLimit.getRetryAfter());
        }

        // 4. IP 级防爬
        RateLimitResult ipLimit = rateLimitService.tryAcquire(
            "rate:ip:" + ip + ":api", 100, 100, 60);
        if (!ipLimit.isAllowed()) {
            log.warn("IP 疑似爬虫: ip={}", ip);
            throw new RateLimitExceededException("请求过于频繁", ipLimit.getRetryAfter());
        }
    }
}

6. 限流结果处理

// 全局异常处理:限流ExceededException
@ExceptionHandler(RateLimitExceededException.class)
public ResponseEntity<ApiResponse<Void>> handleRateLimit(
        RateLimitExceededException ex, HttpServletResponse response) {

    // 设置 Retry-After 响应头(RFC 6585)
    response.setHeader("Retry-After", String.valueOf(ex.getRetryAfter()));
    response.setHeader("X-RateLimit-Remaining", "0");

    return ResponseEntity
        .status(429)  // 429 Too Many Requests
        .header("Retry-After", String.valueOf(ex.getRetryAfter()))
        .body(ApiResponse.error("RATE_LIMIT_EXCEEDED", ex.getMessage(), null));
}

7. Redis-Cell vs 其他方案对比

特性Redis-CellLua 脚本滑动窗口
原子性✅ 原生✅ Lua 原子⚠️ 需配合
实现难度
性能最高
精度令牌桶精度可自定义依赖设计
额外依赖需安装模块
阻塞模式支持可实现可实现

相关阅读:项目稳定性-限流方案全解析 · [Redis 实现分布式锁](/coding/Redis/Redis 实现分布式锁) · Spring Cloud Gateway 银行网关实战