Redis 实现限流:从滑动窗口到多方案对比
限流是保障系统稳定性的关键手段,通过控制单位时间内的请求量,防止服务因过载而崩溃。Redis 凭借其高性能和原子操作特性,成为实现分布式限流的常用工具。
滑动窗口限流
基于 Redis 的 ZSet 实现了滑动窗口限流,核心思想是精确统计任意时间窗口内的请求量,避免固定窗口的 “边界突发” 问题。
通过统计该窗口内的行为数量和限制的最大数量maxCount进行比较就可以得出当前的请求是否允许
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| public class RedisRateLimiter { @Autowired private StringRedisTemplate stringRedisTemplate;
@Test public void test() { for (int i = 0; i < 10; i++) { System.out.println(isActionAllow("user/list", "127.0.0.1", 1, 5)); try { Thread.sleep(150); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
public boolean isActionAllow(String uri, String ip, int period, int maxCount) { String key = String.format("hist:%s:%s", uri, ip); long cur = System.currentTimeMillis(); List<Object> pipelined = stringRedisTemplate.executePipelined( new RedisCallback<Long>() {
@Override public Long doInRedis(RedisConnection connection) throws DataAccessException { connection.zAdd(key.getBytes(), cur, String.valueOf(cur).getBytes()); connection.zRemRangeByScore(key.getBytes(), 0, cur - period * 1000); Long count = connection.zCard(key.getBytes()); connection.expire(key.getBytes(), period + 1); return count; } } );
Object o = pipelined.get(2);
return Long.parseLong(String.valueOf(o)) <= maxCount; } }
|
如果时间窗口内允许的数量较大,会消耗大量的内存。则不适合该方式
实现原理
- 存储结构:用
ZSet 存储请求记录,score 为请求时间戳(毫秒级),value 为时间戳(确保唯一性)。
- 核心操作:
- 新增当前请求的时间戳到
ZSet。
- 移除时间窗口外的记录(如窗口为 1 秒,则移除
当前时间 - 1000ms 之前的记录)。
- 统计窗口内的记录数,若小于等于
maxCount 则允许请求,否则限流。
- 为
ZSet 设置过期时间,避免冷数据占用内存。
代码优化:原子性保证
上述代码使用了 pipeline 批量执行命令,但 pipeline 仅能减少网络往返,不能保证操作的原子性。高并发下,可能出现 “新增记录后,其他请求已修改了 ZSet,导致计数不准确” 的问题。
解决方案:用 Lua 脚本封装操作,确保添加、移除、计数三步原子执行: