5种限流算法,7种限流方式,挡住突发流量?(三)

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 5种限流算法,7种限流方式,挡住突发流量?

7. Redis 分布式限流

Redis 是一个开源的内存数据库,可以用来作为数据库、缓存、消息中间件等。Redis 是单线程的,又在内存中操作,所以速度极快,得益于 Redis 的各种特性,所以使用 Redis 实现一个限流工具是十分方便的。

下面的演示都基于Spring Boot 项目,并需要以下依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置 Redis 信息。

spring:
  redis:
    database: 0
    password: 
    port: 6379
    host: 127.0.0.1
    lettuce:
      shutdown-timeout: 100ms
      pool:
        min-idle: 5
        max-idle: 10
        max-active: 8
        max-wait: 1ms

7.1. 固定窗口限流

Redis 中的固定窗口限流是使用 incr 命令实现的,incr 命令通常用来自增计数;如果我们使用时间戳信息作为 key,自然就可以统计每秒的请求量了,以此达到限流目的。

这里有两点要注意。

  1. 对于不存在的 key,第一次新增时,value 始终为 1。
  2. INCR 和 EXPIRE 命令操作应该在一个原子操作中提交,以保证每个 key 都正确设置了过期时间,不然会有 key 值无法自动删除而导致的内存溢出。

由于 Redis 中实现事务的复杂性,所以这里直接只用 lua 脚本来实现原子操作。下面是 lua 脚本内容。

local count = redis.call("incr",KEYS[1])
if count == 1 then
  redis.call('expire',KEYS[1],ARGV[2])
end
if count > tonumber(ARGV[1]) then
  return 0
end
return 1

下面是使用 Spring Boot 中 RedisTemplate 来实现的 lua 脚本调用测试代码。

/**
 * @author https://www.wdbyte.com
 */
@SpringBootTest
class RedisLuaLimiterByIncr {
    private static String KEY_PREFIX = "limiter_";
    private static String QPS = "4";
    private static String EXPIRE_TIME = "1";
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Test
    public void redisLuaLimiterTests() throws InterruptedException, IOException {
        for (int i = 0; i < 15; i++) {
            Thread.sleep(200);
            System.out.println(LocalTime.now() + " " + acquire("user1"));
        }
    }
    /**
     * 计数器限流
     *
     * @param key
     * @return
     */
    public boolean acquire(String key) {
        // 当前秒数作为 key
        key = KEY_PREFIX + key + System.currentTimeMillis() / 1000;
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(Long.class);
        //lua文件存放在resources目录下
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("limiter.lua")));
        return stringRedisTemplate.execute(redisScript, Arrays.asList(key), QPS, EXPIRE_TIME) == 1;
    }
}

代码中虽然限制了 QPS 为 4,但是因为这种限流实现是把毫秒时间戳作为 key 的,所以会有临界窗口突变的问题,下面是运行结果,可以看到因为时间窗口的变化,导致了 QPS 超过了限制值 4。

17:38:23.122044 true
17:38:23.695124 true
17:38:23.903220 true
# 此处有时间窗口变化,所以下面继续 true
17:38:24.106206 true
17:38:24.313458 true
17:38:24.519431 true
17:38:24.724446 true
17:38:24.932387 false
17:38:25.137912 true
17:38:25.355595 true
17:38:25.558219 true
17:38:25.765801 true
17:38:25.969426 false
17:38:26.176220 true
17:38:26.381918 true

7.3. 滑动窗口限流

通过对上面的基于 incr 命令实现的 Redis 限流方式的测试,我们已经发现了固定窗口限流所带来的问题,在这篇文章的第三部分已经介绍了滑动窗口限流的优势,它可以大幅度降低因为窗口临界突变带来的问题,那么如何使用 Redis 来实现滑动窗口限流呢?

这里主要使用 ZSET 有序集合来实现滑动窗口限流,ZSET 集合有下面几个特点:

  1. ZSET 集合中的  key 值可以自动排序。
  2. ZSET 集合中的 value 不能有重复值。
  3. ZSET 集合可以方便的使用 ZCARD 命令获取元素个数。
  4. ZSET 集合可以方便的使用 ZREMRANGEBYLEX 命令移除指定范围的 key 值。

基于上面的四点特性,可以编写出基于 ZSET 的滑动窗口限流 lua 脚本。

--KEYS[1]: 限流 key
--ARGV[1]: 时间戳 - 时间窗口
--ARGV[2]: 当前时间戳(作为score)
--ARGV[3]: 阈值
--ARGV[4]: score 对应的唯一value
-- 1. 移除时间窗口之前的数据
redis.call('zremrangeByScore', KEYS[1], 0, ARGV[1])
-- 2. 统计当前元素数量
local res = redis.call('zcard', KEYS[1])
-- 3. 是否超过阈值
if (res == nil) or (res < tonumber(ARGV[3])) then
    redis.call('zadd', KEYS[1], ARGV[2], ARGV[4])
    return 1
else
    return 0
end

下面是使用 Spring Boot 中 RedisTemplate 来实现的 lua 脚本调用测试代码。

@SpringBootTest
class RedisLuaLimiterByZset {
    private String KEY_PREFIX = "limiter_";
    private String QPS = "4";
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Test
    public void redisLuaLimiterTests() throws InterruptedException, IOException {
        for (int i = 0; i < 15; i++) {
            Thread.sleep(200);
            System.out.println(LocalTime.now() + " " + acquire("user1"));
        }
    }
    /**
     * 计数器限流
     *
     * @param key
     * @return
     */
    public boolean acquire(String key) {
        long now = System.currentTimeMillis();
        key = KEY_PREFIX + key;
        String oldest = String.valueOf(now - 1_000);
        String score = String.valueOf(now);
        String scoreValue = score;
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(Long.class);
        //lua文件存放在resources目录下
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("limiter2.lua")));
        return stringRedisTemplate.execute(redisScript, Arrays.asList(key), oldest, score, QPS, scoreValue) == 1;
    }
}

代码中限制 QPS 为 4,运行结果信息与之一致。

17:36:37.150370 true
17:36:37.716341 true
17:36:37.922577 true
17:36:38.127497 true
17:36:38.335879 true
17:36:38.539225 false
17:36:38.745903 true
17:36:38.952491 true
17:36:39.159497 true
17:36:39.365239 true
17:36:39.570572 false
17:36:39.776635 true
17:36:39.982022 true
17:36:40.185614 true
17:36:40.389469 true

这里介绍了 Redis 实现限流的两种方式,当然使用 Redis 也可以实现漏桶和令牌桶两种限流算法,这里就不做演示了,感兴趣的可以自己研究下。

8. 总结

这篇文章介绍实现限流的几种方式,主要是窗口算法和桶算法,两者各有优势。

  • 窗口算法实现简单,逻辑清晰,可以很直观的得到当前的 QPS 情况,但是会有时间窗口的临界突变问题,而且不像桶一样有队列可以缓冲。
  • 桶算法虽然稍微复杂,不好统计 QPS 情况,但是桶算法也有优势所在。
  • 漏桶模式消费速率恒定,可以很好的保护自身系统,可以对流量进行整形,但是面对突发流量不能快速响应。
  • 令牌桶模式可以面对突发流量,但是启动时会有缓慢加速的过程,不过常见的开源工具中已经对此优化。

单机限流与分布式限流

上面演示的基于代码形式的窗口算法和桶算法限流都适用于单机限流,如果需要分布式限流可以结合注册中心、负载均衡计算每个服务的限流阈值,但这样会降低一定精度,如果对精度要求不是太高,可以使用。

而 Redis 的限流,由于 Redis 的单机性,本身就可以用于分布式限流。使用 Redis 可以实现各种可以用于限流算法,如果觉得麻烦也可以使用开源工具如 redisson,已经封装了基于 Redis 的限流。

其他限流工具

文中已经提到了 Guava 的限流工具包,不过它毕竟是单机的,开源社区中也有很多分布式限流工具,如阿里开源的 Sentinel 就是不错的工具,Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

一如既往,文章中的代码存放在:github.com/niumoo/JavaNotes

参考

Redis INCR:https://redis.io/commands/incr

Rate Limiting Wikipedia:https://en.wikipedia.org/wiki/Rate_limiting

SpringBoot Redis:https://www.cnblogs.com/lenve/p/10965667.html

相关实践学习
基于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月前
|
人工智能 算法 前端开发
无界批发零售定义及无界AI算法,打破传统壁垒,累积数据流量
“无界批发与零售”是一种结合了批发与零售的商业模式,通过后端逻辑、数据库设计和前端用户界面实现。该模式支持用户注册、登录、商品管理、订单处理、批发与零售功能,并根据用户行为计算信用等级,确保交易安全与高效。
|
4月前
|
负载均衡 监控 算法
揭秘负载均衡的五大算法秘籍:让你的服务器轻松应对亿万流量,不再崩溃!
【8月更文挑战第31天】在互联网快速发展的今天,高可用性和可扩展性成为企业关注的重点。负载均衡作为关键技术,通过高效分配网络流量提升系统处理能力。本文介绍了轮询、加权轮询、最少连接及IP哈希等常见负载均衡算法及其应用场景,并提供Nginx配置示例。此外,还探讨了如何根据业务需求选择合适算法、配置服务器权重、实现高可用方案、监控性能及定期维护等最佳实践,助力系统优化与用户体验提升。
74 2
|
4月前
|
算法 NoSQL Java
spring cloud的限流算法有哪些?
【8月更文挑战第18天】spring cloud的限流算法有哪些?
88 3
|
5月前
|
存储 算法 Java
高并发架构设计三大利器:缓存、限流和降级问题之滑动日志算法问题如何解决
高并发架构设计三大利器:缓存、限流和降级问题之滑动日志算法问题如何解决
|
5月前
|
算法 Java 调度
高并发架构设计三大利器:缓存、限流和降级问题之使用Java代码实现令牌桶算法问题如何解决
高并发架构设计三大利器:缓存、限流和降级问题之使用Java代码实现令牌桶算法问题如何解决
|
5月前
|
缓存 算法 Java
高并发架构设计三大利器:缓存、限流和降级问题之使用代码实现漏桶算法问题如何解决
高并发架构设计三大利器:缓存、限流和降级问题之使用代码实现漏桶算法问题如何解决
|
5月前
|
算法 UED 缓存
高并发架构设计三大利器:缓存、限流和降级问题之滑动窗口算法适用于哪些场景
高并发架构设计三大利器:缓存、限流和降级问题之滑动窗口算法适用于哪些场景
|
5月前
|
存储 算法 缓存
高并发架构设计三大利器:缓存、限流和降级问题之滑动窗口算法的原理是什么
高并发架构设计三大利器:缓存、限流和降级问题之滑动窗口算法的原理是什么
|
2月前
|
算法 安全 数据安全/隐私保护
基于game-based算法的动态频谱访问matlab仿真
本算法展示了在认知无线电网络中,通过游戏理论优化动态频谱访问,提高频谱利用率和物理层安全性。程序运行效果包括负载因子、传输功率、信噪比对用户效用和保密率的影响分析。软件版本:Matlab 2022a。完整代码包含详细中文注释和操作视频。
|
11天前
|
算法 数据安全/隐私保护 索引
OFDM系统PAPR算法的MATLAB仿真,对比SLM,PTS以及CAF,对比不同傅里叶变换长度
本项目展示了在MATLAB 2022a环境下,通过选择映射(SLM)与相位截断星座图(PTS)技术有效降低OFDM系统中PAPR的算法实现。包括无水印的算法运行效果预览、核心程序及详尽的中文注释,附带操作步骤视频,适合研究与教学使用。
下一篇
无影云桌面