对大量的访问请求进行限流

114 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第10天,点击查看活动详情

为什么要限流

在高并发环境下,海量的用户请求极易对后台服务器带来性能压力,甚至使服务器宕机。一种有效的避免并发访问压力的手段就是“限流”。顾名思义,限流就是限制并发访问的流量。在实际项目中,限流的手段多种多样,可以在前台、网关、后台、数据库等不同层次限制用户的访问流量

比如前天晚上微博,贴吧b站都崩了,我愿称之为崩坏3,为什么会出现“崩坏”呢?

最简单的举例就是,假设一个线程占用20mb内存,但是总内存只有1gb,那么如果有几千甚至几万线程进入,那么服务器肯定接收不了,只能“崩坏”。所以我们需要对进入服务器的线程做限流,保证进入服务器的线程,服务器能接收,其他的进行阻塞。

限流算法

最常用的限流算法是漏桶算法和令牌桶算法,二者相对而言,令牌桶算法的使用性更加普遍。令牌桶算法采用了生产者消费者的思想:有一个存放令牌的桶,令牌桶算法以恒定的速率生成令牌,并放入桶中(生产者);与此同时,客户端的请求会从该令牌桶中取出令牌(消费者),只有获取到令牌的消费者,才能向服务器发起请求。显然,令牌桶算法生产令牌的速率决定了服务器处理用户请求的速率。如果令牌的生产速度太慢,用户请求获取令牌的速度就慢,因此请求抵达服务器的数量就少;反之,如果令牌的生产速度太快,用户请求获取令牌的速度就快,因此请求抵达服务器的数量就多。

令牌桶算法如何编写呢?可以直接使用 Google Guava 组件封装好的 API。我们进行如下实验来模拟电商平台销售过程:

首先使用maven创建一个项目,并且新建文件。

项目结构如下:

image.png

在 pom.xml 中添加如下依赖:

<project 
......
  <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
   </properties>

   <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
        <!-- 引入 Google Guava 依赖 -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>30.1-jre</version>
        </dependency>
   </dependencies>
</project>

在 TokenRateLimiter.java 中添加代码

首先定义商品库存,假设是100,用MAX_NUM表示,int类型。

然后定义已经卖出去的商品数量,用goods表示,类型AtomicInteger,

通过guava定义生成令牌速度,static RateLimiter tokenRateLimiter = RateLimiter.create(5);

五表示一秒五个。

然后开始写请求令牌桶算法。

public static void currentLimite() {
    // 每个用户请求会持续1秒
    if (tokenRateLimiter.tryAcquire(1, TimeUnit.SECONDS)) {
        if (goods.get() < MAX_NUM) {
            // 卖出的商品数量+1
            int goodNo = goods.getAndIncrement();
            // 购买商品服务
            //TODO 商品服务的实现方法
            System.out.println("第" + goodNo + "件商品购买成功");
        } else {
            System.out.println("售罄!");
        }
    }

第一个if表示强制线程去抢令牌桶,抢不到等待一秒 第二个,如果抢到了令牌桶,并且我们商品卖出的不超过100个,才能进行下面卖出

然后去appTest书写test文件

需要书写1:apptest的String类型构造器 书写2:模拟多线程并发。 这里假设10个线程。run方法里面是,抢我们Token桶,抢不到就阻塞,抢完后,打印当前卖出商品。

在 AppTest.java 中编写一个测试类:

import 。。。。
public class AppTest
    extends TestCase
{
 
    public AppTest( String testName )
    {
        super( testName );
    }
    public static Test suite()
    {
        return new TestSuite( AppTest.class );
    }
    public void testApp()throws InterruptedException {
        //模拟多线程高并发环境
        List<Thread> threadList = new ArrayList<>();
        Date date1 = new Date();
        for(int i = 0; i < 10; i++){
            threadList.add(new Thread(() -> {
                while(true) {
                    // 抢 1 个 Token,抢不到阻塞
                    TokenRateLimiter.currentLimite();
                    if(TokenRateLimiter.getGoods() == 100){
                        break;
                    }
                }
            }));
            threadList.get(i).start();
        }
        threadList.get(0).join();
        Date date2 = new Date();
        System.out.println("100 件商品经过 " + ((date2.getTime() - date1.getTime()) / 1000.0) + " 秒售罄");
    }
}

结果如下

image.png

把桶里面的5,改成10

image.png

可以看到此时销售速度很明显变快了。

总结

本次实例其实是一个简单的模拟情景,在真实开发中,还会根据项目成本,测试结果等进行扩展。比如在卖出商品,进行db操作,而且还需要在多线程访问TokenRateLimiter之前,进行限流,缓存等,保证不会把TokenRateLimiter对应服务器冲坏。

“限流”是软件项目的一个全局性策略,不要想着通过一个类、一个算法就能彻底解决。