由于互联网公司的流量巨大,系统上线会做一个流量峰值的评估,尤其是像各种秒杀促销活动,为了保证系统不被巨大的流量压垮,会在系统流量到达一定阈值时,拒绝掉一部分流量。
限流会导致用户在短时间内(这个时间段是毫秒级的)系统不可用,一般我们衡量系统处理能力的指标是每秒的QPS
或者TPS
,假设系统每秒的流量阈值是1000,理论上一秒内有第1001个请求进来时,那么这个请求就会被限流。
限流方案
计数器
Java内部可以通过原子类计数器AtomicInteger
、Semaphore
信号量来做简单的限流。
1 | // 限流的个数 |
漏桶算法
漏桶算法思路很简单,我们把水比做请求,漏桶比做是系统处理能力极限,水先进入到漏桶里,漏桶里的水按一定速率流出,当流出速率小于流入的速率时,由于漏桶容量有限,后续进入的水直接溢出(拒绝请求),以此实现限流。
令牌桶算法
令牌桶算法的原理也比较简单,可以理解成医院的挂号看病,只要拿到号以后才可以进行诊病。
系统会维护 一个令牌(token)桶,以一个恒定的速度往桶里放入令牌(token),这时如果有请求进来想要被处理,则需要先从桶里获取一个令牌(token), 当桶里没有令牌(token)可取时,则该请求将被拒绝服务。令牌桶算法通过控制桶的容量,发放令牌的速率,来达到对请求的限制。
Redis + Lua
个人理解,Lua
脚本和 MySQL
数据库的存储过程比较相似,他们执行一组命令,所有命令的执行要么全部成功或者失败,以此达到原子性。也可以把Lua
脚本理解为,一段具有业务逻辑的代码块。
而Lua
本身就是一种编程语言,虽然redis
官方没有直接提供限流相应的API
,但却支持了 Lua
脚本的功能,可以使用它实现复杂的令牌桶或漏桶算法,也是分布式系统中实现限流的主要方式之一。
相比Redis
事务,Lua脚本
的优点:
- 减少网络开销:使用
Lua
脚本,无需向Redis
发送多次请求,执行一次即可,减少网络传输 - 原子操作:
Redis
将整个Lua
脚本作为一个命令执行,原子,无需担心并发 - 复用:
Lua
脚本一旦执行,会永久保存Redis
中,,其他客户端可复用
Lua
脚本大致逻辑如下:
1 | -- 获取调用脚本时传入的第一个key值(用作限流的 key) |
- 通过
KEYS[1]
获取传入的key参数 - 通过
ARGV[1]
获取传入的limit
参数 redis.call
方法,从缓存中get
和key
相关的值,如果为null
那么就返回0- 接着判断缓存中记录的数值是否会大于限制大小,如果超出表示该被限流,返回0
- 如果未超过,那么该key的缓存值+1,并设置过期时间为1秒钟以后,并返回缓存值+1
这种方式是本文推荐的方案,具体实现会在后边做细说。
网关层限流
限流常在网关这一层做,比如Nginx
、Openresty
、kong
、zuul
、Spring Cloud Gateway
等,而像spring cloud - gateway
网关限流底层实现原理,就是基于Redis + Lua
,通过内置Lua
限流脚本的方式。