使用令牌桶算法限流

限流

限流是对某一时间窗口内的请求数进行限制,保持系统的可用性和稳定性,防止因流量暴增而导致的系统运行缓慢或宕机。常用的限流算法有令牌桶和和漏桶,而Google开源项目Guava中的RateLimiter使用的就是令牌桶控制算法。
在开发高并发系统时有三把利器用来保护系统:

  • 缓存:缓存的目的是提升系统访问速度和增大系统处理容量
  • 降级:降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行
  • 限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理

限流算法

漏桶算法

漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率.示意图如下:
漏桶算法示意图
因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率.因此,漏桶算法对于存在突发特性的流量来说缺乏效率.

令牌桶算法

令牌桶算法原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。
示意图如下:
令牌桶算法示意图
令牌桶的好处是可以方便的改变速度. 一旦需要提高速率,则按需提高放入桶中的令牌的速率. 一般会定时(比如100毫秒)往桶中增加一定数量的令牌, 有些变种算法则实时的计算应该增加的令牌的数量.

令牌桶算法的使用

RateLimiter简介

Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法(Token Bucket)来完成限流,非常易于使用。RateLimiter经常用于限制对一些物理资源或者逻辑资源的访问速率.它支持两种获取permits接口,一种是如果拿不到立刻返回false,一种会阻塞等待一段时间看能不能拿到。

RateLimiter的使用

  • maven
    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>23.0</version>
    </dependency>
  • 实例
1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
// 这里的1表示每秒允许处理的量为1个
RateLimiter limiter = RateLimiter.create(1.0);
for (int i = 1; i <= 10; i++) {
// 请求RateLimiter, 超过permits会被阻塞
limiter.acquire();
String start = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
System.out.println("time:" + start + "---------------" + +i);
}
}
  • 结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    time:2019-09-03 10:08:11---------------1
    time:2019-09-03 10:08:12---------------2
    time:2019-09-03 10:08:13---------------3
    time:2019-09-03 10:08:14---------------4
    time:2019-09-03 10:08:15---------------5
    time:2019-09-03 10:08:16---------------6
    time:2019-09-03 10:08:17---------------7
    time:2019-09-03 10:08:18---------------8
    time:2019-09-03 10:08:19---------------9
    time:2019-09-03 10:08:20---------------10

    实际使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Service
    public class GuavaRateLimiterService {
    /**
    * 每秒控制5个许可
    */
    RateLimiter rateLimiter = RateLimiter.create(5.0);

    /**
    * 获取令牌
    *
    * @return
    */
    public boolean tryAcquire() {
    return rateLimiter.tryAcquire();
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Autowired
    private GuavaRateLimiterService guavaRateLimiterService;

    @GetMapping(value = "/sayHello")
    public String sayHello() {
    if (guavaRateLimiterService.tryAcquire()) {
    log.info("获取许可成功");
    return "hello";
    }
    log.info("获取许可失败");
    return "hi";
    }
    亦可用自定义注解+切面的方式实现