滑动窗口限流

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 滑动窗口限流

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可以大大提升效率

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);

   }


}

测试代码:

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版本

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()就知道了!多思考问题,技术其实都在心里!

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
2月前
|
缓存 算法 Java
限流算法 - 基本实现
限流算法 - 基本实现
26 0
|
2月前
|
存储 算法 NoSQL
常见限流算法及其实现
在分布式系统中,随着业务量的增长,如何保护核心资源、防止系统过载、保证系统的稳定性成为了一个重要的问题。限流算法作为一种有效的流量控制手段,被广泛应用于各类系统中。本文将详细介绍四种常见的限流算法、两种常用的限流器工具,从原理、源码的角度进行分析。
157 0
|
4月前
|
算法 Go API
限流算法~
限流算法~
36 1
|
9月前
|
消息中间件 算法 Sentinel
只需5分钟,了解常见的四种限流算法
只需5分钟,了解常见的四种限流算法
199 4
|
10月前
|
存储 算法 Java
限流常见的算法有哪些呢?
限流常见的算法有哪些呢?
43 0
|
5月前
|
缓存 算法 NoSQL
常见限流算法解读
常见限流算法解读
|
10月前
|
算法
限流常见的算法有哪些?
常见的限流算法有以下几种:
53 0
|
12月前
|
算法
平稳限流?突发限流?还是时间窗口?三种限流算法分析与对比
漏桶限流算法和令牌桶限流算法是两种常见的限流算法,它们的原理和实现方式有所不同。 漏桶限流算法 漏桶限流算法是一种固定容量的桶,水以恒定的速率流出,来限制请求的流量。当请求到来时,会先加入到漏桶中,漏桶以恒定的速率处理请求,处理不了的请求会被丢弃。 以下是漏桶限流算法的流程图:
82 0
|
数据采集 算法 安全
限流不只有计数器,带你快速了解四种经典限流算法实现
限流不只有计数器,带你快速了解四种经典限流算法实现
108 0
限流不只有计数器,带你快速了解四种经典限流算法实现
|
算法
熔断和限流原理和使用(2)
熔断和限流原理和使用(2)
160 0
熔断和限流原理和使用(2)