如何基于Redis实现滑动窗口限流?
2 个回答
天一亮,时间就不属于我了。李青云基于 Redis 的滑动窗口限流是一种流量控制策略,用于控制在固定时间内允许的请求数量。通过 Redis 的有序集合(ZSET)数据结构,可以实现简单的滑动窗口限流,下面是实现流程与示例代码。 ### 滑动窗口限流概念 滑动窗口限流通过滑动时间窗口限制固定时间内的请求数量。窗口内允许的请求量是固定的,随着时间推进,窗口持续滑动。在 Redis 中可以通过 ZSET 实现滑动窗口限流,因为 ZSET 能高效存储请求的时间戳,便于查询和删除过期请求。 #### 滑动窗口限流的主要优势 滑动窗口限流能够平滑控制流量,避免系统过载,同时也能灵活应对突发流量。 ### 实现思路 假设我们限制某接口在 60 秒内最多允许请求 100 次,具体实现步骤如下: 1. **定义滑动窗口时间范围**:例如窗口大小为 60 秒。 2. **记录请求时间戳**:每次请求到达时,将请求的时间戳存入 Redis 的 ZSET 中,以接口名或资源名作为 Key。 3. **删除过期请求**:使用 `ZREMRANGEBYSCORE` 命令删除超出窗口时间(小于当前时间减去 60 秒)的请求。 4. **统计请求数量**:通过 `ZCARD` 获取窗口内的请求数量,判断请求数是否超过限制。 ### 基于 Redis ZSET 实现滑动窗口限流示例 以下是 Java 代码示例,使用 Lua 脚本来保证操作的原子性: ```java import redis.clients.jedis.Jedis; public class SlidingWindowRateLimiter { private Jedis jedis; private int limit; // 限制的请求数量 private int windowSize; // 窗口大小,单位毫秒 public SlidingWindowRateLimiter(Jedis jedis, int limit, int windowSize) { this.jedis = jedis; this.limit = limit; this.windowSize = windowSize; } public boolean allowRequest(String key) { long currentTime = System.currentTimeMillis(); String luaScript = "local window_start = ARGV[1] - tonumber(ARGV[2])\n" + "redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', window_start)\n" + "local current_requests = redis.call('ZCARD', KEYS[1])\n" + "if current_requests < tonumber(ARGV[3]) then\n" + " redis.call('ZADD', KEYS[1], ARGV[1], ARGV[1])\n" + " return 1\n" + "else\n" + " return 0\n" + "end"; Object result = jedis.eval(luaScript, 1, key, String.valueOf(currentTime), String.valueOf(windowSize), String.valueOf(limit)); return (Long) result == 1; } } ``` #### 代码说明 1. **ZREMRANGEBYSCORE**:删除 ZSET 中超出窗口时间范围的请求记录,`-inf` 表示负无穷大。 2. **ZCARD**:统计窗口内的请求数量,判断是否超过限制。 3. **Lua 脚本**:保证删除、统计和添加请求是原子操作。 ### Redis 命令说明 - **ZREMRANGEBYSCORE key min max**:删除 ZSET 中指定范围(`min` 到 `max`)内的成员。在代码中,`-inf` 表示删除早于窗口起始时间的请求。 - **ZCARD key**:获取 ZSET 中的成员数量,便于统计窗口内的请求量。 ### 扩展知识:Redisson 限流器 在 Redisson 框架中,有自带的限流器(RRateLimiter),其底层实现是基于令牌桶算法,不支持滑动窗口限流。它的使用示例如下: ```java public Boolean tryAcquire(String key, int limit, int windowSize) { RRateLimiter rRateLimiter = redissonClient.getRateLimiter("rateLimiter:" + key); if (!rRateLimiter.isExists()) { rRateLimiter.trySetRate(RateType.OVERALL, limit, windowSize, RateIntervalUnit.SECONDS); } return rRateLimiter.tryAcquire(); } ``` ### 总结 Redis 的 ZSET 可以高效实现滑动窗口限流。通过随机化请求的时间戳和使用 Lua 脚本保证操作原子性,可以在高并发场景下实现平滑的流量控制。Redisson 的限流器则提供了令牌桶的限流策略,可在不同需求下灵活选用。编辑于 2024-11-27 01:19 •IP属地 美国举报评论收藏
为了击碎你伪装的表面,我只能用尽全身力气挥出那惊鸿的半月弧光。云说### 1、需求 限定用户的某个行为在指定时间T内,只允许发生N次。假设T为1秒钟,N为1000次。 ### 2、常见的错误设计 程序员设计了一个在每分钟内只允许访问1000次的限流方案,如下图01:00s-02:00s之间只允许访问1000次,这种设计最大的问题在于,请求可能在01:59s-02:00s之间被请求1000次,02:00s-02:01s之间被请求了1000次,这种情况下01:59s-02:01s间隔0.02s之间被请求2000次,很显然这种设计是错误的。  ### 3、滑动窗口算法 #### 3.1 解决方案 指定时间T内,只允许发生N次。我们可以将这个指定时间T,看成一个滑动时间窗口(定宽)。我们采用Redis的zset基本数据类型的score来圈出这个滑动时间窗口。在实际操作zset的过程中,我们只需要保留在这个滑动时间窗口以内的数据,其他的数据不处理即可。 每个用户的行为采用一个zset存储,score为毫秒时间戳,value也使用毫秒时间戳(比UUID更加节省内存) 只保留滑动窗口时间内的行为记录,如果zset为空,则移除zset,不再占用内存(节省内存)  #### 3.2 pipeline代码实现 代码的实现的逻辑是统计滑动窗口内zset中的行为数量,并且与阈值maxCount直接进行比较就可以判断当前行为是否被允许。这里涉及多个redis操作,因此使用pipeline可以大大提升效率 ```java package com.lizba.redis.limit; import redis.clients.jedis.Jedis; import redis.clients.jedis.Pipeline; import redis.clients.jedis.Response; /** * <p> * 通过zset实现滑动窗口算法限流 * </p> * * @Author: Liziba * @Date: 2021/9/6 18:11 */ public class SimpleSlidingWindowByZSet { private Jedis jedis; public SimpleSlidingWindowByZSet(Jedis jedis) { this.jedis = jedis; } /** * 判断行为是否被允许 * * @param userId 用户id * @param actionKey 行为key * @param period 限流周期 * @param maxCount 最大请求次数(滑动窗口大小) * @return */ public boolean isActionAllowed(String userId, String actionKey, int period, int maxCount) { String key = this.key(userId, actionKey); long ts = System.currentTimeMillis(); Pipeline pipe = jedis.pipelined(); pipe.multi(); pipe.zadd(key, ts, String.valueOf(ts)); // 移除滑动窗口之外的数据 pipe.zremrangeByScore(key, 0, ts - (period * 1000)); Response<Long> count = pipe.zcard(key); // 设置行为的过期时间,如果数据为冷数据,zset将会删除以此节省内存空间 pipe.expire(key, period); pipe.exec(); pipe.close(); return count.get() <= maxCount; } /** * 限流key * * @param userId * @param actionKey * @return */ public String key(String userId, String actionKey) { return String.format("limit:%s:%s", userId, actionKey); } } ``` 测试代码: ```java package com.lizba.redis.limit; import redis.clients.jedis.Jedis; /** * * @Author: Liziba * @Date: 2021/9/6 20:10 */ public class TestSimpleSlidingWindowByZSet { public static void main(String[] args) { Jedis jedis = new Jedis("192.168.211.108", 6379); SimpleSlidingWindowByZSet slidingWindow = new SimpleSlidingWindowByZSet(jedis); for (int i = 1; i <= 15; i++) { boolean actionAllowed = slidingWindow.isActionAllowed("liziba", "view", 60, 5); System.out.println("第" + i +"次操作" + (actionAllowed ? "成功" : "失败")); } jedis.close(); } } ``` ### 测试效果: 从测试输出的数据可以看出,起到了限流的效果,从第11次以后的请求操作都是失败的,但是这个和我们允许的5次误差还是比较大的。这个问题的原因是我们测试System.currentTimeMillis()的毫秒可能相同,而且此时value也是System.currentTimeMillis()也相同,会导致zset中元素覆盖! **修改代码测试:** 在循环中睡眠1毫秒即可,测试结果符合预期! ``` TimeUnit.MILLISECONDS.sleep(1); ```  #### 3.3 lua代码实现 我们在项目中使用原子性的lua脚步来实现限流的使用会更多,因此这里也提供一个基于操作zset的lua版本 ```java package com.lizba.redis.limit; import com.google.common.collect.ImmutableList; import redis.clients.jedis.Jedis; import redis.clients.jedis.Pipeline; import redis.clients.jedis.Response; /** * <p> * 通过zset实现滑动窗口算法限流 * </p> * * @Author: Liziba * @Date: 2021/9/6 18:11 */ public class SimpleSlidingWindowByZSet { private Jedis jedis; public SimpleSlidingWindowByZSet(Jedis jedis) { this.jedis = jedis; } /** * lua脚本限流 * * @param userId * @param actionKey * @param period * @param maxCount * @return */ public boolean isActionAllowedByLua(String userId, String actionKey, int period, int maxCount) { String luaScript = this.buildLuaScript(); String key = key(userId, actionKey); long ts = System.currentTimeMillis(); System.out.println(ts); ImmutableList<String> keys = ImmutableList.of(key); ImmutableList<String> args = ImmutableList.of(String.valueOf(ts),String.valueOf((ts - period * 1000)), String.valueOf(period)); Number count = (Number) jedis.eval(luaScript, keys, args); return count != null && count.intValue() <= maxCount; } /** * 限流key * * @param userId * @param actionKey * @return */ private String key(String userId, String actionKey) { return String.format("limit:%s:%s", userId, actionKey); } /** * 针对某个key使用lua脚本限流 * * @return */ private String buildLuaScript() { return "redis.call('ZADD', KEYS[1], tonumber(ARGV[1]), ARGV[1])" + "\nlocal c" + "\nc = redis.call('ZCARD', KEYS[1])" + "\nredis.call('ZREMRANGEBYSCORE', KEYS[1], 0, tonumber(ARGV[2]))" + "\nredis.call('EXPIRE', KEYS[1], tonumber(ARGV[3]))" + "\nreturn c;"; } } ``` 测试代码不变,大家可以自行测试,记得还是要考虑我们测试的时候System.currentTimeMillis()相等的问题,不信你输出System.currentTimeMillis()就知道了!多思考问题,技术其实都在心里!发布于 2024-10-26 04:05 •IP属地 天津举报评论收藏
公众号进入网站体验更佳

