Guava限流器原理浅析

news2024/11/16 4:30:42

文章目录

  • 基本知识
    • 限流器的类图
    • 使用示例
  • 原理解析
    • 限流整体流程
    • 问题驱动
      • 1、限流器创建的时候会初始化令牌吗?
      • 2、令牌是如何放到桶里的?
      • 3、如果要获取的令牌数大于桶里的令牌数会怎么样
      • 4、令牌数量的更新会有并发问题吗
  • 总结

实际工作中难免有限流的场景。我们熟知的限流算法有计数器限流(固定窗口、滑动窗口)算法、漏桶算法、令牌桶算法等。其具体实现也多种多样,本文就来简单窥探一下Guava的实现。

基本知识

限流器的类图

在这里插入图片描述
RateLimiter:限流器基类,定义限流器的创建、令牌的获取等操作。
SmoothRateLimiter:定义一种平滑的限流器,也是抽象类,继承RateLimiter。
SmoothBursty:普通的平滑限流器实现类,实现SmoothRateLimiter。以稳定的速率生成令牌,则会同时全部被获取到。比如令牌桶现有令牌数为5,这时连续进行10个请求,则前5个请求会全部直接通过,没有等待时间,之后5个请求则每隔200毫秒通过一次。
SmoothWarmingUp:预热的平滑限流器实现类,实现SmoothRateLimiter。随着请求量的增加,令牌生成速率会缓慢提升直到一个稳定的速率。比如令牌桶现有令牌数为5,这时连续进行10个请求,只会让第一个请求直接通过,之后的请求都会有等待时间,等待时间不断缩短,直到稳定在每隔200毫秒通过一次。这样,就会有一个预热的过程。

下文以SmoothBursty为例来分析限流原理。

使用示例

public class RateLimitTest {
    public static void main(String[] args) throws InterruptedException {
    	// 1、创建限流器,一秒内最多允许2个请求通过
        RateLimiter rateLimiter = RateLimiter.create(2);
        serial(rateLimiter);
    }

    private static void serial(RateLimiter rateLimiter) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            String time = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME);
            // 2、尝试获取令牌,不论是否能获取到都直接返回
            boolean res = rateLimiter.tryAcquire();
            // 获取令牌,如果获取不到就一直等待
			// rateLimiter.acquire();
            if (res) {
                System.out.println(time + ":请求被允许");
            } else {
                System.out.println(time + ":请求被限流");
            }
            Thread.sleep(250);
        }
    }
 }

执行结果:

15:52:08.583:请求被允许
15:52:08.852:请求被限流
15:52:09.108:请求被允许
15:52:09.361:请求被限流
15:52:09.617:请求被允许
15:52:09.872:请求被限流
15:52:10.127:请求被允许
15:52:10.378:请求被限流
15:52:10.629:请求被允许
15:52:10.882:请求被限流

可以看到同一秒内最多只有2个请求被允许。

原理解析

限流整体流程

在这里插入图片描述

  1. 创建限流器。此时桶里的令牌数为0。设置QPS=5(每秒最多允许5个请求),这个数字“5”带表了两层含义:
    1)桶里最大只能容纳5个令牌。
    2)一秒可以生成5个令牌,生成一个令牌需要1/5=0.2秒=200毫秒。
  2. 发起请求。此时距离限流器创建已经经过了一秒,桶里应该存在5个令牌,而本次请求需要获取并消耗1个令牌。
  3. 更新令牌数量。

上面只是描述了一个大致思路,还有很多细节问题需要考虑,下文就以问题来驱动原理探究。

问题驱动

限流器关键属性解释
SmoothRateLimiter.java

/**
 * 当前桶中已存在的令牌数,如果请求需要的令牌数小于已存在的令牌数,就允许通过
 */
double storedPermits;

/**
 * 令牌桶可以保存的最大令牌数
 */
double maxPermits;

/**
 * 多长时间可以生成一个令牌,单位是微秒。比如RateLimiter.create(5),就意味着1秒生成5个令牌,那么生成一个令牌就需要200ms
 */
double stableIntervalMicros;

/**
 * 重要!!!下一个请求可以被允许获取令牌的时间点,单位是微秒。
 */
private long nextFreeTicketMicros = 0L;

1、限流器创建的时候会初始化令牌吗?

我们从限流器的创建源码着手分析。
RateLimiter.java

public static RateLimiter create(double permitsPerSecond) {
    return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer());
  }

  static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {
    // 创建一个普通平滑限流器
    RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);
    // 关键:设置限流器速率相关信息
    rateLimiter.setRate(permitsPerSecond);
    return rateLimiter;
  }

public final void setRate(double permitsPerSecond) {
    checkArgument(
        permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive");
    synchronized (mutex()) {
      // 关键
      doSetRate(permitsPerSecond, stopwatch.readMicros());
    }
  }

  // 由子类即SmoothRateLimiter来实现
  abstract void doSetRate(double permitsPerSecond, long nowMicros);

SmoothRateLimiter.java

@Override
  final void doSetRate(double permitsPerSecond, long nowMicros) {
    // 重点1:生成令牌,并同步下次可以获取令牌的时间
    resync(nowMicros);
    double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;
    // 将stableIntervalMicros从默认的0.0设置为 生成一个令牌所需的时间
    this.stableIntervalMicros = stableIntervalMicros;
    // 重点2
    doSetRate(permitsPerSecond, stableIntervalMicros);
  }

  // 重点1
    /** 限流器创建(doSetRate(double permitsPerSecond, long nowMicros))
     * 以及 获取令牌(reserveEarliestAvailable(int requiredPermits, long nowMicros))的时候都会调用这个方法
     * 如果是创建时调用 由于coolDownIntervalMicros返回值即stableIntervalMicros=0,所以当前storedPermits的计算结果仍为0
     *
     */
  void resync(long nowMicros) {
    if (nowMicros > nextFreeTicketMicros) {
      // 下一次可以获取令牌的时间到现在这段时间内,需要生成多少令牌,由于当前coolDownIntervalMicros()会返回0.0,所以计算结果为Infinity(无穷)
      double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
      // 保证桶里的令牌数不能超过最大允许的令牌数,因为newPermits=无穷,所以这里计算出桶里的令牌数应该是0
      storedPermits = min(maxPermits, storedPermits + newPermits);
      // 将nextFreeTicketMicros值设为限流器创建的时间
      nextFreeTicketMicros = nowMicros;
    }
  }

  // 由子类即SmoothBursty来实现
  abstract void doSetRate(double permitsPerSecond, double stableIntervalMicros);


static final class SmoothBursty extends SmoothRateLimiter {
    // 重点2
    @Override
    void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
      // 当前允许的最大令牌数,限流器创建时该值为0.0
      double oldMaxPermits = this.maxPermits;
      // 计算最新的允许的最大令牌数
      maxPermits = maxBurstSeconds * permitsPerSecond;
      if (oldMaxPermits == Double.POSITIVE_INFINITY) {
        // if we don't special-case this, we would get storedPermits == NaN, below
        storedPermits = maxPermits;
      } else {
        // 如果最大允许的令牌数时0,则将桶里的令牌数也置为0
        storedPermits =
            (oldMaxPermits == 0.0)
                ? 0.0 // initial state
                : storedPermits * maxPermits / oldMaxPermits;
      }
    }
    
    @Override
    double coolDownIntervalMicros() {
      // 返回的就是生成一个令牌需要多长时间,该值在限流器创建的时候初始值为0.0
      return stableIntervalMicros;
    }
  }

通过上面源码中 重点1和重点2的分析可以发现,在创建限流器的时候,当前桶中的令牌数一直是0。

结论:限流器创建的时候不会初始化令牌

2、令牌是如何放到桶里的?

我们经常看到对于令牌桶限流算法的描述是:将令牌每隔一段时间定时放入桶中。
乍一看也许需要一个定时器才能达到这个效果。但Guava的实现告诉我们其实不用这么复杂,只需要一个计数器(storedPermits)变量就能搞定。

想要知道令牌如何放到桶里,就需要从获取令牌的时候开始探索。

这有点奇怪对吗,正常是先把令牌放到桶里,然后才获取令牌,即有因才有果;但是我们却需要先知道如何获取令牌,才能知道令牌是如何放到桶里的。
在我看来,这正是Guava实现的巧妙之处。

RateLimiter.java

/**
* 尝试获取令牌
* @param permits 要获取的令牌数
* @param timeout 能获取到令牌的最大等待时间,等待时间超过这个时间就直接返回false。如果该值是0,不做任何等待,直接返回是否获取到令牌
*/
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) {
    long timeoutMicros = max(unit.toMicros(timeout), 0);
    checkPermits(permits);
    long microsToWait;
    synchronized (mutex()) {
      long nowMicros = stopwatch.readMicros();
      // 判断在超时时间内能否获取到令牌
      if (!canAcquire(nowMicros, timeoutMicros)) {
        // 获取不了就返回false
        return false;
      } else {
        // 关键:如果在超时时间内能获取到令牌,计算需要等待的时间
        microsToWait = reserveAndGetWaitLength(permits, nowMicros);
      }
    }
    // 睡眠等待足够的时间
    stopwatch.sleepMicrosUninterruptibly(microsToWait);
    return true;
  }

  private boolean canAcquire(long nowMicros, long timeoutMicros) {
    // 获取最早可以获得令牌的时间
    return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;
  }

  final long reserveAndGetWaitLength(int permits, long nowMicros) {
    // 关键:获取令牌并返回最早能获得令牌的时间
    long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
    return max(momentAvailable - nowMicros, 0);
  }

  // 由子类即SmoothBursty实现
  abstract long queryEarliestAvailable(long nowMicros);
  
  // 由子类即SmoothBursty实现
  abstract long reserveEarliestAvailable(int permits, long nowMicros);

SmoothBursty.java

final long queryEarliestAvailable(long nowMicros) {
    // 又是它!!!待会分析它到底是个什么东西
    return nextFreeTicketMicros;
  }

  /**
   * 获取令牌的核心方法
   *
   * @param requiredPermits 需要获取的令牌数
   * @param nowMicros
   * @return
   */
  @Override
  final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
    // 关键:生成令牌,并将下一次可以获取令牌的时间设置为当前时间
    resync(nowMicros);
    // 这里拿到的是最早可以获取到令牌的时间
    long returnValue = nextFreeTicketMicros;
    // 实际能获取的令牌数,有可能需要的令牌数大于当前桶里的令牌数,两者取最小
    double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
    // 实际拿到的令牌数相比需要的令牌数还差多少
    double freshPermits = requiredPermits - storedPermitsToSpend;
    // 要拿到还差的令牌数,还需要等多久
    long waitMicros =
        storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
            + (long) (freshPermits * stableIntervalMicros);

    // 重点3:更新下一次可以获取令牌的时间 = 当前时间 + 要拿到还差的令牌数要等的时间
    this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
    // 重点4:更新桶里还剩的令牌数
    this.storedPermits -= storedPermitsToSpend;
    return returnValue;
  }

  void resync(long nowMicros) {
    if (nowMicros > nextFreeTicketMicros) {
      // 下一次可以获取令牌的时间到现在这段时间内,需要生成多少令牌
      double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
      // 重点1:生成令牌并放入桶中
      storedPermits = min(maxPermits, storedPermits + newPermits);
      // 重点2:将nextFreeTicketMicros值设为当前时间
      nextFreeTicketMicros = nowMicros;
    }
  }

通过上面源码中的重点1、重点2、重点3、重点4可以发现:

  • 重点1是向桶里放令牌,既增加令牌计数器storedPermits
  • 重点4是从桶里获取令牌,既减少令牌计数器storedPermits
  • 重点2和重点3都是更新nextFreeTicketMicros

所以令牌的生成、获取都围绕着两个变量:storedPermits(当前桶里的令牌数)和nextFreeTicketMicros(下次可以获得令牌的时间)。

而这两个变量也正是Guava限流设计的巧妙之处:不必提前向桶里放入令牌,或通过一个单独的定时器向桶里放令牌,而是在获取令牌的时候增加令牌数量再减少令牌数量。

用图来更加直观的体现这里的逻辑。

nextFreeTicketMicros在源码中其实是用微秒级时间戳表示,为了方便理解,下面就用正常时间来表示。

在这里插入图片描述

  1. 创建限流器。RateLimiter rateLimiter = RateLimiter.create(5);即QPS=5,每秒生成5个令牌,生成1个令牌需要200毫秒,桶内最大令牌数=5。storedPermits(此时桶里的令牌数)=0,nextFreeTicketMicros(下次可以获取令牌的时间)=0。
  2. 请求A要获取1个令牌。rateLimiter.acquire();当前时间是2023-9-26 10:00:00。
  3. 发现当前时间 > nextFreeTicketMicros,两者相差的这段时间远远大于1秒,而1秒可以生成5个令牌(最多也只能存5个)。同时要把nextFreeTicketMicros设置为当前时间,意味着现在桶里已经有令牌了,现在马上就可以获取到令牌。此时storedPermits=5,nextFreeTicketMicros=2023-9-26 10:00:00。
  4. 获取到1个令牌,此时storedPermits=4,nextFreeTicketMicros=2023-9-26 10:00:00。
  5. 请求B要获取10个令牌。rateLimiter.acquire(10);当前时间是2023-9-26 10:00:01.001。
  6. 发现当前时间 > nextFreeTicketMicros,两者相差的这段时间大于1秒,1秒可以生成5个令牌,当前桶里还有4个,5+4=9,但桶最多只能存5个。同时要把nextFreeTicketMicros设置为当前时间,意味着现在桶里已经有令牌了,现在马上就可以获取到令牌。此时storedPermits=5,nextFreeTicketMicros=2023-9-26 10:00:01.001。
  7. 需要获取10个令牌,但是现在桶里只有5个,即使全部获取还欠5个,那就提前透支5个咯。意味着接下来这1秒生成的5个令牌是预留给当前请求的,其它请求1秒后才能再获取令牌。此时storedPermits=0,nextFreeTicketMicros=2023-9-26 10:00:02.001。
  8. 请求C要获取1个令牌。rateLimiter.acquire();当前时间是2023-9-26 10:00:01.999。
  9. 由于nextFreeTicketMicros=2023-9-26 10:00:02.001。还没到下次可以获取令牌的时间,就只能等待。
  10. 等待ing …
  11. 当前时间是2023-9-26 10:00:02.200。当前时间 > nextFreeTicketMicros,相差的这段时间是200毫秒,刚好能生成1个令牌。同时要把nextFreeTicketMicros设置为当前时间,意味着现在桶里已经有令牌了,现在马上就可以获取到令牌。此时storedPermits=1,nextFreeTicketMicros=2023-9-26 10:00:02.200。
  12. 获取到1个令牌,此时storedPermits=0,nextFreeTicketMicros=2023-9-26 10:02:200。

结论:令牌的生成其实是在令牌的获取逻辑中。

3、如果要获取的令牌数大于桶里的令牌数会怎么样

经过上面的分析可以得出结论:会透支/预支不足的令牌数。

4、令牌数量的更新会有并发问题吗

可以看一下获取令牌时的源码:

public double acquire(int permits) {
    long microsToWait = reserve(permits);
    stopwatch.sleepMicrosUninterruptibly(microsToWait);
    return 1.0 * microsToWait / SECONDS.toMicros(1L);
  }

  final long reserve(int permits) {
    checkPermits(permits);
    // 这里已经加了同步处理
    synchronized (mutex()) {
      return reserveAndGetWaitLength(permits, stopwatch.readMicros());
    }
  }

结论:同一个限流器不会有并发问题。

总结

本文并不过多深度剖析源码和原理。旨在以初学者的角度窥探Guava限流器的限流实现思路,并解答一些理解中存在的疑惑。

尤其是令牌生成和获取的设计思路也能对自己的日常工作有启发作用~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1047896.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

振弦采集仪结合无线中继扩展应用岩土工程监测的解决方案

振弦采集仪结合无线中继扩展应用岩土工程监测的解决方案 岩土工程监测是现代工程建设不可或缺的一部分&#xff0c;其目的是确保工程安全和稳定。然而&#xff0c;在进行监测时常常面临许多挑战。传统的岩土工程监测方法需要大量的人力、物力和时间&#xff0c;而且往往难以采…

顽固污渍一键去除,还有紫外线除菌功能,希亦超声波清洗机体验

日常生活中&#xff0c; 眼镜、牙刷等小物件的清洗该怎么清洗呢&#xff1f;如果使用肥皂水或者专业清洁剂来处理&#xff0c;还需要使用一些小工具来帮助清洁&#xff0c;比如清洁布、棉签等&#xff0c;不仅会很麻烦&#xff0c;而且需要频繁擦拭&#xff0c;镜片等物品光滑的…

601-体育馆的人流量

文章目录 601-体育馆的人流量1. 题目2. 思路3. 解决4. 运行结果 601-体育馆的人流量 1. 题目 2. 思路 思路&#xff1a;查询Stadium表中人流量超过100的记录&#xff0c;将查询结果与自身的临时表连接&#xff0c;再使用where获得满足条件的记录 查询Stadium表中人流量超过10…

现代数据中心发明人Luiz André Barroso去世,享年59岁,Jeff Dean、劈柴发推悼念

Luiz Andr Barroso因故去世&#xff0c;享年59岁。他作为现代云计算行业的奠基人&#xff0c;为谷歌的发展做出了不可磨灭的贡献。 数据中心发明人&#xff0c;云计算的奠基人&#xff0c;谷歌22年老兵Luiz Andr Barroso于9月16日意外去世&#xff0c;享年59岁。 谷歌CEO 劈柴…

NVM的下载安装和使用

node包管理工具NVM让我们更加方便在各个node版本之间切换来适配不同的项目 一、下载安装 下载地址&#xff1a;github下载地址 https://github.com/coreybutler/nvm-windows/releases安装一直点下一步就行&#xff0c;可以安装在D、E盘都行&#xff0c;安装后检查是否安装成功…

“身份验证失败” authentication failed报错的原因

问题描述 问题出在target “send_emai” 的password属性&#xff0c;我们认为输入的是邮箱的密码&#xff0c;即使是给出正确的密码仍报错的情况下&#xff0c;还是让大家输入正确的密码。结果呢&#xff1f; 这里的“password”输入的并不是密码&#xff0c;而是一种授权码&…

Servlet开发-session和cookie理解案例-登录页面

项目展示 进入登录页面&#xff0c;输入正确的用户名和密码以后会自动跳到主页 登录成功以后打印用户名以及上次登录的时间&#xff0c;如果浏览器和客户端都保存有上次登录的信息&#xff0c;则不需要登录就可以进入主页 编码思路 1.首先提供一个登录的前端页面&…

使用MySQL聚合函数来聚合数据,结果发现有刺客...

问题&#xff1a; 使用MySQL聚合函数 group_concat 的坑&#xff01; 现象&#xff1a; 我有个业务&#xff0c;需要将表中符合条件的数据行的id聚合成一个字符串&#xff0c;以供另外一张表的查询过滤。 SELECTx FROMt_A WHEREFIND_IN_SET(guan_lian,(SELECTgroup_concat( i…

毫米波V2I网络的链路层仿真研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

工时管理系统,一款灵活性强、轻量级工时记录和管理工具

一、开源项目简介 无鱼工时管理系统&#xff0c;是一款轻量级工时记录和管理工具&#xff0c;包括项目管理&#xff0c;工时上报&#xff0c;工时日报&#xff0c;工时统计等功能。 无鱼工时管理系统可通过员工工时上报的方式&#xff0c;来记录项目所花费的工时&#xff0c;…

分享74个Java源代码总有一个是你想要的

分享74个Java源代码总有一个是你想要的 链接&#xff1a;https://pan.baidu.com/s/1DfGiAF7Ipde-SKeHZJ7-jQ?pwd8888 提取码&#xff1a;8888 项目名称 Akaxin v0.5.4 B2C网站管理系统购物网 v1.5 B3LOG Solo v1.0.0 for MySQL belog博客程序 v1.1 CKFinder 文件管理器…

创建视图权限问题ora1031,ora1720

Ora-1031 使用sql查询无问题&#xff0c;创建视图告警ora-1031 根据文档Document 271587.1 递归对象权限中角色收回时&#xff0c;会造成权限断档 解决方法&#xff1a; 对该对象进行授权&#xff08;尽量属主用户&#xff09; Grant select on interface.Material_view_pa…

字符检测专题第二期:通用、简单、快速,见证AI字符识别的超能力!

随着科技的不断进步&#xff0c;OCR&#xff08;光学字符识别&#xff09;技术在工业应用中扮演着越来越重要的角色。 在实际生产中&#xff0c;OCR技术可在生产流程监控、自动化设备控制、品质控制和物流控制等方面发挥作用&#xff0c;提高生产流水线的产量和质量&#xff0c…

基于Android的学生考勤签到请假管理系统+全套文档+全套视频教程

【项目功能介绍】 功能列表: 本系统包含后台管理和前端app双端系统, 本系统包含三个角色: 管理员,老师,学生。后台管理员的功能包含: 登录, 退出, ,通知管理,课程管理,课表管理,请假管理,用户管理; app端功能, 老师功能: 登录&#xff0c;退出, 注册, 日历控制日期展示, 查看不…

【JUC系列-08】深入理解CyclicBarrier底层原理和基本使用

JUC系列整体栏目 内容链接地址【一】深入理解JMM内存模型的底层实现原理https://zhenghuisheng.blog.csdn.net/article/details/132400429【二】深入理解CAS底层原理和基本使用https://blog.csdn.net/zhenghuishengq/article/details/132478786【三】熟练掌握Atomic原子系列基本…

【超详细】超详细的Linux系统入门教程,超适合小白和想转网络安全行业的朋友学习。

二&#xff0c;Linux系统 2.1 window远程连接Linux系统 ## 语法&#xff1a;ssh 用户名ip地址 ## 比如&#xff1a;ssh root192.168.61.1312.2 简单指令总结 # 远程登录linux系统ssh root192.168.202.136 # 断开连接exit # 重启系统reboot 或者 shutdown -h now # 关机指令s…

05-前端基础CSS第三天

01-CSS三大特性之层叠性 1.CSS的三大特性 CSS有三个非常重要的三个特性&#xff1a;层叠性、继承性、优先级。 1.1 层叠性 相同选择器给设置相同的样式&#xff0c;此时一个样式就会**覆盖&#xff08;层叠&#xff09;**另一个冲突的样式。层叠性主要解决样式冲突的问题。…

容器化运维:构建高可用RabbitMQ集群的Docker Compose指南

容器化运维&#xff1a;构建高可用RabbitMQ集群的Docker Compose指南 前言RabbitMQ相关知识点❓&#xff1a;Exchanges中的类型有什么区别❓&#xff1a;policies是什么❓&#xff1a;策略如何设置 docker-compose 搭建RabbitMQ的集群创建docker-compose.yml文件创建rabbitmq.c…

Docker——认识并安装Docker(上篇)

Docker 一、Docker认识二、Docker功能1、更快速的交付和部署2、更高效的虚拟化3、更轻松的迁移和扩展4、更简单的管理Docker 和 VM 三、学习Docker前的必备知识1、环境配置2、虚拟化部署方式3、虚拟化优点4、虚拟化局限性5、容器与虚拟机的区别6、Docker为什么比VM快&#xff1…

CentOS 7下载安装MySQL 5.7以及连接虚拟机CentOS 7数据库教程

## CentOS 7下载安装MySQL 5.7以及连接虚拟机CentOS 7数据库教程 文章目录 ## [CentOS 7下载安装MySQL 5.7以及连接虚拟机CentOS 7数据库教程](https://my.oschina.net/u/3986411/blog/4714780) 通过如下命令查看mariadb的安装包 rpm -qa | grep mariadb通过如下命令卸载mariad…