令牌桶算法与Guava的实现RateLimiter源码分析

令牌桶算法与Guava的实现RateLimiter源码分析

  • 令牌桶RateLimiter
    • 简介
    • RateLimiter使用示例
      • 导入maven依赖
      • 编写测试代码
    • RateLimiter的实现
    • 源码解析
      • SmoothRateLimiter
      • SmoothBursty恒速
      • 获取令牌
        • acquire(int)
        • tryAcquire(int,long,TimeUnit)
      • 存量桶系数
      • 小结
    • 优缺点
    • 与漏桶的区别
    • 总结

令牌桶RateLimiter

简介

令牌桶算法是一种限流算法。

令牌桶算法的原理就是以一个恒定的速度往桶里放入令牌,每一个请求的处理都需要从桶里先获取一个令牌,当桶里没有令牌时,则请求不会被处理,要么排队等待,要么降级处理,要么直接拒绝服务。当桶里令牌满时,新添加的令牌会被丢弃或拒绝。

令牌桶算法主要是可以控制请求的平均处理速率,它允许预消费,即可以提前消费令牌,以应对突发请求,但是后面的请求需要为预消费买单(等待更长的时间),以满足请求处理的平均速率是一定的。

RateLimiter使用示例

导入maven依赖

<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>29.0-jre</version>
</dependency>

编写测试代码

public class RateLimiterTest {
    public void limit() {
        // 创建一个限流器,设置每秒放置的令牌数为1个
        RateLimiter rateLimiter = RateLimiter.create(1);
        IntStream.range(1, 10).forEach(i -> {
            // 一次获取i个令牌
            double waitTime = rateLimiter.acquire(i);
            System.out.println("acquire:" + i + " waitTime:" + waitTime);
        });
    }

    public static void main(String[] args) {
        RateLimiterTest rateLimiterTest = new RateLimiterTest();
        rateLimiterTest.limit();
    }
}

这段代码创建一个限流器,设置每秒放置的令牌数为1个,并循环获取令牌,每次获取i个。
执行结果:
image.png
第一次获取一个令牌时,等待0s立即可获取到(这里之所以不需要等待是因为令牌桶的预消费特性),第二次获取两个令牌,等待时间1s,这个1s就是前面获取一个令牌时因为预消费没有等待延到这次来等待的时间,这次获取两个又是预消费,所以下一次获取(取3个时)就要等待这次预消费需要的2s了,依此类推。可见预消费不需要等待的时间都由下一次来买单,以保障一定的平均处理速率(上例为1s一次)。

RateLimiter的实现

RateLimiter类在guava里是一个抽象类,其有两个具体实现:

  1. SmoothBursty(平滑突发):以恒定的速率生成令牌。
  2. SmoothWarmingUp(顺利热身):令牌生成的速度逐渐提升,直到达到一个稳定的值。

其类图如下:
image.png
他们的关系与作用:

  • RateLimiter是顶层封装,提供新建令牌桶的方法
  • SleepingStopwatch是guava实现的一个时钟类,提供时钟功能
  • SmoothRateLimiter是令牌桶抽象,提供操作令牌桶的抽象方法,其有两个内部类SmoothBursty和SmoothWarmingUp
  • SmoothBursty是恒定速率令牌桶实现
  • SmoothWarmingUp是逐渐加速知道稳定的令牌桶实现

源码解析

SmoothRateLimiter

SmoothRateLimiter是令牌桶抽象,其有四个关键的属性:

/** 当前存储的许可证。 */
double storedPermits;

/** 存储许可证的最大数量。 */
double maxPermits;

/**
 * 两个单位请求之间的间隔,以我们的稳定速率。
 * 例如,每秒 5 个许可的稳定速率具有 200 毫秒的稳定间隔。
 */
double stableIntervalMicros;

/**
 * 授予下一个请求(无论其大小如何)的时间。
 * 在批准请求后,这将在将来进一步推送。大请求比小请求更进一步。
 */
private long nextFreeTicketMicros = 0L; // could be either in the past or future

他们的作用可以看注释。此外还有两个内部类,实现此抽象:

  1. SmoothBursty(平滑突发):以恒定的速率生成令牌。
  2. SmoothWarmingUp(顺利热身):令牌生成的速度逐渐提升,直到达到一个稳定的值。

SmoothBursty恒速

首先来看下恒定速率生成令牌的实现。其使用方法是:

// 创建一个限流器,设置每秒放置的令牌数为1个
RateLimiter rateLimiter = RateLimiter.create(1);

RateLimiter#create:

public static RateLimiter create(double permitsPerSecond) {
  /*
   * The default RateLimiter configuration can save the unused permits of up to one second. This
   * is to avoid unnecessary stalls in situations like this: A RateLimiter of 1qps, and 4 threads,
   * all calling acquire() at these moments:
   *
   * T0 at 0 seconds
   * T1 at 1.05 seconds
   * T2 at 2 seconds
   * T3 at 3 seconds
   *
   * Due to the slight delay of T1, T2 would have to sleep till 2.05 seconds, and T3 would also
   * have to sleep till 3.05 seconds.
   */
  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;
}

实际是创建了一个SmoothBursty实例,默认的maxBurstSeconds是1,其中SleepingStopwatch是guava实现的一个时钟类。
代码第三行调用了RateLimiter#setRate:

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

abstract void doSetRate(double permitsPerSecond, long nowMicros);

方法签名为:更新此 RateLimiter的稳定速率, permitsPerSecond 即构造 RateLimiter的工厂方法中提供的参数。当前受限制的 线程不会因此 调用而被唤醒,因此它们不会遵守新的速率,只有后续请求才会。
但请注意,由于每个请求都会偿还(如有必要,通过等待)前一个请求的成本,这意味着调用setRate后的下一个请求将不受新速率的影响,它将支付前一个请求的成本,这是根据以前的速率计算的。

此方法调用了抽象方法doSetRate,这里的实现是SmoothRateLimiter提供的,来看SmoothRateLimiter#doSetRate源码:

@Override
final void doSetRate(double permitsPerSecond, long nowMicros) {
    resync(nowMicros);
    double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;
    this.stableIntervalMicros = stableIntervalMicros;
    doSetRate(permitsPerSecond, stableIntervalMicros);
}

此方法:

  1. 调用 resync(nowMicros) 对 storedPermits 与 nextFreeTicketMicros 进行了调整——如果当前时间晚于 nextFreeTicketMicros,则计算这段时间内产生的令牌数,累加到 storedPermits 上,并更新下次可获取令牌时间 nextFreeTicketMicros 为当前时间。
  2. 计算stableIntervalMicros的值,此值代表产生令牌的时间间隔,1/permitsPerSecond(每秒几个令牌)
  3. 调用doSetRate(double, double)

其将输入的permitsPerSecond转换为速度,传给SmoothRateLimiter的doSetRate(permitsPerSecond, stableIntervalMicros)方法,doSetRate(permitsPerSecond, stableIntervalMicros)方法是抽象方法

abstract void doSetRate(double permitsPerSecond, double stableIntervalMicros);

由SmoothBursty的实现为:

@Override
void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
    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 {
        storedPermits =
        (oldMaxPermits == 0.0)
        ? 0.0 // initial state
        : storedPermits * maxPermits / oldMaxPermits;
    }
}

此方法计算maxPermits的值maxBurstSeconds * permitsPerSecond。maxBurstSeconds是new SmoothBursty(SleepingStopwatch stopwatch, double maxBurstSeconds)构造方法传进来的,这里离默认是1。

获取令牌

acquire(int)

acquire(int)方法在获取不到令牌时阻塞等待,直到获取到令牌。
获取令牌方法为RateLimiter#acquire(int):

// 从中 RateLimiter获取给定数量的令牌,阻塞直到请求可以被批准。告诉睡眠时间(如果有)
@CanIgnoreReturnValue
public double acquire(int permits) {
  long microsToWait = reserve(permits);
  stopwatch.sleepMicrosUninterruptibly(microsToWait);
  return 1.0 * microsToWait / SECONDS.toMicros(1L);
}

调用RateLimiter#reserve 获取需要等待的时间:

// RateLimiter 保留给定数量的令牌以供将来使用,并返回使用这些令牌需要等待的微秒数。
final long reserve(int permits) {
  checkPermits(permits);
  synchronized (mutex()) {
    return reserveAndGetWaitLength(permits, stopwatch.readMicros());
  }
}

调用RateLimiter#reserveAndGetWaitLength:

//	保留令牌并返回使用者需要等待的时间。
final long reserveAndGetWaitLength(int permits, long nowMicros) {
    long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
    return max(momentAvailable - nowMicros, 0);
}

调用RateLimiter#reserveEarliestAvailable:

abstract long reserveEarliestAvailable(int permits, long nowMicros);

是抽象方法,具体实现为SmoothRateLimiter#reserveEarliestAvailable:

// 更新下次可取令牌时间点与存储的令牌数,返回本次可取令牌的时间点
@Override
final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
    resync(nowMicros); // 如果nextFreeTicket小于当前时间,更新当前存储的令牌数,和下次可使用令牌的时间为now
    // nextFreeTicketMicros表示下一个可以分配令牌的时间点,这个值返回后,
    // 上一层的函数会调用stopwatch.sleepMicrosUninterruptibly(microsToWait);
    // 即阻塞到这个分配的时间点
    long returnValue = nextFreeTicketMicros;
    // 本次需要用掉的令牌数,取本次需要的和当前可以使用的令牌数的最小值
    double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
    // 需要用的令牌大于暂存的令牌数,计算需要新增的令牌数
    double freshPermits = requiredPermits - storedPermitsToSpend;
    // 计算补齐需要新增的令牌需要等待的时间
    long waitMicros =
    	storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
    	+ (long) (freshPermits * stableIntervalMicros);

    // 将下一个可分配令牌的时间点向后移动!!!!这里是实现与消费的关键
    this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
    // 更新当前存储的令牌数
    this.storedPermits -= storedPermitsToSpend;
    return returnValue;
}
// 如果nextFreeTicket小于当前时间,更新当前存储的令牌数,和下次可使用令牌的时间为now
void resync(long nowMicros) {
    // if nextFreeTicket is in the past, resync to now
    if (nowMicros > nextFreeTicketMicros) {
        double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
        storedPermits = min(maxPermits, storedPermits + newPermits);
        nextFreeTicketMicros = nowMicros;
    }
}

上面是获取令牌的关键方法:

tryAcquire(int,long,TimeUnit)

指定时间内尝试获取令牌,获取到或获取超时返回。

public boolean tryAcquire(int permits, long timeout, TimeUnit unit) {
    // 将timeout换算成微秒
    long timeoutMicros = max(unit.toMicros(timeout), 0);
    checkPermits(permits);
    long microsToWait;
    synchronized (mutex()) {
        long nowMicros = stopwatch.readMicros();
        // 判断是否可以在timeoutMicros时间范围内获取令牌
        if (!canAcquire(nowMicros, timeoutMicros)) {
            return false;
        } else {
            // 获取令牌,并返回需要等待的毫秒数
            microsToWait = reserveAndGetWaitLength(permits, nowMicros);
        }
    }
    // 等待microsToWait时间
    stopwatch.sleepMicrosUninterruptibly(microsToWait);
    return true;
}

private boolean canAcquire(long nowMicros, long timeoutMicros) {
    return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;
}

// 返回可用令牌的最早时间
abstract long queryEarliestAvailable(long nowMicros);

// SmoothRateLimiter#queryEarliestAvailable
final long queryEarliestAvailable(long nowMicros) {
	// 授予下一个请求(无论其大小如何)的时间
	return nextFreeTicketMicros;
}

该方法执行下面三步:

  1. 判断能否在指定超时时间内获取到令牌,通过 nextFreeTicketMicros <= nowMicros + timeoutMicros 是否为true来判断,即当前时间+超时时间 在可取令牌时间 之后,则可取(预消费的特性),否则不可获取。
  2. 如果不可获取,立即返回false。
  3. 如果可获取,则调用 reserveAndGetWaitLength(permits, nowMicros) 来更新下次可取令牌时间点与当前存储的令牌数,返回等待时间(逻辑与acquire(int)相同),并阻塞等待相应的时间,返回true。

存量桶系数

令牌桶算法中,多余的令牌会放到桶里。这个桶的容量是有上限的,决定这个容量的就是存量桶系数,默认为 1.0,即默认存量桶的容量是 1.0 倍的限流值。推荐设置 0.6~1.5 之间。
存量桶系数的影响有两方面:

  • 突发流量第一个周期放过的请求数。如存量桶系数等于 0.6,第一个周期最多放过 1.6 倍限流值的请求数。
  • 影响误杀率。存量桶系数越大,越能容忍流量不均衡问题。误杀率:服务限流是对单机进行限流,线上场景经常会用单机限流模拟集群限流。由于机器之间的秒级流量不够均衡,所以很容易出现误限。例如两台服务器,总限流值 20,每台限流 10,某一秒两台服务器的流量分别是 5、15,这时其中一台就限流了 5 个请求。减小误杀率的两个办法:
    • 拉长限流周期。
    • 使用令牌桶算法,并且调出较好的存量桶系数。

小结

RateLimiter 令牌桶的实现并不是起一个线程不断往桶里放令牌,而是以一种延迟计算的方式(参考resync函数),在每次获取令牌之前计算该段时间内可以产生多少令牌,将产生的令牌加入令牌桶中并更新数据来实现,比起一个线程来不断往桶里放令牌高效得多。(想想如果需要针对每个用户限制某个接口的访问,则针对每个用户都得创建一个RateLimiter,并起一个线程来控制令牌存放的话,如果在线用户数有几十上百万,起线程来控制是一件多么恐怖的事情)

优缺点

优点:

  • 放过的流量比较均匀,有利于保护系统。
  • 存量令牌能应对突发流量,很多时候,我们希望能放过脉冲流量。而对于持续的高流量,后面又能均匀地放过不超过限流值的请求数。

缺点:

  • 存量令牌没有过期时间,突发流量时第一个周期会多放过一些请求,可解释性差。即在突发流量的第一个周期,默认最多会放过 2 倍限流值的请求数。
  • 实际限流数难以预知,跟请求数和流量分布有关。

与漏桶的区别

  • 令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时,则拒绝新的请求。
  • 漏桶则是按照固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝。
  • 令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,或4个令牌), 并允许一定程度的突发流量。
  • 漏桶限制的是常量流出速率(即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2) , 从而平滑突发流入速率。
  • 令牌桶允许一定程度的突发,而漏桶主要目的是平滑流入速率。

总结

令牌桶算法是一种单机限流算法,已一定速率向桶中添加令牌,允许突发流量,支持预消费,预消费的等待时间由之后的请求承担。
当QPS小于100时,比较适合使用。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python爬虫时被封IP,该怎么解决?四大动态IP平台测评

在使用 Python 进行爬虫时&#xff0c;很有可能因为一些异常行为被封 IP&#xff0c;这主要是因为一些爬虫时产生的异常行为导致的。 在曾经的一次数据爬取的时候&#xff0c;我尝试去爬取Google地图上面的商家联系方式和地址信息做营销&#xff0c;可是很不幸&#xff0c;还只…

CloudPanel file-manager/backend/makefile接口存在远程命令执行漏洞CVE-2023-35885

@[toc] 免责声明:请勿利用文章内的相关技术从事非法测试,由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失,均由使用者本人负责,所产生的一切不良后果与文章作者无关。该文章仅供学习用途使用。 1. CloudPanel 简介 微信公众号搜索:南风漏…

【漏洞复现】Hikvision摄像头产品越权漏洞(CVE-2017-7921)

Nx01 产品简介 Hikvision&#xff08;海康威视&#xff09;是一家在中国颇具影响力的安防公司&#xff0c;其网络摄像头产品在市场上占据了相当大的份额。Hikvision的网络摄像头产品线非常丰富&#xff0c;涵盖了各种型号和功能&#xff0c;以满足不同用户的需求。 Nx02 漏洞描…

Spring DI

目录 什么是依赖注入 属性注入 构造函数注入 Setter 注入 依赖注入的优势 什么是依赖注入 依赖注入是一种设计模式&#xff0c;它通过外部实体&#xff08;通常是容器&#xff09;来注入一个对象的依赖关系&#xff0c;而不是在对象内部创建这些依赖关系。这种方式使得对象…

03-黑马程序员大数据开发:Apache Hive

一、 Apache Hive概述 1. 目的&#xff1a;&#xfeff;了解什么是分布式SQL计算&#xff1b;了解什么是Apache Hive 2. 使用Hive处理数据的好处 &#xfeff;操作接口采用类SQL语法&#xff0c;提供快速开发的能力&#xff08;简单、容易上手)&#xfeff;底层执行MapReduc…

第七回 林教头刺配沧州道 鲁智深大闹野猪林-FreeBSD/Linux图形界面安装配置

高俅定林冲&#xff1a;手持利刃&#xff0c;故入节堂&#xff0c;杀害本官的罪名&#xff0c;将林冲押解去开封府&#xff0c;暗示开封府将林冲处决。 开封府负责办案的叫孙定&#xff0c;他为人刚正不阿&#xff0c;宅心仁厚。在他的据理力争之下&#xff0c;开封府尹最终对…

【linux】ps的基本使用

ps是linux中用于显示进程的工具&#xff0c;确切来说是显示活动进程的工具 ps的基本格式是 ps [选项] sh-3.2# ps --help ps: illegal option -- - usage: ps [-AaCcEefhjlMmrSTvwXx] [-O fmt | -o fmt] [-G gid[,gid...]][-g grp[,grp...]] [-u [uid,uid...]][-p pid[,pid..…

windows下redis使用教程

创建临时服务 redis-server.exe redis.windows.conf启动客户端 验证 # 使用set和get命令&#xff0c;对Redis数据库进行数据存储和获取&#xff0c;如下图所示 config get *创建永久服务 关闭临时服务的cmd窗口&#xff0c;输入以下命令 redis-server.exe --service-insta…

【设计模式-08】Flyweight享元模式

简要说明 简要的理解&#xff1a;享元模式就是新建一个池(Pool)&#xff0c;该池子(Pool)中有新建好的一堆对象&#xff0c;当需要使用时&#xff0c;从池子(Pool)中直接获取&#xff0c;不用重新新建一个对象。通俗的讲就是&#xff1a;共享元数据。 比如Java中的String就是使…

Maven详解(入门到精通)学习maven有这个就够了

目录 1. Maven简介 2. 什么是Maven? 3. Maven的下载和安装 安装maven核心程序 4.Maven 核心概念 5. 第一个maven项目 创建约定的目录结构 6. 为什么创建约定的目录结构&#xff1f; 7. 基本的Maven命令 8. 关于联网下载的问题 9. 仓库 10. pom 11.坐标 12. 依赖初步认…

扎克伯格宣布将购买35万个GPU

Meta公司马克.扎克伯格1月18日在Instagram上发表文章称&#xff0c;该公司正在加强人工智能研究团队的力量&#xff0c;并在充实AI基础设施“弹药库“&#xff0c;计划在今年年底前向芯片设计商英伟达购买35万个H100 GPU芯片&#xff0c;从而使该公司的GPU总量达到约60万个&…

蓝桥杯练习题dfs与bfs

&#x1f4d1;前言 本文主要是【算法】——dfs与bfs的文章&#xff0c;如果有什么需要改进的地方还请大佬指出⛺️ &#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是听风与他&#x1f947; ☁️博客首页&#xff1a;CSDN主页听风与他 &#x1f304;每日一句&#xff…

璀璨2023,共赴2024——Tempo大数据分析产品年度回顾

随着2024年的到来&#xff0c;2023年已落下了帷幕&#xff0c;这一年里&#xff0c;Tempo大数据分析产品不断追求创新&#xff0c;进行了四次重要的版本升级。为用户带来新功能的同时确保用户在使用产品时获得卓越的体验感&#xff0c;从而更大程度地提升用户的工作效率。 现在…

使用Nginx和Fancyindex组合搭建文件下载站点详细教程

目录 简介 TIPS 1.下载Nginx 2. 安装Fancyindex和Nginx-Fancyindex-Theme模块 2.1 安装编译工具和依赖 2.2 下载Fancyindex和Nginx-Fancyindex-Theme 2.3 编译Nginx并包括Fancyindex 3. 配置Nginx 4.体验 4.1light主题 4.2dark主题 后记 简介 当使用Nginx和Fancyinde…

基于SpringBoot的欢乐校园管理系统

文章目录 项目介绍主要功能截图&#xff1a;部分代码展示设计总结项目获取方式 &#x1f345; 作者主页&#xff1a;超级无敌暴龙战士塔塔开 &#x1f345; 简介&#xff1a;Java领域优质创作者&#x1f3c6;、 简历模板、学习资料、面试题库【关注我&#xff0c;都给你】 &…

使用Python监听并下载微信聊天表情包

实现的功能 只要有人给你发了表情包&#xff0c;不管是群聊还是个人发的&#xff0c;都将它保存到本地。也许某天斗图的时候就能用到&#xff0c;不过即使有了表情包&#xff0c;还需要一个检索功能&#xff0c;不然这一张一张看也太费眼睛了。 检索表情包 检索表情包的功能…

Redis: Redis介绍

文章目录 一、redis介绍二、通用的命令三、数据结构1、字符串类型&#xff08;String&#xff09;&#xff08;1&#xff09;介绍&#xff08;2&#xff09;常用命令&#xff08;3&#xff09;数据结构 2、列表&#xff08;List&#xff09;&#xff08;1&#xff09;介绍&…

【C语言编程之旅 6】刷题篇-for循环

第1题 解析 思路&#xff1a; 两个循环进行控制 外层循环控制打印多少行 内部循环控制每行打印多少个表达式以及表达式内容&#xff0c; 比较简单&#xff0c;具体参考代码 #include <stdio.h> int main() {int i 0;//控制行数for(i1; i<9; i){//打印每一行内容&am…

FlinkAPI开发之处理函数

案例用到的测试数据请参考文章&#xff1a; Flink自定义Source模拟数据流 原文链接&#xff1a;https://blog.csdn.net/m0_52606060/article/details/135436048 概述 之前所介绍的流处理API&#xff0c;无论是基本的转换、聚合&#xff0c;还是更为复杂的窗口操作&#xff0c…

STL——list

1、list介绍 1. list 是可以在常数范围内在任意位置进行插入和删除的序列式容器&#xff0c;并且该容器可以前后双向迭代。 2. list 的底层是带头双向循环链表结构&#xff0c;双向链表中每个元素存储在互不相关的独立节点中&#xff0c;在节点中通过指针指向其前一个元素和后…