项目需要使用限流措施,查阅后主要使用令牌桶算法实现java 令牌桶算法,为了更灵活的实现限流,就自己实现了一个简单的基于令牌桶算法的限流实现。
令牌桶算法描述
令牌桶这种控制机制基于令牌桶中是否存在令牌来指示什么时候可以发送流量。令牌桶中的每一个令牌都代表一个字节。如果令牌桶中存在令牌,则允许发送流量;而如果令牌桶中不存在令牌,则不允许发送流量。因此,如果突发门限被合理地配置并且令牌桶中有足够的令牌,那么流量就可以以峰值速率发送。
令牌桶算法图描述
简单的说就是,一边请求时会消耗桶内的令牌,另一边会以固定速率往桶内放令牌。当消耗的请求大于放入的速率时,进行相应的措施,比如等待,或者拒绝等。
Java的简单实现
为了更灵活的定制限流措施,自己实现了限流的部分代码,如下:
/**
* @
* @since 18/1/3 上午9:45.
* 限流器
*/
class {
int token;
final int ;
= null;
final long ;
final lock = new ();
{
try {
// 应用开发中使用对象必须通过反射获取
Class> clazz = .class;
Field f = clazz.("");
f.(true);
= () f.get(clazz);
= .(.class.("token"));
} catch ( ex) {throw new Error(ex);}
}
(int token){
this. = token;
this.token = token;
}
/**
* 获取一个令牌
*/
(){
int = token;
if(){
if((, -1)){
true;
}
= token;
if(){
// 在有效期内等待通知
(lock){
try {
lock.wait();
} catch ( e) {
.().();
}
}
= token;
if( {
(lock){
lock.();
}
});
}
}
核心代码在方法部分主要思路就是,优先采用CAS进行自旋操作获取令牌,一直尝试令牌消耗光,那么会进入等待,在定时线程调用方法时,会唤醒所有等待线程,此时进入CAS进行尝试消耗令牌,以此循环一直到设置的最大等待时间(代码中的)消耗光,如果还没获得令牌,那么会返回false
这段代码如果自己起一个线程进行限流,然后自己开个定时线程进行补充也可以,但是实际运用中往往需要一个限流管理器来分配限流器,然后通过限流管理器统一的进行定时触发,这样可以不用开很多的定时线程,同时通过线程池也避免了在定时线程竞争锁时引发的过长等待造成定时线程不准的情况。
下面贴出限流管理器部分代码
/**
* @
* @since 18/1/3 上午9:43.
* 限流管理器
*/
@
class {
// 定时线程
final = new (2);
// 执行补充线程池
final = new (5, 200,
60L, ., new (),
new ("",true,false));
// 限流器容器
Map = new ();
@
void init(){
.(new r(), 1, 1, .);
}
@
void (){
.();
}
/**
* 通过key获取相应的限流器
*/
void ( key,int ){
= .get(key);
// 双检锁确保安全创建
if(==null){
(this){
// init
= (key, k -> new ());
}
}
// 尝试获取令牌
if(!.()){
// 获取失败,根据实际情况进行处理java 令牌桶算法,这里直接抛异常了
.(.ITER);
}
}
/**
* 补充相应的令牌数
*/
class r {
@
void run(){
.().( -> .());
}
}
}
代码中主要是创建了定时线程,补充令牌线程池。
部分代码不是开源的,需要调整下,不影响主流程,代码使用了一些的注解。
其中的不足
在集群的环境下,没有考虑分布式的情况,也就是如果一个应用部署的限流是1s产生10个令牌,假设部署了5个应用,那么实际1s可以产生50个令牌。如果需要考虑这部分,那么在CAS操作可以替换为通过redis的setnx来进行获取锁操作然后更新redis存储对应的令牌,补充则直接设置更新redis对应的令牌数即可,这样效率肯定比现在基于CAS操作低。
总结
实际上实现这个限流器更多的考虑是可以自行定义等待的最大时间java 令牌桶算法-java令牌_基于令牌桶算法的Java限流实现,超时措施,定时补充令牌时间间隔等,同时也温习了一下之前的并发知识。
文章来源:https://blog.csdn.net/weixin_29948389/article/details/114033055