首页 > 数据库 >分布式限流——基于Redis的Lua脚本限流实现

分布式限流——基于Redis的Lua脚本限流实现

时间:2024-01-13 18:45:53浏览次数:38  
标签:INFO 2024 13 01 Redis --- Lua 限流 main

分布式限流

当你的应用分布式部署出现对等端(peer)时,单机的限流往往不能满足对下游保护的作用,因为它仅仅是jvm内存层面的流量控制。这个时候自然而然会想到用一些跨JVM的分布式中间件控制在单位时间窗口内的请求是否通行,本文我们将探讨如何借助Redis实现分布式限流。

1 固定窗口限流

前文已经介绍了固定时间窗口的限流原理和算法,此处不再赘述。直接上代码:

@Slf4j
@Component
public class DistributedFixedRateLimiter {

    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;

    public boolean tryAcquire( String key,int windowSize, int maxRequestCount){
        String script = getFixedRateLimiterLuaScript();
        Long res = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
                Collections.singletonList(key),
                maxRequestCount, windowSize);
        if (res != null && res == -1) {
            log.info("请求失败");

            return true;
        }else {
            log.info("--- 请求成功 , 剩余可用请求数:{}", res);
            return false;
        }
    }
}

这里的getFixedRateLimiterLuaScript()方法就是获取Lua脚本,Lua脚本代码如下:

local key = KEYS[1] -- 限流资源
local maxRequestCount = ARGV[1] -- 限流请求数
local windowSize = ARGV[2] -- 限流时间
local currentCount = redis.call('get', key) -- 当前请求数
-- 限流存在并且超过限流大小,则返回剩余可用请求数=0
if (currentCount and tonumber(currentCount) >= tonumber(maxRequestCount)) then
    return -1
end
-- 请求数自增
currentCount = redis.call('incr', key)
-- 第一次请求,则设置过期时间
if (tonumber(currentCount) == 1) then
    redis.call('expire', key, windowSize)
end
-- 返回剩余可用请求数
return tonumber(maxRequestCount) - tonumber(currentCount)

测试:

@Test
public void test05() {
    String key = "/fixed/window";
    for (int i = 0; i < 10; i++) {
        boolean b = limiter.tryAcquire(key,60, 5);
    }
}

image-20240112222838911

2 滑动窗口限流

基于redis+lua脚本的滑动窗口限流和前文中介绍的略有不同,并不是通过将时间窗口分割成固定大小的子窗口,以一个子窗口为单位移动,而是根据每次请求的间隔,释放掉这次请求的时间减去时间窗口之前的请求,以此来移动。

具体的,通过zset实现滑动窗口,其中zset的key为请求资源,member为单次的请求id,score为当前时间戳:

@Slf4j
@Component
public class DistributedSlideWindowRateLimiter {

    @Resource
    private RedisTemplate<Object, Object> redisTemplate;

    public boolean tryAcquire(String key, String currentTimeKey ,long windowSize, int limitCount, long currentTime) throws Exception {
        String script = getSlideWindowLuaScript();
        List<Long> result = redisTemplate.execute(new DefaultRedisScript<>(script, List.class),
                Arrays.asList(new String[]{key, currentTimeKey}),
                windowSize, limitCount, currentTime);
        if (result == null)
            throw new Exception("redis execute occur exception!");
        log.info("剩余请求数:{}", result.get(1));
        return result.get(0) == 1;
    }

    private String getSlideWindowLuaScript() {
        return "local key = KEYS[1]  -- 限流关键字\n" +
                "local current_time_key = KEYS[2]   -- 当前时间戳的key\n"+
                "local window_size = tonumber(ARGV[1])  -- 滑动窗口大小\n" +
                "local limit = tonumber(ARGV[2])  -- 限制的请求数\n" +
                "local current_time = tonumber(ARGV[3])  -- 当前时间戳\n" +
                "local last_requested = 0   -- 已经用掉的请求数\n"+
                "local remain_request = 0   -- 剩余可以分配的请求数\n"+
                "local allowed_num = 0  -- 本次允许通过的请求数\n" +
                "\n" +
                "local exists_key = redis.call('exists', key)\n" +
                "if (exists_key == 1) then\n" +
                "    last_requested = redis.call('zcard', key)\n" +
                "end\n"+
                "remain_request = limit - last_requested\n" +
                "if (last_requested < limit) then\n" +
                "    allowed_num = 1\n" +
                "    redis.call('zadd', key, current_time, current_time_key)\n" +
                "end\n" +
                "redis.call('zremrangebyscore', key, 0, current_time - window_size)\n" +
                "redis.call('expire', key, window_size)\n" +
                "\n" +
                "return { allowed_num, remain_request }";
    }
}

需要注意,返回的remain_request是请求前剩余的可用请求数。

为了更直观的看出效果,测试时每隔1秒请求一次,通过日志输出可以看出,每隔1秒请求,每次请求可以淘汰5秒前的请求,所以基本上都会有1个请求可用:

2024-01-13 14:39:10.783  INFO 7296 --- [           main] .r.d.s.DistributedSlideWindowRateLimiter : 剩余请求数:5
2024-01-13 14:39:11.797  INFO 7296 --- [           main] com.dianping.ratelimiter.AppTest         : 请求成功
2024-01-13 14:39:11.800  INFO 7296 --- [           main] .r.d.s.DistributedSlideWindowRateLimiter : 剩余请求数:4
2024-01-13 14:39:12.808  INFO 7296 --- [           main] com.dianping.ratelimiter.AppTest         : 请求成功
2024-01-13 14:39:12.810  INFO 7296 --- [           main] .r.d.s.DistributedSlideWindowRateLimiter : 剩余请求数:3
2024-01-13 14:39:13.813  INFO 7296 --- [           main] com.dianping.ratelimiter.AppTest         : 请求成功
2024-01-13 14:39:13.816  INFO 7296 --- [           main] .r.d.s.DistributedSlideWindowRateLimiter : 剩余请求数:2
2024-01-13 14:39:14.816  INFO 7296 --- [           main] com.dianping.ratelimiter.AppTest         : 请求成功
2024-01-13 14:39:14.818  INFO 7296 --- [           main] .r.d.s.DistributedSlideWindowRateLimiter : 剩余请求数:1
2024-01-13 14:39:15.821  INFO 7296 --- [           main] com.dianping.ratelimiter.AppTest         : 请求成功
2024-01-13 14:39:15.823  INFO 7296 --- [           main] .r.d.s.DistributedSlideWindowRateLimiter : 剩余请求数:0
2024-01-13 14:39:16.828  INFO 7296 --- [           main] com.dianping.ratelimiter.AppTest         : 请求失败
2024-01-13 14:39:16.830  INFO 7296 --- [           main] .r.d.s.DistributedSlideWindowRateLimiter : 剩余请求数:1
2024-01-13 14:39:17.832  INFO 7296 --- [           main] com.dianping.ratelimiter.AppTest         : 请求成功
2024-01-13 14:39:17.834  INFO 7296 --- [           main] .r.d.s.DistributedSlideWindowRateLimiter : 剩余请求数:1
2024-01-13 14:39:18.835  INFO 7296 --- [           main] com.dianping.ratelimiter.AppTest         : 请求成功
2024-01-13 14:39:18.837  INFO 7296 --- [           main] .r.d.s.DistributedSlideWindowRateLimiter : 剩余请求数:1
2024-01-13 14:39:19.837  INFO 7296 --- [           main] com.dianping.ratelimiter.AppTest         : 请求成功
2024-01-13 14:39:19.840  INFO 7296 --- [           main] .r.d.s.DistributedSlideWindowRateLimiter : 剩余请求数:1
2024-01-13 14:39:20.841  INFO 7296 --- [           main] com.dianping.ratelimiter.AppTest         : 请求成功
2024-01-13 14:39:20.843  INFO 7296 --- [           main] .r.d.s.DistributedSlideWindowRateLimiter : 剩余请求数:1
2024-01-13 14:39:21.851  INFO 7296 --- [           main] com.dianping.ratelimiter.AppTest         : 请求成功
2024-01-13 14:39:21.853  INFO 7296 --- [           main] .r.d.s.DistributedSlideWindowRateLimiter : 剩余请求数:0
2024-01-13 14:39:22.856  INFO 7296 --- [           main] com.dianping.ratelimiter.AppTest         : 请求失败
2024-01-13 14:39:22.857  INFO 7296 --- [           main] .r.d.s.DistributedSlideWindowRateLimiter : 剩余请求数:1
2024-01-13 14:39:23.863  INFO 7296 --- [           main] com.dianping.ratelimiter.AppTest         : 请求成功
2024-01-13 14:39:23.865  INFO 7296 --- [           main] .r.d.s.DistributedSlideWindowRateLimiter : 剩余请求数:1
2024-01-13 14:39:24.870  INFO 7296 --- [           main] com.dianping.ratelimiter.AppTest         : 请求成功
2024-01-13 14:39:24.872  INFO 7296 --- [           main] .r.d.s.DistributedSlideWindowRateLimiter : 剩余请求数:1
2024-01-13 14:39:25.877  INFO 7296 --- [           main] com.dianping.ratelimiter.AppTest         : 请求成功
2024-01-13 14:39:25.880  INFO 7296 --- [           main] .r.d.s.DistributedSlideWindowRateLimiter : 剩余请求数:1
2024-01-13 14:39:26.893  INFO 7296 --- [           main] com.dianping.ratelimiter.AppTest         : 请求成功
2024-01-13 14:39:26.895  INFO 7296 --- [           main] .r.d.s.DistributedSlideWindowRateLimiter : 剩余请求数:1
2024-01-13 14:39:27.898  INFO 7296 --- [           main] com.dianping.ratelimiter.AppTest         : 请求成功
2024-01-13 14:39:27.900  INFO 7296 --- [           main] .r.d.s.DistributedSlideWindowRateLimiter : 剩余请求数:0
2024-01-13 14:39:28.916  INFO 7296 --- [           main] com.dianping.ratelimiter.AppTest         : 请求失败
2024-01-13 14:39:28.918  INFO 7296 --- [           main] .r.d.s.DistributedSlideWindowRateLimiter : 剩余请求数:1
2024-01-13 14:39:29.924  INFO 7296 --- [           main] com.dianping.ratelimiter.AppTest         : 请求成功
2024-01-13 14:39:29.925  INFO 7296 --- [           main] .r.d.s.DistributedSlideWindowRateLimiter : 剩余请求数:1
2024-01-13 14:39:30.930  INFO 7296 --- [           main] com.dianping.ratelimiter.AppTest         : 请求成功

通过redis中zset成员的变化也不难看出,每次请求会淘汰5秒前的请求:

image-20240113144028957

image-20240113144037274

3 令牌桶算法

基于redis的令牌桶算法可以用hash实现,其中hash的key为请求资源,lua中内设字段tokens_count_fieldlast_refreshed_field分别表示桶中的令牌数和上次更新时间,编写Lua脚本时需要注意一点,rate速率在Java代码中的语义是每秒生成令牌的数量,在Lua中计算的时间差是用时间戳计算的,单位是毫秒,所以需要转换为秒。具体如下:

local key = KEYS[1]
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local current_time = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local tokens_count_field = 'bucket_token_count'
local last_refreshed_field = 'last_update_time'

local fill_time = capacity/rate
local ttl = math.floor(fill_time*2)

-- 如果过期了,先让bucket充满令牌
local last_tokens = tonumber(redis.call("hget", key, tokens_count_field))
if last_tokens == nil then
  last_tokens = capacity
end

local last_refreshed = tonumber(redis.call("hget", key, last_refreshed_field))
if last_refreshed == nil then
  last_refreshed = 0
end

local delta = math.max(0, current_time - last_refreshed)
local filled_tokens = math.min(capacity, math.floor(last_tokens + (delta / 1000 * rate)))
local allowed = filled_tokens >= requested
local remain_tokens = filled_tokens
local allowed_num = 0
if allowed then
  remain_tokens = filled_tokens - requested
  allowed_num = 1
end

redis.call("hset", key, tokens_count_field, remain_tokens)
redis.call("hset", key, last_refreshed_field, current_time)
redis.call("expire", key, ttl)

return { allowed_num, remain_tokens }

在测试代码时,我发现在计算filled_tokens时,理论上来说令牌数应该是整型,所以写代码时我下意识加了math.floor()向下取整,但是时间戳往往是很零碎的值(因为以毫秒为单位),如果每次保存时间间隔内生成的令牌时都向下取整,将会“损失精度”,使生成令牌的速率更”离散化“,而这样在突发流量到来时,由于请求间隔小(每次都是趋于0的状态),导致每次生成的令牌数都是0,通过日志输出可以证明这一猜想:

测试代码:(每秒生成1个令牌,桶容量为10,每次请求需要3个令牌)

@Test
public void test07() throws InterruptedException {
    String key = "/token/bucket";
    for (int i = 0; i < 20; i++) {
        long currentTime = System.currentTimeMillis();
        Thread.sleep(500);
        boolean b = tokenBucketRateLimiter.tryAcquire(key, 1, 10, currentTime, 3);
    }
}

以下是向下取整的输出结果:

2024-01-13 18:20:27.959  INFO 1368 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求成功, 剩余令牌:7
2024-01-13 18:20:28.474  INFO 1368 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求成功, 剩余令牌:5
2024-01-13 18:20:28.977  INFO 1368 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求成功, 剩余令牌:2
2024-01-13 18:20:29.488  INFO 1368 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:20:30.004  INFO 1368 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:20:30.509  INFO 1368 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:20:31.012  INFO 1368 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:20:31.518  INFO 1368 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:20:32.022  INFO 1368 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:20:32.524  INFO 1368 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:20:33.039  INFO 1368 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:20:33.546  INFO 1368 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:20:34.051  INFO 1368 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:20:34.566  INFO 1368 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:20:35.071  INFO 1368 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:20:35.575  INFO 1368 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:20:36.082  INFO 1368 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:20:36.584  INFO 1368 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:20:37.087  INFO 1368 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:20:37.595  INFO 1368 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败

以下是不取整的输出结果:

2024-01-13 18:14:00.753  INFO 14144 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求成功, 剩余令牌:7
2024-01-13 18:14:01.256  INFO 14144 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求成功, 剩余令牌:5
2024-01-13 18:14:01.759  INFO 14144 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求成功, 剩余令牌:2
2024-01-13 18:14:02.265  INFO 14144 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求成功, 剩余令牌:0
2024-01-13 18:14:02.785  INFO 14144 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:14:03.289  INFO 14144 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:14:03.792  INFO 14144 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:14:04.295  INFO 14144 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:14:04.801  INFO 14144 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:14:05.307  INFO 14144 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求成功, 剩余令牌:0
2024-01-13 18:14:05.813  INFO 14144 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:14:06.318  INFO 14144 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:14:06.824  INFO 14144 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:14:07.327  INFO 14144 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:14:07.833  INFO 14144 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:14:08.338  INFO 14144 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求成功, 剩余令牌:0
2024-01-13 18:14:08.844  INFO 14144 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:14:09.351  INFO 14144 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:14:09.857  INFO 14144 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败
2024-01-13 18:14:10.360  INFO 14144 --- [           main] .r.d.t.DistributedTokenBucketRateLimiter : 请求失败

image-20240113175043019

所以经过对比,我认为还是不应该取整,因为在如果生成的速率小于消费的速率,即短时间内到来的大量请求将会被阻塞。

本博客内容仅供个人学习使用,禁止用于商业用途。转载需注明出处并链接至原文。

标签:INFO,2024,13,01,Redis,---,Lua,限流,main
From: https://www.cnblogs.com/zhaobo1997/p/17962738

相关文章

  • VIM/NeoVIM:解决LuaSnip下Tab按键跳转冲突问题
    主要现象当使用LuaSnip生成片段时,即使切换过VIM模式,输入模式下的Tab按键仍然会导致光标跳转到Snippet的片段占位符处,导致光标“随机跳转”的问题。发生原因这是因为触发代码片段之后,LauSnip会一直维持一个记录占位符跳转的Session,这个Session在当前Buffer会一直持续到占位符结......
  • delphi redisclient测试
    unitUnit1;interfaceusesWinapi.Windows,Winapi.Messages,System.SysUtils,System.Variants,System.Classes,Vcl.Graphics,Vcl.Controls,Vcl.Forms,Vcl.Dialogs,Vcl.StdCtrls,Vcl.Buttons;typeTForm1=class(TForm)Memo1:TMemo;BitBtn1:......
  • freeswitch+lua实现IVR(互动式语音应答)
    IVR(InteractiveVoiceResponse)交互式语言应答,是呼叫中心的1个经典应用场景,FreeSwitch官方有一个利用lua实现的简单示例,大致原理是利用lua脚本+TTS实现,记录一下:(环境:FreeSwitch 1.10.11+Windows10)步骤1:安装TTSFreeSwitch自带了1个TTS引擎(发音效果比较生硬,仅支持英文,不过......
  • Redis 哨兵启动 以及 手动切换节点
      服务启动  ./redis-server ../redis.conf   哨兵启动./redis-sentinel../sentinel.conf查看当前服务是否是主节点(先登录到redis)INFOreplication 要将从节点切换为主节点,您可以执行以下步骤:首先,确保从节点已成功连接到主节点。您可以使用 INFOrep......
  • Linux 部署redis集群(三主三从)
    1、由于redis是C语言编写的,安装之前需要保证有gcc的环境配置首先使用命令,查看gcc版本,若已经存在则跳过gcc的安装:gcc-v若不存在gcc,则使用命令安装gcc:yuminstallgcc-c++2、下载redis源文件mkdir/usr/local/rediscd/usr/local/rediswgethttp://download.redis.io/relea......
  • redis 浅谈3
    1redis数据结构简介sds链表字典跳跃表整数集合 压缩列表 2过期时间redis每个库都会保存一个结构,里面包含了每个键的过期时间的字典结构;redis 如何判断过期,首先检查给的键是否在过期字典中,如果在,那就获取过期时间,在检查当前Unix时间戳是否大于键的过期时间 3......
  • Redis持久化之RDB和AOF
    Redis是基于内存的,内存中的信息断电丢失,有时需要持久化来解决这个弊端。在之前的文章中Shiro中使用Redis管理session-东方来客-博客园(cnblogs.com)使用了Redis管理Shiro的session。想要配置Redis持久化不是在Maven项目中,而是要通过redis.conf配置来影响Redis,这里通过Doc......
  • 玩转Redis:哨兵模式揭秘,带你骑上“哨兵战车”
    摘要:大家好!今天我们要聊一聊Redis的哨兵模式。说到Redis,相信很多人都对它的高性能、高可靠性留下了深刻的印象。而在这众多强大的功能中,哨兵模式无疑是一个备受关注的话题!哨兵模式在Redis中的作用就像是一支战车部队,能够时刻监控并保护我们的Redis集群。当集群中的某个主节点发生......
  • Redis分布式锁的Java实现之道
    摘要:在当今的微服务架构中,分布式锁是一个非常重要的概念。它允许我们在多个服务之间同步操作,确保数据的一致性和完整性。而Redis作为一种高性能的内存数据存储系统,常常被用来实现分布式锁。一、分布式锁的基本概念在分布式系统中,多个节点可能同时访问和修改共享资源。如果没有适......
  • Redis哨兵模式:什么是哨兵模式、哨兵模式的优缺点、哨兵模式的主观下线和客观下线、投
    什么是哨兵模式哨兵模式是Redis的高可用解决方案之一,它旨在提供自动故障转移和故障检测的功能。在传统的Redis部署中,单个Redis节点可能成为单点故障,一旦该节点宕机,整个系统将不可用。为了解决这个问题,哨兵模式引入了多个Redis节点,其中一个节点被选为主节点,其他节点作为从节点。......