关于限流这种机制呢也算是老生常谈了, 毕竟在业务开发中实在是很多地方都会用到。比如第三方接口调用限制、并发访问数控制等…
而具体的限流算法在单机中很容易就可以实现, 在java的世界里既有开源库Guava的RateLimiter, 也有JUC中自带的 Semaphore、BlockingQueue等。拿来随手就可以使用。
那么在分布式场景中, 限流就变得不是那么容易了。 在这种环境上想实现限流本质上是在实现一种多个进程之间的协同工作机制。 必须得依靠一个可靠的协调中心才行,这一般都会选一种中间件来实现。
而 Redis 其实就是一个适合这种场景的中间件,既快,又有强大的数据结构对各种限流算法提供支持。那么我这里就来基于Redis实现一种简单粗暴又好用的限流方案—— 信号量(Semaphore)
关于信号量
这里引用一段维基百科的定义
信号量 (英语: semaphore )又称为 信号标 ,是一个同步对象,用于保持在0至指定最大值之间的一个计数值。当线程完成一次对该 semaphore 对象的等待( wait )时,该计数值减一;当线程完成一次对 semaphore 对象的释放( release )时,计数值加一。当计数值为0,则线程等待该 semaphore 对象不再能成功直至该 semaphore 对象变成 signaled 状态。 semaphore 对象的计数值大于0,为 signaled 状态;计数值等于0,为 nonsignaled 状态.
其实白话说起来很简单,信号量就是可以被 多个线程同时持有 的 一种同步对象,比如我设置一个值为5的计数信号量,那么现在有十个线程来获取他就只会有五个可以成功,剩下那五个则获取失败。
所以说如果有个计数信号量定义的值是1,那么他其实就等同于 mutex (互斥锁)
实现的基本思路
既然知道信号量本质是一种锁,那么对于信号量需要拥有的效果自然就有了思路
- 拥有获取、释放的机制
- 需要知道是哪个客户端获取到了信号量
- 获取到信号量之后, 不能因为客户端的崩溃导致无法释放
对于Redis来说,可以使用 ZSet 来实现这些效果。ZSet 是不可重复的有序集合, 内部每个元素都拥有一个属于自己的分数 (score)
那么我们可以将 ZSet 中一条数据视为客户端获取到的信号量, key就是客户端的唯一标识, score 可以设置为 客户端获取信号量的时间 。
这样就能够实现上面所说的几种机制
- 在ZSet中插入数据即为获取。 删除即为释放。
- 利用ZSet的有序特性, 可以根据 score 的排名 来判断是否成功获取到了信号量
- 因为 score 存的是客户端获取到信号量的时间, 所以可以约定一个过期时间来对死掉的客户端获取到的信号量进行清除
值得注意的是, 在 Redis 上实现信号量,如果客户端持有信号量之后由于处理时间太久导致没在规定的超时时间内释放的话, 那么这个持有 信号量延时机制 , 是需要自己实现的 (守护线程定时更新等) 因为Redis他本身没有提供这种功能的实现, 所以只能自己动手了。 不过像这种需求在使用到分布式信号量的场景中多数不怎么会出现, 所以也可以不管。
使用 Redis 构建分布式信号量的实现细节
知道了需求和对应的实现思路后, 那么可以来决定一下具体的实现细节
这里就需要对 Redis 的命令有一定的了解, 不过就算不了解也没关系, 反正就这么几个命令。
可行的具体流程
- 获取系统当前时间, 因为集合的分数储存的是时间毫秒值, 所以可以通过 ZREMRANGEBYSCORE 清理掉过期的信号量
- 使用 ZADD 向集合中添加代表自身信号量的元素, 分数为当前时间
- 通过 ZRANK 得到当前客户端在集合中的排名, 如果在许可证数量的范围内 (即不大于信号量最大持有数量) 即视为成功获取信号量
- 如果不在范围内, 比如信号量设置的最大许可数为 5, 自己在集合中的排名是5 (