📫作者简介:小明java问道之路,专注于研究 Java/ Liunx内核/ C++及汇编/计算机底层原理/源码,就职于大型金融公司后端高级工程师,擅长交易领域的高安全/可用/并发/性能的架构设计与演进、系统优化与稳定性建设。
📫 热衷分享,喜欢原创~ 关注我会给你带来一些不一样的认知和成长。
🏆 CSDN博客专家/后端领域优质创作者/内容合伙人、InfoQ签约作者、阿里云专家/签约博主、51CTO专家 🏆
🔥如果此文还不错的话,还请👍关注、点赞、收藏三连支持👍一下博主~
专栏系列(点击解锁)
学习路线(点击解锁)
知识定位
🔥MySQL从入门到精通🔥
MySQL从入门到精通
全面讲解MySQL知识与实战
🔥计算机底层原理🔥
深入理解计算机系统CSAPP
构件计算机体系和计算机思维
Linux内核源码解析
围绕Linux内核讲解计算机底层原理与并发
🔥数据结构与企业题库精讲🔥
数据结构与企业题库精讲
结合工作经验深入浅出,适合各层次,笔试面试算法题精讲
🔥互联网架构分析与实战🔥
企业系统架构分析实践与落地
行业前沿视角,专注于技术架构升级路线、架构实践
互联网企业防资损实践
金融公司的防资损方法论、代码与实践。
本文目录
本文目录
本文导读
一、什么是分布式限流
二、基于Redis的setnx操作
三、基于Redis的数据结构zset
三、Redis + Lua脚本实现限流
四、基于Redis的List数据结构实现令牌桶算法
总结
本文导读
本文介绍分布式系统和分布式限流,我们现在的生产中的限流包括网关层的限流与Redis实现的限流策略,主要有基于Redis的 setnx 操作、List、zset实现的滑动窗口,以及Redis的Lua脚本实现分布式限流。
一、什么是分布式限流
分布式系统必须是由多个节点组成的系统,其中节点是指计算机服务器,这些节点一般不是孤立的,而是相互连接的,这些连接的节点部署我们的节点,它们的操作将得到协调。
不同的业务模块部署在不同的服务器上,或者同一业务模块拆分多个子业务并将其部署在不同服务器上,以解决高并发性问题,并提供可扩展性和高可用性。
分布式限流的原理很简单,一言以蔽之,分布式与单机流量限制场景不同,它将整个分布式环境中的所有服务器视为一个整体。
例如对于IP流限制,假设,我们限制一个IP每秒最多1000次访问。
无论来自该IP的请求落在哪台计算机上,只要它访问集群中的服务节点,它都将受到流限制规则的约束。
从上面的示例中很容易看出,我们必须将流量限制信息保存在“集中式”组件上,这样它才能获得集群中所有机器的访问状态。
目前主流的限流方案有两种:
1、网关层(Nginx、Openresty、Spring Cloud Gateway等)流量限制将流量限制规则应用于所有交通入口
(在Nginx中 ngx_http_limit_req_module 模块限制访问频率,nginx.conf配置文件中可以使用limit_req_zone命令及limit_req命令限制请求速率;限制请求速率)。
2、中间件限流,将流限制信息存储在分布式环境中的中间件中(如Redis),每个组件可以从中获取当前时间的流量统计信息,从而决定是拒绝服务还是释放流量。
二、基于Redis的setnx操作
当我们使用Redis分布式锁时,依赖于setnx指令。
流量限制的主要目的是在单位时间内只允许N个请求访问我的代码程序。因此,依靠setnx可以轻松实现此功能。
例如,如果我们需要在1秒内限制1000个请求,我们可以在设置setnx时将过期时间设置为1000。当setnx请求数达到20个时,就达到了流量限制效果,代码相对简单。
当然,这种方法有很多缺点。例如,当我们计数1-10秒时,我们不能在2-11秒内计数。如果我们需要在N秒内计数M个请求,我们需要在Redis中保留N个密钥。
三、基于Redis的数据结构zset
限制中最重要算法之一就是的是滑动窗口,可以解决上述,提到了1-10如何变成2-11的缺陷。zset起始值和结束值都+1即可。
我们可以将请求放入 zset 数组中,当请求进入时,该 value 保持唯一。它可以实现滑动窗口的效果,并且可以保证每N秒最多M个请求。缺点是zset的数据结构会越来越大。实现方法相对简单。
原理是获取一段时间的 size 值,如果这个 size 大于我们设定的一个值,例如1000,则return失败,小于则把这个请求放入 zset 中。
// intervalTime是限流的时间
Integer count = redisTemplate.opsForZSet()
.rangeByScore("limit", currentTime - intervalTime, currentTime).size();
// 把这个请求放入 zset 中
redisTemplate.opsForZSet()
.add("limit",UUID.randomUUID().toString(), currentTime);
三、Redis + Lua脚本实现限流
分布式流量限制的关键是使流量限制服务全局化。Redis+Lua脚本可用于实现高并发和高性能的流量限制。
Lua是一种轻量级、紧凑的脚本语言,用标准C语言编写,嵌入在应用程序,为应用程序提供灵活的扩展和定制功能。
-- 获取调用脚本时传入的第一个key值(用作限流的 key)
local key = KEYS[1]
-- 获取调用脚本时传入的第一个参数值(限流大小)
local limit = tonumber(ARGV[1])
-- 获取当前流量大小
-- (redis.call方法,从缓存中get和key相关的值,如果为null那么就返回0)
local curentLimit = tonumber(redis.call('get', key) or "0")
-- 判断缓存中记录的数值是否会大于限制大小,如果超出表示该被限流,返回0
-- 如果未超过,那么该key的缓存值+1,并设置过期时间,并返回缓存值+1
-- 是否超出限流(如果超出限流大小,直接返回)
if curentLimit + 1 > limit then
return 0
else
redis.call("INCRBY", key, 1) -- 没有超出 value + 1(请求数+1)
redis.call("EXPIRE", key, 2) -- 设置过期时间(设置2秒过期)
return 1 -- 放行请求
end
好处:减少网络开销,使用Lua脚本,不需要向Redis发送多个请求,只需执行一次,并减少网络传输;Redis将整个Lua脚本作为命令执行是原子操作,无需担心并发问题;一旦Lua脚本被执行,它将被永久保存在Redis中,并且可以被其他客户端重用。
四、基于Redis的List数据结构实现令牌桶算法
令牌桶算法指的是输入速率和输出速率。当输出速率大于输入速率时,超过流量限制。也就是说,我们每次访问请求时都可以从Redis获取令牌。如果我们得到一个令牌,这意味着没有超过限制。如果我们不能获得令牌,结果是相反的。
也就是说令牌桶算法的原理是系统将以恒定的速度将令牌放入桶中,需要则获取令牌。当存储桶中没有可用令牌时,则将被拒绝。
基于以上思想,Redis的List、zset等数据结构都可以实现。
// 获取令牌,返回 null 代表当前令牌桶中无令牌,则拒绝请求
Object result = redisTemplate.opsForList().leftPop("limit_list");
// 在令牌桶中添加令牌(令牌需要唯一)
redisTemplate.opsForList()
.rightPush("limit_list",UUID.randomUUID().toString());
总结
本文介绍分布式系统和分布式限流,我们现在的生产中的限流包括网关层的限流与Redis实现的限流策略,主要有基于Redis的 setnx 操作、List、zset实现的滑动窗口,以及Redis的Lua脚本实现分布式限流。