为什么需要限流?

在了解限流之前,先了解几个相关概念。

吞吐量(TPS)、QPS、并发数、响应时间(RT)几个概念做下了解,记录如下:

  1. 响应时间(RT)
    响应时间是指系统对请求作出响应的时间。直观上看,这个指标与人对软件性能的主观感受是非常一致的,因为它完整地记录了整个计算机系统处理请求的时间。由于一个系统通常会提供许多功能,而不同功能的处理逻辑也千差万别,因而不同功能的响应时间也不尽相同,甚至同一功能在不同输入数据的情况下响应时间也不相同。所以,在讨论一个系统的响应时间时,人们通常是指该系统所有功能的平均时间或者所有功能的最大响应时间。当然,往往也需要对每个或每组功能讨论其平均响应时间和最大响应时间。
    对于单机的没有并发操作的应用系统而言,人们普遍认为响应时间是一个合理且准确的性能指标。需要指出的是,响应时间的绝对值并不能直接反映软件的性能的高低,软件性能的高低实际上取决于用户对该响应时间的接受程度。对于一个游戏软件来说,响应时间小于100毫秒应该是不错的,响应时间在1秒左右可能属于勉强可以接受,如果响应时间达到3秒就完全难以接受了。而对于编译系统来说,完整编译一个较大规模软件的源代码可能需要几十分钟甚至更长时间,但这些响应时间对于用户来说都是可以接受的。
  2. 吞吐量(Throughput)
    吞吐量是指系统在单位时间内处理请求的数量。对于无并发的应用系统而言,吞吐量与响应时间成严格的反比关系,实际上此时吞吐量就是响应时间的倒数。前面已经说过,对于单用户的系统,响应时间(或者系统响应时间和应用延迟时间)可以很好地度量系统的性能,但对于并发系统,通常需要用吞吐量作为性能指标。
    对于一个多用户的系统,如果只有一个用户使用时系统的平均响应时间是t,当有你n个用户使用时,每个用户看到的响应时间通常并不是n×t,而往往比n×t小很多(当然,在某些特殊情况下也可能比n×t大,甚至大很多)。这是因为处理每个请求需要用到很多资源,由于每个请求的处理过程中有许多不走难以并发执行,这导致在具体的一个时间点,所占资源往往并不多。也就是说在处理单个请求时,在每个时间点都可能有许多资源被闲置,当处理多个请求时,如果资源配置合理,每个用户看到的平均响应时间并不随用户数的增加而线性增加。实际上,不同系统的平均响应时间随用户数增加而增长的速度也不大相同,这也是采用吞吐量来度量并发系统的性能的主要原因。一般而言,吞吐量是一个比较通用的指标,两个具有不同用户数和用户使用模式的系统,如果其最大吞吐量基本一致,则可以判断两个系统的处理能力基本一致。
  3. 并发用户数
    并发用户数是指系统可以同时承载的正常使用系统功能的用户的数量。与吞吐量相比,并发用户数是一个更直观但也更笼统的性能指标。实际上,并发用户数是一个非常不准确的指标,因为用户不同的使用模式会导致不同用户在单位时间发出不同数量的请求。一网站系统为例,假设用户只有注册后才能使用,但注册用户并不是每时每刻都在使用该网站,因此具体一个时刻只有部分注册用户同时在线,在线用户就在浏览网站时会花很多时间阅读网站上的信息,因而具体一个时刻只有部分在线用户同时向系统发出请求。这样,对于网站系统我们会有三个关于用户数的统计数字:注册用户数、在线用户数和同时发请求用户数。由于注册用户可能长时间不登陆网站,使用注册用户数作为性能指标会造成很大的误差。而在线用户数和同事发请求用户数都可以作为性能指标。相比而言,以在线用户作为性能指标更直观些,而以同时发请求用户数作为性能指标更准确些。
  4. QPS每秒查询率(Query Per Second)
    每秒查询率QPS是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准,在因特网上,作为域名系统服务器的机器的性能经常用每秒查询率来衡量。对应fetches/sec,即每秒的响应请求数,也即是最大吞吐能力。 (看来是类似于TPS,只是应用于特定场景的吞吐量)

通过了解以上概念,可以知道,之所以需要限流,实际上就是解决上面几个指标出现的问题。

正常的店铺营销都会进行前期大量宣传,也会有黄牛进行刷存货贩卖,比如淘宝双十一,很早就进行活动开展,这样便会吸引极其多的人,抢购极少的商品,必然会有很多人抢购不到,然而却给系统带来了很大压力。

在秒杀中,高并发问题是最常见的,秒杀的特点就是,瞬时,量大,极高的吞吐量QPS,并且要保证响应时间并发用户数

为了解决短时间内会有大量请求涌进来,后端防止并发过高造成缓存击穿或者失效,击垮数据库限流是重要的一环。

限流的几种实现方式

  1. 前端限流
  2. 后端限流
  • 应用限流
  • tomcat限流
  • nginx限流
  • 接口限流
  • redis限流
  • Java SDK限流(令牌桶,漏桶算法)
  • 组件限流
  • 阿里Sentinel

前端限流

前端限流,用户在秒杀按钮点击以后发起请求,那么在接下来的x秒是无法点击(通过设置按钮为disable)。这样可以防止用户疯狂点击请求。

Tomcat限流

在Tomcat容器中,我们可以通过自定义线程池,配置最大连接数,请求处理队列等参数来达到限流的目的。

但是不能达到接口级别得限流。

nginx限流

Nginx是一个高性能Web服务器,它的并发能力可以达到几万,而Tomcat只有几百。通过Nginx映射客户端请求,再分发到后台Tomcat服务器集群中可以大大提升并发能力

但是也可以利用nginx进行限流:比如单个IP限制每秒访问50次。通过Nginx限流模块,我们可以设置一旦并发连接数超过我们的设置,将返回503错误给客户端。

redis+lua限流

redis限流属于接口限流,主要事借助redis做限流,通过aop或拦截器做限流,利用lua脚步是为了做到原子性。

核心代码:

public boolean acquire(String key) {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<Long>();
        String lua = "local key = KEYS[1]" +
                " local period = ARGV[1]" +
                " local limit= ARGV[2]" +
                " local times = redis.call('incr',key)" +
                " if times == 1 then" +
                " redis.call('expire',KEYS[1], period)" +
                " end" +
                " if times > tonumber(limit) then" +
                " return 0" +
                " end" +
                " return 1";
        redisScript.setScriptText(lua);
        redisScript.setResultType(Long.class);
        //表示1s 内最多访问3次
        //key [key ...],被操作的key,可以多个,在lua脚本中通过KEYS[1], KEYS[2]获取
        //arg [arg ...],参数,可以多个,在lua脚本中通过ARGV[1], ARGV[2]获取。
        //0: 超出限制,else:正常请求
        Long count = (Long) stringRedisTemplate.execute(redisScript, Arrays.asList(key), "1", "3");
        System.err.println(System.currentTimeMillis() + "<>" + count);
        //0:超出范围
        return count == 0;

以上代码,通过lua脚本的优势:

  • 减少网络开销。可以将多个请求通过脚本的形式一次发送,减少网络时延
  • 原子操作。redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在编写脚本的过程中无需担心会出现竞态条件,无需使用事务。
  • 复用。客户端发送的脚本会永久存在redis中,这样,其他客户端可以复用这一脚本而不需要使用代码完成相同的逻辑。

Java SDK限流

  • 令牌桶算法

生成令牌的速度是恒定的,而请求去拿令牌是没有速度限制的。这意味,面对瞬时大流量,该算法可以在短时间内请求拿到大量令牌,而且拿令牌的过程并不是消耗很大的事情

设计一个秒杀系统之限流得几种方式_限流

实现方式: RateLimiter

  1. RateLimiter在并发环境下使用是安全的:它将限制所有线程调用的总速率。注意,它不保证公平调用。Rate limiter(直译为:速度限制器)经常被用来限制一些物理或者逻辑资源的访问速率。这和java.util.concurrent.Semaphore正好形成对照。
  2. 一个RateLimiter主要定义了发放permits的速率。如果没有额外的配置,permits将以固定的速度分配,单位是每秒多少permits。默认情况下,Permits将会被稳定的平缓的发放。
  3. 可以配置一个RateLimiter有一个预热期,在此期间permits的发放速度每秒稳步增长直到到达稳定的速率

核心代码:

<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>26.0-jre</version>
  <!-- or, for Android: -->
  <version>26.0-android</version>
</dependency>
//一秒创建3个令牌
    RateLimiter rateLimiter = RateLimiter.create(3.0);

    public void query() throws InterruptedException {
       //尝试获取令牌
        if (rateLimiter.tryAcquire()) {
            System.err.println(System.currentTimeMillis() + "   业务执行成功!");
            TimeUnit.MILLISECONDS.sleep(200);
        } else {
            TimeUnit.MILLISECONDS.sleep(200);
            System.err.println("************ " + System.currentTimeMillis() + "   业务执行失败!");
        }
    }
  • 漏桶算法

设计一个秒杀系统之限流得几种方式_限流_02

漏桶的出水速度是恒定的,那么意味着如果瞬时大流量的话,将有大部分请求被丢弃掉(也就是所谓的溢出)。

漏桶算法没有标准的sdk提供实现,可以自己去实现一个漏桶算法。

分布式限流-阿里Sentinel

由于官网的示例特别清晰,这里只简单介绍一下,比较难得地方

官网:Spring Cloud Alibaba Sentinel

  • 引入pom文件
<dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-core</artifactId>
        </dependency>

        <!-- 持久化到nacos时使用,本地规则限流可以不用 -->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
      
</dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>

1, 引入上述全部依赖

2, 配置nacos

定义一个bootstrap.yml文件,写入

# Spring
spring:
  cloud:
       nacos:
         discovery:
           # 服务注册地址
           server-addr: xxx.xxx.xx.xx:8848
         config:
           # 配置中心地址
           server-addr: xxx.xxx.xx.xx:8848
           # 配置文件格式
           file-extension: yml
           #配置文件名称dataId
           name: cloud-sentinel.yml

在application种配置好sentinel

spring:
  cloud:
    sentinel:
      transport:
      # 控制台地址
        dashboard: localhost:8080   #sentinel
        port: 8719
      #配置nacos后一定要配置datasource,否则本地规则会失效
      datasource:
        flow: #flow是数据源名,可以自行随意修改
          nacos:
            server-addr: 47.99.216.57:8848
            dataId: ${spring.application.name}-flow-rules
            groupId: SENTINEL_GROUP
            data-type: json
            rule-type: flow
  application:
    name: cloud-sentinel
  • 定义一个接口
@GetMapping("/anno")
    @ResponseBody
    @SentinelResource(value = "cloud-sentinel-flow-rules",blockHandler = "handleException")
    public String getOrder1() {
        return "访问正常";
    }

    @ResponseBody
    public String handleException(BlockException exception){
        return  "熔断、限流成功"+exception.getClass().getCanonicalName();
    }
  • 定义一个规则

设计一个秒杀系统之限流得几种方式_微服务_03

[{"app":"cloud-sentinel","clusterConfig":{"fallbackToLocalWhenFail":true,"sampleCount":10,"strategy":0,"thresholdType":0,"windowIntervalMs":1000},"clusterMode":false,"controlBehavior":0,"count":1.0,"gmtCreate":1634713185810,"gmtModified":1634713608918,"grade":1,"id":11,"ip":"172.28.112.1","limitApp":"default","port":8720,"resource":"cloud-sentinel-flow-rules","strategy":0}]

在nacos定义一个规则,后续修改删除,和控制台两边会同步的

设计一个秒杀系统之限流得几种方式_限流_04

  • 测试

设计一个秒杀系统之限流得几种方式_限流_05