基于Redis和Lua的分布式限流(基于redis分布式session的实现)
mhr18 2024-11-05 10:25 27 浏览 0 评论
Java单机限流可以使用AtomicInteger,RateLimiter或Semaphore来实现,但是上述方案都不支持集群限流。集群限流的应用场景有两个,一个是网关,常用的方案有Nginx限流和Spring Cloud Gateway,另一个场景是与外部或者下游服务接口的交互,因为接口限制必须进行限流。
本文的主要内容为:
- Redis和Lua的使用场景和注意事项,比如说KEY映射的问题。
- Spring Cloud Gateway中限流的实现。
集群限流的难点
在上篇Guava RateLimiter的分析文章中,我们学习了令牌桶限流算法的原理,下面我们就探讨一下,如果将RateLimiter扩展,让它支持集群限流,会遇到哪些问题。
RateLimiter会维护两个关键的参数nextFreeTicketMicros和storedPermits,它们分别是下一次填充时间和当前存储的令牌数。当RateLimiter的acquire函数被调用时,也就是有线程希望获取令牌时,RateLimiter会对比当前时间和nextFreeTicketMicros,根据二者差距,刷新storedPermits,然后再判断更新后的storedPermits是否足够,足够则直接返回,否则需要等待直到令牌足够(Guava RateLimiter的实现比较特殊,并不是当前获取令牌的线程等待,而是下一个获取令牌的线程等待)。
由于要支持集群限流,所以nextFreeTicketMicros和storedPermits这两个参数不能只存在JVM的内存中,必须有一个集中式存储的地方。而且,由于算法要先获取两个参数的值,计算后在更新两个数值,这里涉及到竞态限制,必须要处理并发问题。
集群限流由于会面对相比单机更大的流量冲击,所以一般不会进行线程等待,而是直接进行丢弃,因为如果让拿不到令牌的线程进行睡眠,会导致大量的线程堆积,线程持有的资源也不会释放,反而容易拖垮服务器。
Redis和Lua
分布式限流本质上是一个集群并发问题,Redis单进程单线程的特性,天然可以解决分布式集群的并发问题。所以很多分布式限流都基于Redis,比如说Spring Cloud的网关组件Gateway。
Redis执行Lua脚本会以原子性方式进行,单线程的方式执行脚本,在执行脚本时不会再执行其他脚本或命令。并且,Redis只要开始执行Lua脚本,就会一直执行完该脚本再进行其他操作,所以Lua脚本中不能进行耗时操作。使用Lua脚本,还可以减少与Redis的交互,减少网络请求的次数。
Redis中使用Lua脚本的场景有很多,比如说分布式锁,限流,秒杀等,总结起来,下面两种情况下可以使用Lua脚本:
- 使用 Lua 脚本实现原子性操作的CAS,避免不同客户端先读Redis数据,经过计算后再写数据造成的并发问题。
- 前后多次请求的结果有依赖时,使用 Lua 脚本将多个请求整合为一个请求。
但是使用Lua脚本也有一些注意事项:
- 要保证安全性,在 Lua 脚本中不要定义自己的全局变量,以免污染 Redis内嵌的Lua环境。因为Lua脚本中你会使用一些预制的全局变量,比如说redis.call()
- 要注意 Lua 脚本的时间复杂度,Redis 的单线程同样会阻塞在 Lua 脚本的执行中。
- 使用 Lua 脚本实现原子操作时,要注意如果 Lua 脚本报错,之前的命令无法回滚,这和Redis所谓的事务机制是相同的。
- 一次发出多个 Redis 请求,但请求前后无依赖时,使用 pipeline,比 Lua 脚本方便。
- Redis要求单个Lua脚本操作的key必须在同一个Redis节点上。解决方案可以看下文对Gateway原理的解析。
性能测试
Redis虽然以单进程单线程模型进行操作,但是它的性能却十分优秀。总结来说,主要是因为:
- 绝大部分请求是纯粹的内存操作
- 采用单线程,避免了不必要的上下文切换和竞争条件
- 内部实现采用非阻塞IO和epoll,基于epoll自己实现的简单的事件框架。epoll中的读、写、关闭、连接都转化成了事件,然后利用epoll的多路复用特性,绝不在io上浪费一点时间。
所以,在集群限流时使用Redis和Lua的组合并不会引入过多的性能损耗。我们下面就简单的测试一下,顺便熟悉一下涉及的Redis命令。
# test.lua脚本的内容 local test = redis.call("get", "test") local time = redis.call("get", "time") redis.call("setex", "test", 10, "xx") redis.call("setex", "time", 10, "xx") return {test, time} # 将脚本导入redis,之后调用不需再传递脚本内容 redis-cli -a 082203 script load "$(cat test.lua)" "b978c97518ae7c1e30f246d920f8e3c321c76907" # 使用redis-benchmark和evalsha来执行lua脚本 redis-benchmark -a 082203 -n 1000000 evalsha b978c97518ae7c1e30f246d920f8e3c321c76907 0 ====== 1000000 requests completed in 20.00 seconds 50 parallel clients 3 bytes payload keep alive: 1 93.54% <= 1 milliseconds 99.90% <= 2 milliseconds 99.97% <= 3 milliseconds 99.98% <= 4 milliseconds 99.99% <= 5 milliseconds 100.00% <= 6 milliseconds 100.00% <= 7 milliseconds 100.00% <= 7 milliseconds 49997.50 requests per second
通过上述简单的测试,我们可以发现本机情况下,使用Redis执行Lua脚本的性能极其优秀,一百万次执行,99.99%在5毫秒以下。
本来想找一下官方的性能数据,但是针对Redis + Lua的性能数据较少,只找到了几篇个人博客,感兴趣的同学可以去探索。这篇文章有Lua和zadd的性能比较(具体数据请看原文,链接缺失的话,请看文末)。
以上lua脚本的性能大概是zadd的70%-80%,但是在可接受的范围内,在生产环境可以使用。负载大概是zadd的1.5-2倍,网络流量相差不大,IO是zadd的3倍,可能是开启了AOF,执行了三次操作。
Spring Cloud Gateway的限流实现
Gateway是微服务架构Spring Cloud的网关组件,它基于Redis和Lua实现了令牌桶算法的限流功能,下面我们就来看一下它的原理和细节吧。
Gateway基于Filter模式,提供了限流过滤器RequestRateLimiterGatewayFilterFactory。只需在其配置文件中进行配置,就可以使用。具体的配置感兴趣的同学自行学习,我们直接来看它的实现。
RequestRateLimiterGatewayFilterFactory依赖RedisRateLimiter的isAllowed函数来判断一个请求是否要被限流抛弃。
public Mono<Response> isAllowed(String routeId, String id) { //routeId是ip地址,id是使用KeyResolver获取的限流维度id,比如说基于uri,IP或者用户等等。 Config routeConfig = loadConfiguration(routeId); // 每秒能够通过的请求数 int replenishRate = routeConfig.getReplenishRate(); // 最大流量 int burstCapacity = routeConfig.getBurstCapacity(); try { // 组装Lua脚本的KEY List<String> keys = getKeys(id); // 组装Lua脚本需要的参数,1是指一次获取一个令牌 List<String> scriptArgs = Arrays.asList(replenishRate + "", burstCapacity + "", Instant.now().getEpochSecond() + "", "1"); // 调用Redis,tokens_left = redis.eval(SCRIPT, keys, args) Flux<List<Long>> flux = this.redisTemplate.execute(this.script, keys, scriptArgs); ..... // 省略 } static List<String> getKeys(String id) { String prefix = "request_rate_limiter.{" + id; String tokenKey = prefix + "}.tokens"; String timestampKey = prefix + "}.timestamp"; return Arrays.asList(tokenKey, timestampKey); }
需要注意的是getKeys函数的prefix包含了"{id}",这是为了解决Redis集群键值映射问题。Redis的KeySlot算法中,如果key包含{},就会使用第一个{}内部的字符串作为hash key,这样就可以保证拥有同样{}内部字符串的key就会拥有相同slot。Redis要求单个Lua脚本操作的key必须在同一个节点上,但是Cluster会将数据自动分布到不同的节点,使用这种方法就解决了上述的问题。
然后我们来看一下Lua脚本的实现,该脚本就在Gateway项目的resource文件夹下。它就是如同Guava的RateLimiter一样,实现了令牌桶算法,只不过不在需要进行线程休眠,而是直接返回是否能够获取。
local tokens_key = KEYS[1] -- request_rate_limiter.${id}.tokens 令牌桶剩余令牌数的KEY值 local timestamp_key = KEYS[2] -- 令牌桶最后填充令牌时间的KEY值 local rate = tonumber(ARGV[1]) -- replenishRate 令令牌桶填充平均速率 local capacity = tonumber(ARGV[2]) -- burstCapacity 令牌桶上限 local now = tonumber(ARGV[3]) -- 得到从 1970-01-01 00:00:00 开始的秒数 local requested = tonumber(ARGV[4]) -- 消耗令牌数量,默认 1 local fill_time = capacity/rate -- 计算令牌桶填充满令牌需要多久时间 local ttl = math.floor(fill_time*2) -- *2 保证时间充足 local last_tokens = tonumber(redis.call("get", tokens_key)) -- 获得令牌桶剩余令牌数 if last_tokens == nil then -- 第一次时,没有数值,所以桶时满的 last_tokens = capacity end local last_refreshed = tonumber(redis.call("get", timestamp_key)) -- 令牌桶最后填充令牌时间 if last_refreshed == nil then last_refreshed = 0 end local delta = math.max(0, now-last_refreshed) -- 获取距离上一次刷新的时间间隔 local filled_tokens = math.min(capacity, last_tokens+(delta*rate)) -- 填充令牌,计算新的令牌桶剩余令牌数 填充不超过令牌桶令牌上限。 local allowed = filled_tokens >= requested local new_tokens = filled_tokens local allowed_num = 0 if allowed then -- 若成功,令牌桶剩余令牌数(new_tokens) 减消耗令牌数( requested ),并设置获取成功( allowed_num = 1 ) 。 new_tokens = filled_tokens - requested allowed_num = 1 end -- 设置令牌桶剩余令牌数( new_tokens ) ,令牌桶最后填充令牌时间(now) ttl是超时时间? redis.call("setex", tokens_key, ttl, new_tokens) redis.call("setex", timestamp_key, ttl, now) -- 返回数组结果 return { allowed_num, new_tokens }
后记
Redis的主从异步复制机制可能丢失数据,出现限流流量计算不准确的情况,当然限流毕竟不同于分布式锁这种场景,对于结果的精确性要求不是很高,即使多流入一些流量,也不会影响太大。
?正如Martin在他质疑Redis分布式锁RedLock文章中说的,Redis的数据丢弃了也无所谓时再使用Redis存储数据。
I think it’s a good fit in situations where you want to share some transient, approximate, fast-changing data between servers, and where it’s not a big deal if you occasionally lose that data for whatever reason
接下来我们回来学习阿里开源的分布式限流组件sentinel,希望大家持续关注。
相关推荐
- 【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库
-
如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...
- Pure Storage推出统一数据管理云平台及新闪存阵列
-
PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...
- 对Java学习的10条建议(对java课程的建议)
-
不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...
- SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!
-
官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...
- JDK21有没有什么稳定、简单又强势的特性?
-
佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...
- 「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了
-
在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...
- Java面试题及答案最全总结(2025版)
-
大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...
- 数据库日常运维工作内容(数据库日常运维 工作内容)
-
#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...
- 分布式之系统底层原理(上)(底层分布式技术)
-
作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...
- oracle 死锁了怎么办?kill 进程 直接上干货
-
1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...
- SpringBoot 各种分页查询方式详解(全网最全)
-
一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...
- 《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略
-
《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...
- LoadRunner(loadrunner录制不到脚本)
-
一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...
- Redis数据类型介绍(redis 数据类型)
-
介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...
- RMAN备份监控及优化总结(rman备份原理)
-
今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (63)
- oracle批量插入数据 (62)
- oracle事务隔离级别 (53)
- oracle 空为0 (50)
- oracle主从同步 (55)
- oracle 乐观锁 (51)
- redis 命令 (78)
- php redis (88)
- redis 存储 (66)
- redis 锁 (69)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)