在分布式系统中,限流是保护服务的重要手段之一。通过限流,可以防止接口被恶意刷请求或突发流量压垮,从而保证系统的稳定性。Redis 是一个高性能的键值存储工具,因其高效的读写性能和丰富的数据结构,被广泛用于限流场景。本文将介绍三种使用 Redis 实现限流的方式,并通过代码示例说明其实现原理和应用场景。

一、基于计数器的固定窗口限流

实现原理

固定窗口限流是一种最简单的限流方式。以时间窗口为单位,例如 1 秒或 1 分钟,统计当前时间窗口内的请求次数。如果超过限定次数,则拒绝后续请求。

适用场景

适用于流量较为均匀的场景,适合简单的限流需求。

实现步骤

  1. 使用 Redis 的 INCR 命令记录每个窗口的请求次数。
  2. 设置键的过期时间为窗口时长。
  3. 判断请求次数是否超过阈值。

代码实现

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class FixedWindowRateLimiter {

    @Autowired
    private StringRedisTemplate redisTemplate;

    public boolean isAllowed(String key, int limit, int windowSeconds) {
        String redisKey = "rate_limit:" + key;
        Long count = redisTemplate.opsForValue().increment(redisKey);

        if (count == 1) {
            redisTemplate.expire(redisKey, windowSeconds, TimeUnit.SECONDS);
        }

        return count <= limit;
    }
}

缺点

  • 流量分布不均时容易产生短时间的流量突发问题(比如窗口边界的请求会被集中执行)。

二、基于滑动窗口的限流

实现原理

滑动窗口限流通过更精细地统计一定时间范围内的请求,避免了固定窗口限流的边界问题。例如,统计过去 1 分钟内的请求总数,而不是固定在某些时间段内。

适用场景

适用于对流量分布较为敏感的场景。

实现步骤

  1. 使用 Redis 的有序集合(ZSET)存储每次请求的时间戳。
  2. 每次请求时,移除集合中超出时间窗口的记录。
  3. 统计集合中剩余的记录数,判断是否超出阈值。

代码实现

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.util.concurrent.TimeUnit;

@Service
public class SlidingWindowRateLimiter {

    @Autowired
    private StringRedisTemplate redisTemplate;

    public boolean isAllowed(String key, int limit, int windowSeconds) {
        String redisKey = "rate_limit:" + key;
        long currentTime = Instant.now().getEpochSecond();
        long windowStart = currentTime - windowSeconds;

        // 添加当前请求时间戳到 ZSET 中
        redisTemplate.opsForZSet().add(redisKey, String.valueOf(currentTime), currentTime);

        // 移除 ZSET 中超出时间窗口的请求
        redisTemplate.opsForZSet().removeRangeByScore(redisKey, 0, windowStart);

        // 获取当前窗口内的请求数量
        Long count = redisTemplate.opsForZSet().zCard(redisKey);

        return count != null && count <= limit;
    }
}

优点

  • 较好地解决了固定窗口限流的边界问题。

缺点

  • 比固定窗口限流实现更复杂,对 Redis 的性能要求更高。

三、基于令牌桶算法的限流

实现原理

令牌桶算法是最常用的限流算法之一。其核心思想是按照固定的速率向桶中放入令牌,用户每次请求需要获取一个令牌,如果桶为空,则拒绝请求。

适用场景

适用于对流量速率有严格控制的场景,例如 API 网关、秒杀系统等。

实现步骤

  1. 初始化令牌桶大小和放令牌的速率。
  2. 使用 Redis 的 Lua 脚本实现原子操作,保证线程安全。
  3. 判断是否有足够令牌满足请求。

代码实现

Lua 脚本

local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local tokens = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local refillTime = tonumber(ARGV[4])

local currentTokens = tonumber(redis.call("get", key)) or capacity
local lastRefillTime = tonumber(redis.call("get", key .. ":time")) or now

local timeElapsed = now - lastRefillTime
local refillTokens = math.floor(timeElapsed / refillTime)

currentTokens = math.min(capacity, currentTokens + refillTokens)
if currentTokens > 0 then
    redis.call("set", key, currentTokens - 1)
    redis.call("set", key .. ":time", now)
    return 1
else
    return 0
end

Java 实现

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.Collections;

@Service
public class TokenBucketRateLimiter {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final String LUA_SCRIPT = "-- Lua Script Here (见上文)";

    public boolean isAllowed(String key, int capacity, int tokens, int refillTime) {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>(LUA_SCRIPT, Long.class);
        Long result = redisTemplate.execute(
                script,
                Collections.singletonList("rate_limit:" + key),
                String.valueOf(capacity),
                String.valueOf(tokens),
                String.valueOf(System.currentTimeMillis() / 1000),
                String.valueOf(refillTime)
        );

        return result != null && result == 1;
    }
}

优点

  • 控制流量更加精确。
  • 支持突发流量处理。

缺点

  • 实现复杂度较高。

四、总结

限流方式

优点

缺点

适用场景

固定窗口限流

实现简单,性能开销低

边界问题可能导致短时间流量突发

简单的限流需求

滑动窗口限流

更精确的流量控制,避免边界问题

实现复杂度和性能开销较高

对流量敏感的业务场景

令牌桶算法

控制流量精确,支持突发流量处理

实现较为复杂,需要 Lua 脚本支持

秒杀系统、API 网关等关键场景

通过 Redis 实现限流,可以结合业务需求选择合适的限流方案,从而保护系统稳定运行并提高用户体验。