【Guava】使用Guava的RateLimiter作限流

1、常见的限流算法

目前经常使用的限流算法有两个:漏桶算法和令牌桶算法。java

1.漏桶算法

漏桶算法的原理比较简单,请求进入到漏桶中,漏桶以必定的速率漏水。当请求过多时,水直接溢出。能够看出,漏桶算法能够强制限制数据的传输速度。算法

2.令牌桶算法

令牌桶算法的原理是系统以必定速率向桶中放入令牌,若是有请求时,请求会从桶中取出令牌,若是能取到令牌,则能够继续完成请求,不然等待或者拒绝服务。这种算法能够应对突发程序的请求,所以比漏桶算法好。缓存

在Wikipedia上,令牌桶算法是这么描述的:并发

  • 每秒会有r个令牌放入桶中,或者说,每过1/r 秒桶中增长一个令牌
  • 桶中最多存放b个令牌,若是桶满了,新放入的令牌会被丢弃
  • 当一个n字节的数据包到达时,消耗n个令牌,而后发送该数据包
  • 若是桶中可用令牌小于n,则该数据包将被缓存或丢弃

2、RateLimiter

Guava中开源出来一个令牌桶算法的工具类RateLimiter,能够轻松实现限流的工做。RateLimiter对简单的令牌桶算法作了一些工程上的优化,具体的实现是SmoothBursty。须要注意的是,RateLimiter的另外一个实现SmoothWarmingUp,就不是令牌桶了,而是漏桶算法。也许是出于简单起见,RateLimiter中的时间窗口能且仅能为1S,若是想搞其余时间单位的限流,只能另外造轮子。app

RateLimiter有一个有趣的特性是[前人挖坑后人跳],也就是说RateLimiter容许某次请求拿走了超出剩余令牌数的令牌,可是下一次请求将为此付出代价,一直等到令牌亏空补上,而且桶中有足够本次请求使用的令牌为止。这里面就涉及到一个权衡,是让前一次请求干等到令牌够用才走掉呢,仍是让它走掉后面的请求等一等呢?Guava的设计者选择的是后者,先把眼前的活干了,后面的过后面再说。工具

测试代码:测试

public class RateLimiterMain {
    public static void main(String[] args) {
        RateLimiter rateLimiter = RateLimiter.create(2);
        System.out.println(rateLimiter.acquire(5));
        System.out.println(rateLimiter.acquire(2));
        System.out.println(rateLimiter.acquire(1));
    }
}

输出内容:优化

0.0
2.496889
0.992149

能够看出,令牌桶每秒只能产生2个令牌,咱们能够第一次取出5个,可是第二次再去取令牌的时候,须要等2.5s,也就是第一次令牌取完后,须要等2.5s才能取到令牌。一样的,第三次取1个令牌的时候,也须要等待第二次的1s的时间。也就是,取的速率能够超过令牌产生的速率,可是下一次再次去取的时候,须要阻塞等待。ui

固然也可使用tryAcquire来非阻塞的获取,能够实时返回结果。另外tryAcquire也能够传入参数,也就是等待的时间,超时直接返回false。这点等同于常见的lock,tryLock。google

3、并发控制Semapphore

通常来讲,在网关系统中,还有一个参数叫并发控制,就是某一个资源能够被同时访问的个数。这种状况下,咱们可使用Semaphore来控制。

Semaphore不一样于互斥锁。互斥锁是某个资源只能支持同时一个访问,而Semaphore能够支持多个访问,可是加上了总数的控制。

4、实战

4.1 在pom中加入guava依赖

<dependency>
	<groupId>com.google.guava</groupId>
	<artifactId>guava</artifactId>
	<version>18.0</version>
</dependency>

把限流服务封装到一个类中AccessLimitService,提供tryAcquire()方法,用来尝试获取令牌,返回true表示获取到,以下所示:

@Service
public class AccessLimitService {

    //每秒只发出5个令牌
    RateLimiter rateLimiter = RateLimiter.create(5.0);

    /**
     * 尝试获取令牌
     * @return
     */
    public boolean tryAcquire(){
        return rateLimiter.tryAcquire();
    }
}

调用方是个普通的controller,每次收到请求的时候都尝试去获取令牌,获取成功和失败打印不一样的信息,以下:

@Controller
public class HelloController {

    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    @Autowired
    private AccessLimitService accessLimitService;

    @RequestMapping("/access")
    @ResponseBody
    public String access(){
        //尝试获取令牌
        if(accessLimitService.tryAcquire()){
            //模拟业务执行500毫秒
            try {
                Thread.sleep(500);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            return "aceess success [" + sdf.format(new Date()) + "]";
        }else{
            return "aceess limit [" + sdf.format(new Date()) + "]";
        }
    }
}
相关文章
相关标签/搜索