Redis 常用命令时间复杂度:哪些操作会悄悄拖慢实例

Redis 很快,但“快”并不等于所有命令都是 O(1)。 一次 GET 通常很轻,一次 ZRANGE 可能返回十万个成员,一次 SINTER 可能扫描几个大集合,一次 DEL 也可能为了释放大 key 在主线程上忙很久。它们都只是一条 Redis 命令,对实例造成的压力却完全不同。 这也是线上 Redis 延迟偶尔尖刺时最容易被忽略的地方:QPS 没有明显上涨,CPU 看起来也不算很高,但某个请求做了一次与数据规模相关的 O(N) 操作,后面的请求就一起排队了。 这篇文章按 Redis 的常用数据类型整理时间复杂度。目的不是背完整张命令表,而是学会看到命令时迅速判断两件事:它会扫描多少数据,又会返回多少数据。 如果还不熟悉 Hash、quicklist、intset、listpack 和 skiplist 的关系,可以先看Redis 底层数据结构。底层结构正是这些复杂度的来源。 读复杂度之前,先认识几个变量 后面的表格会反复使用这些字母: 变量 含义 K 一次命令传入的 key 数量 N 当前集合、列表、Hash 或 ZSet 的元素总数 M 本次返回、删除或处理的元素数量 S 为了到达起始位置需要跨过的元素数量 B 需要读取、复制或传输的字节数 比如 ZRANGE rank 0 9 的复杂度是 O(log N + M):先在有序结构中定位起点,再返回 M 个成员。排行榜里有一百万个人并不可怕,只取前十名依然很轻;真正危险的是 ZRANGE rank 0 -1,因为这时 M 也变成了一百万。 还有三个容易被大 O 记号藏起来的事实。 ...

September 4, 2026 · 6 min · Icyyan

Redis Pipeline 与 Lua:减少网络往返和保证原子性是两回事

假设下单时需要在 Redis 里做三件事:读取库存、判断是否足够、扣减库存。 第一次写这段逻辑,很容易想到 Pipeline:把几条命令一次发给 Redis,既然它们挨在一起执行,是不是就不会被其他请求插进来了? 答案是否定的。 Pipeline 解决的是网络往返太多,Lua 脚本解决的才是多步逻辑不能被其他命令打断。两者都能让客户端“一次请求完成多件事”,但它们处理的是完全不同的问题。 理解这一区别之后,Redis 里很多看似相近的概念都会顺起来:为什么批量写缓存适合 Pipeline,为什么扣库存适合 Lua,为什么事务和 Pipeline 经常同时出现在客户端 API 里,以及“Lua 是原子的”到底原子到什么程度。 先用一张表分清四种工具 工具 主要解决什么 多条命令会不会被其他客户端插入 能否根据中间结果继续判断 Pipeline 减少 RTT,提高吞吐 不保证 不适合 MULTI/EXEC 原子执行一组已经确定的命令 不会 很弱,命令只是先排队 WATCH + MULTI/EXEC 乐观锁与条件更新 提交后不会,冲突时提交失败 可以,但通常需要重试 Lua 脚本 在服务端完成读、计算、写 不会 可以 还有一个经常被忽略的选择:如果 Redis 已经有单条命令能完成需求,优先用单条命令。例如计数用 INCRBY,不存在才写入用 SET ... NX。单命令通常比自己拼事务或 Lua 更简单,也天然具有原子性。 Pipeline 为什么快:它省掉的是 RTT 客户端执行一条 Redis 命令,大致要经历下面这段路: 客户端发送命令 -> 网络传到 Redis -> Redis 执行命令 -> 结果通过网络返回 -> 客户端收到结果 如果依次执行 100 条命令,客户端通常要等待 100 次网络往返。即使命令本身只执行几十微秒,网络等待也可能占掉大部分时间。 ...

September 4, 2026 · 4 min · Icyyan

给本地服务配上 HTTPS:certbot DNS-01 签发 + Pingap SNI 实战

在上一篇里,Marinara 跑在 127.0.0.1:7860,通过 Pingap 用 http://IP 对外提供访问。这样能用,但有一个难受的地方:Basic Auth 的账号密码是在明文 HTTP 里传的。所以目标很明确:给它配上 https://marinara.icyyan.com。 要做的事情拆开来就两件:证书从哪来,证书怎么挂到反向代理上。好消息是这台服务器上已经有两个站点(cloudside.icyyan.com、silly.icyyan.com)跑着 HTTPS,整套体系是现成的——新站点照抄就行。这篇文章把整个过程走一遍,包括中间踩掉的几个坑。 1. 先看现成的体系长什么样 照抄之前先弄清楚抄的是什么。服务器入口是 Pingap,HTTPS 这一块的结构是: 443 端口只监听一次,靠 SNI 分流。SNI 是 TLS 握手时客户端告诉服务器「我要访问哪个域名」,服务器按这个域名选对应的证书、再按域名找到对应的 location 转发。所以多个 HTTPS 站点可以共用同一个 443。 每个域名一张 Let’s Encrypt 证书,统一放在 /etc/letsencrypt/live/<域名>/ 下,都是 certbot 签的。 签证书用的验证方式是 dns-aliyun 插件,这是整个过程里最关键的一环,值得单独说。 2. 为什么用 DNS-01 挑战 CA 签证书之前,必须先确认「你确实控制这个域名」,这个确认动作叫挑战(challenge)。最常见的 HTTP-01 挑战是让 CA 从公网访问 http://<域名>/.well-known/... 下的一个文件——这要求域名已经解析到服务器、80 端口可用。 DNS-01 换了一条路:只要能在这个域名的 DNS 里写下一条指定的 TXT 记录,就证明你控制了整个域。阿里云的 DNS 有 API,certbot 的 dns-aliyun 插件会自动完成「写 TXT 记录 → 等它生效 → 让 CA 来查 → 删掉记录」的全过程,人不用插手。 ...

July 22, 2026 · 3 min · Icyyan

架构演进的六个时代:从原始分布式到无服务

作为后端程序员,架构知识通常是一点一点在工作中捡的:数据一致性踩坑了,知道了事务的重要性;服务改用 K8s 部署,真切体会到了声明式 API 给运维带来的便利;性能遇到瓶颈了,加上缓存系统。每块都懂一点,但这些知识之间是什么关系,为什么要这么设计,一直没有形成体系化的思考,直到读到周志明老师的《凤凰架构》。这本书完整地梳理了服务端的知识地图,将 why 和 what 的问题讲得非常清楚,给了非常清晰的宏观视角。 本文是第一章的总结和读后感:每一代架构风格的诞生,都是因为上一代遇到了它解决不了的具体问题。这些问题的性质各不相同,但贯穿始终的暗线是复杂度 —— 复杂度不会消失,只会转移。 原文出处:微信公众号「张煜中」《架构演进的六个时代:从原始分布式到无服务》,作者张煜中。本文基于该文整理,作为个人读书笔记。 原文内容 原始的分布式 通常我们会认为,服务架构是从单体开始的,为了逃离单体大泥球架构的地狱,才搞分布式。但历史上恰恰相反。对分布式架构的探索,从 20 世纪 70 年代就开始了。那时单机算力极其有限,16 位处理器、不到 5MHz 的主频,单机直接卡住了软件能做到的规模上限。人们不得不寻找多台计算机协作支持一套软件系统的方案。 UNIX 设计风格强调:保持接口与实现的简单性,比系统的任何其他属性,包括准确性、一致性和完整性,都来得更加重要。 理想很美 —— 远程调用应该尽可能透明,开发者无需关心自己调的是本地方法还是远程服务。但一旦触碰到「远程」二字,网络的不确定性便会带来相当的复杂度。远程的服务在哪里(服务发现),有多少个(负载均衡),网络出现分区、超时或者服务出错了怎么办(熔断、隔离、降级),方法的参数与返回结果如何表示(序列化协议),信息如何传输(传输协议),服务权限如何管理(认证、授权),如何保证通信安全(网络安全层),如何令调用不同机器的服务返回相同的结果(分布式数据一致性)—— 每一个都需要设计者耗费大量精力。 这些探索催生了 RPC、DFS 等概念,人们也得到了一个价值千金的教训:某个功能能够进行分布式,并不意味着它就应该进行分布式,强行追求透明的分布式操作,只会自寻苦果。 原始分布式时代的故事,是一次发现复杂度的过程。探索者试图用「透明调用」把分布式的复杂度屏蔽掉,让开发者像写本地程序一样写分布式程序。但现实证明,网络带来的不确定性是无法假装不存在的 —— 服务发现、一致性、网络分区,这些问题不会因为你不看它就消失。这次失败的意义不在于产出了什么可用的系统,而在于让整个行业认清了分布式复杂度的真实面貌。 于是当硬件性能随摩尔定律起飞后,人们做了一个务实的选择:既然分布式的复杂度屏蔽不了,那就别分布式了。 单体系统时代 软件退回到单体 —— 所有代码跑在同一个进程里,不用想网络,不用想一致性。 大型的单体系统,经常是微服务书籍批判的对象。但一定要注意这里的定语 —— 「大型的」。小型单体系统,不仅易于开发、测试、部署,且由于系统中各个功能、模块、方法的调用过程都是进程内调用,没有进程间通信,运行效率也很高。三个人的团队、一台机器撑得住的系统,搞微服务纯粹是给自己找麻烦。 单体系统的缺陷在于,缺乏自治和隔离能力。进程内调用虽然简单高效,但故障也难以隔离,某个模块的 bug 能导致整个系统崩溃。而在大型系统中,出错几乎是必然的 —— 大型系统意味着多人协作、频繁变更,缺乏隔离就意味着一个模块的内存泄漏能拖垮整个进程,一次局部的代码升级需要整体停机重启。 单体系统的设计哲学是「让每一部分都尽量不出错」,靠高质量来保证高可靠。但系统越大,出错越是必然。从「追求不出错」到正视「出错是必然」的观念转变,才是微服务架构得以挑战单体的底气所在。 单体时代面对的不是分布式复杂度 —— 它根本没有分布式。它面对的是规模带来的复杂度,而这种复杂度体现在多个维度上:可维护性(一次局部改动需要整体停机重启)、团队协作(多人改同一个代码库,互相踩脚)、可靠性(单点故障拖垮全局)。系统小的时候,这些问题都不存在;但规模一旦上去,它们会同时爆发,而且在单体架构下无解 —— 因为缺乏隔离和自治能力。 为了获得这种能力,人们再次走向分布式。 SOA 时代 但在微服务之前,业界走过一段弯路 —— SOA。 SOA 的野心极大,它不仅要解决技术问题,还想建立一套自上而下的软件研发方法论:如何挖掘需求、如何分解业务、如何编排服务,一揽子全包。它有 IBM、Oracle 等巨头撑腰,有 SOAP 协议族做底座,有企业服务总线(ESB)做通信管道,从技术可行性上看确实解决了分布式的主要问题。 但问题恰恰出在「太完美」上。过于精密的规范带来过度的复杂性,SOAP 之上层层叠加的 ESB、BPM、SCA、SDO,让整个技术栈变成了只有少数专业人员才能驾驭的奢侈品。 SOA 与 EJB 的失败如出一辙,一旦脱离人民群众,终究会淹没在群众的海洋之中。 ...

July 3, 2026 · 1 min · Icyyan

分布式事务:TCC、Saga、可靠消息、2PC、Seata AT 模式

分布式事务的方案选型我自己查过好几轮,这篇把 TCC、Saga、可靠消息、2PC、Seata AT 各自的原理和最适合的场景讲得比较清楚,整理一份存档。 在微服务架构下,跨服务的数据一致性保证是个绕不开的难题。如果在一次业务操作中,写多个 DB,或既写本地 DB、又调外部 RPC 接口,就会面临一致性问题,怎么保证跨库、跨系统的操作要么都成功、要么都失败呢? 先说结论,最常用的分布式事务方案是:TCC、Saga、可靠消息。最推荐的开源框架是 Apache Seata,支持多种分布式事务模式,包括 TCC、Saga、AT 等。下面展开介绍。 BASE 理论 在一个数据库事务内,多个 SQL 写操作可以保证强一致。而到了分布式场景,只能做到最终一致。这就引出了 BASE 理论:基本可用、软状态、最终一致性,即允许系统存在中间不一致状态,只要最终达到一致即可。分布式事务方案大多在 BASE 理论指导下,放弃强一致,追求最终一致。 主流方案和最佳场景 下面每种方案都用一个独立的、最适合的业务场景来介绍。 1. TCC 最佳场景:下单时扣库存、扣余额 比如下单流程,涉及订单服务、库存服务、支付服务。需要同时创建订单、冻结库存、冻结余额,并发量较高,且对一致性要求严格。 原理与实现机制 TCC 需要一个协调者,一般称为事务管理器,负责编排整个流程。但资源锁定做到了业务层、而非 DB 层,由应用代码显式实现 Try、Confirm、Cancel 三个接口。 TM(事务管理器):负责调用各个服务的 Try 方法,根据结果决定全部 Confirm 或全部 Cancel。需记录事务日志,处理重试和异常。 RM(资源管理器):各业务服务自身,需要提供 Try/Confirm/Cancel 三个接口,并保证幂等(可能会调多次)。 流程细节 TM 依次调用每个服务的 Try 接口,预留业务资源(如冻结库存、冻结余额),不用上数据库行锁。 若所有 Try 成功,TM 依次调用 Confirm 接口,将预留资源实际消耗或状态推进。 若任何 Try 失败或超时,TM 调用所有已成功服务的 Cancel 接口,释放预留资源。 框架需要处理空回滚(Try 未执行但 Cancel 被调用)、悬挂(Cancel 比 Try 先到)、幂等(接口被重复调用)等问题,一般通过记录全局事务状态,并用全局事务 ID 查询状态来实现。 举例:库存服务 Try/Confirm/Cancel ...

July 3, 2026 · 3 min · Java烘焙师

吃透池化技术:内存池与对象池,高性能服务的底层基石

这篇是搬运整理的文章(原作者见文末)。最近在补池化技术这块的基础,这篇是我读到讲得最透的一篇:从 malloc 一行代码背后的成本,一路讲到内存池、对象池的实现、压测方法和上线 checklist,整理一份存档。 很多人一听池化技术,马上想到线程池、连接池。再深入一点,知道内存池、对象池。再往后呢? 内存池和对象池到底是不是一回事? 什么情况下池化有用? 内存池和对象池到底差在哪? 为什么 malloc/new 看着就一行代码,跑起来却可能这么贵? 为什么有些系统池化后更快,有些系统池化后更慢,甚至更容易 OOM? 自己写一个又该注意什么?上线前怎么压测,出了问题怎么查? 这篇文章就聊这个。 高性能服务的核心奥义,从来不是压榨CPU算力,而是尽可能减少无效的资源消耗。而池化技术,就是解决这类问题最核心、最通用的工程手段。 一、池化技术的背景与价值 1.1 从 malloc 和 new 开始看资源分配的成本 我们平时写代码,调用 new 创建对象、malloc 申请内存,看起来只是一行简单代码的事。但你调用的那一刻,你知道这行代码背后,操作系统和运行时做了多少事吗? 上层代码看似无成本,底层全是隐形开销。 C/C++ 的 malloc 并不会直接向操作系统申请内存。用户态会先在进程内存池中截取空闲内存,一旦本地内存不足,才会触发系统调用 brk 或 mmap。 系统调用是什么概念?会陷入内核态,打断CPU流水线,触发上下文切换,这个开销远比普通代码执行高几十倍。 1.2 频繁创建和销毁问题 频繁分配释放带来的问题远不止把系统拖慢。 第一是锁竞争。 在多线程环境下,malloc/free通常需要加锁。线程越多,竞争越激烈。ptmalloc在这种场景下的表现堪称灾难。 **第二是 CPU 成本。**每次分配都要做元数据维护,每次释放也要做回收处理。小对象多了以后,这些成本会被放大。 第三是内存碎片。 你分配释放的节奏不一样,内存就被切得七零八落。明明还有几百兆空闲,但就是分配不出一块连续的大内存。内碎片是你用了5字节但分配器给了你8字节,那3字节就浪费了;外碎片是空闲内存到处都是,但谁也不挨着谁。 第四是GC压力。托管语言里,对象创建便宜不代表没有代价。分配很快,回收也得有人买单。 第五是外部资源成本。有些对象不只是内存里的一个结构。比如数据库连接、TCP 长连接、TLS 会话、压缩器、序列化上下文。创建它们可能要走网络握手、认证、初始化缓冲区,甚至触发系统调用。你每次用完就销毁,这是在烧钱。 1.3 池化技术的基本思路 既然频繁创建销毁这么慢,那能不能换个思路? 不用每次需要资源都去重新申请,不用每次用完都立刻销毁。 这就是池化技术的核心逻辑: 预分配。系统启动时或首次使用时,提前申请一批资源。 复用。资源用完后不立即销毁,而是清理状态后放回池子。 集中管理。资源的创建、分配、归还、销毁都经过统一组件,方便限流、监控和保护系统。 这套思路听起来不复杂,但真正写起来就不一样了。池子满了怎么办?没人还怎么办?还回来的对象状态脏了怎么办?多线程同时借对象怎么办?长期空闲的资源要不要销毁?超过容量时阻塞还是失败? 池化技术说白了,就是用一次性的固定内存开销,换取长期稳定的高性能收益。 1.4 内存池与对象池在性能优化体系中的定位 最底层是内存池,聚焦操作系统内存分配优化,解决内存碎片、系统调用开销问题,属于基础设施级优化。 中层是对象池,聚焦业务对象实例复用,解决对象初始化、资源绑定的重复开销,属于业务框架级优化。 上层是线程池、连接池,聚焦线程、网络连接这类重型资源复用,属于服务调度级优化。 内存池和对象池,是所有池化技术的基石。底层优化到位,上层所有复用逻辑才能稳定高效运行。 二、池化技术的核心思想与适用边界 2.1 池化的本质 池化的本质就八个字:空间换时间,集中控资源。。 ...

July 2, 2026 · 6 min · Debug 蟹老板

在阿里云 ECS 上最小化部署 Marinara Engine:不在服务器上跑本地模型

Marinara Engine 的官方介绍里把它称为一个本地 AI 聊天、角色扮演和游戏引擎。听起来很容易让人以为:既然是“本地”,是不是还得在服务器上准备 GPU、下载模型、跑 llama.cpp 或者 embedding 服务? 如果只是想把 Marinara 当成一个 Web 应用来用,答案是不需要。我的阿里云 ECS 现在跑的就是这种最小化部署:Docker 里只跑 Marinara Engine 本体,模型能力走外部 API,容器端口只绑定到 127.0.0.1,再由反向代理对外提供访问。 本文记录的是这个部署方式。版本核对时间是 2026-07-21:官方最新稳定版是 v2.3.3,我当前服务器上实际运行的是 ghcr.nju.edu.cn/pasta-devs/marinara-engine:2.0.5-lite。新部署可以优先用最新稳定版的 *-lite tag;如果你想完全复刻本文环境,就固定到 2.0.5-lite。 先理解这个“最小化”是什么意思 最小化不是功能阉割到不能用,而是把不适合小云服务器承担的部分拿掉。 Marinara Engine 官方提供 lite 镜像。根据官方容器安装文档,lite 镜像去掉了四类较重的离线能力: 本地 sidecar 模型,也就是容器内直接跑本地 LLM 的那部分。 本地 embedding 模型。 依赖本地 embedding 的语义记忆检索。 本地 Whisper 语音输入,也就是 Conversation 通话的语音转文字。 保留下来的能力仍然包括聊天、角色、游戏模式、agent、lorebook、角色卡、远程 LLM API 连接等。也就是说,只要你本来就打算用 OpenAI、OpenRouter、Gemini、Anthropic 或者其他远程 OpenAI-compatible API,这种部署方式就够了。 我这里的结构是: 浏览器 -> 阿里云安全组开放 80/443 -> Pingap / Nginx / Caddy 这类反向代理 -> 127.0.0.1:7860 -> Docker 容器内的 Marinara Engine -> 远程 LLM / 图片 / TTS API 看到这里,问题就变成了:服务器上到底需要跑什么?答案很少:Docker、一个 compose 文件、一个 .env 文件、一个反向代理入口。 ...

June 29, 2026 · 3 min · Icyyan

tar 不是压缩工具:一篇讲透 Linux 归档与压缩的设计哲学

这篇是搬运整理的文章(原作者见文末)。tar 我用了没有十年也有八年,但「tar 本身不压缩」这件事确实一直没细想过,这篇把归档和压缩的关系讲得又全又清楚,整理一份存档。 你敲了十年 tar -czf,可能从没意识到一个事实:tar 本身一个字节都不压缩。 tar 不是压缩工具——它只负责把一堆文件"归档"成一条字节流,压缩只是顺手套上的外衣。本文以"一条流从生成到还原"为主线,从查看、打包、解压三件日常事,讲到流式传输、增量备份、可复现归档等工程用法,带你重新理解这个最熟悉却最被误解的 Linux 命令。 一、先看懂 tar 在干什么 在敲任何命令之前,先建立一个能贯穿全文的认知。 1. tar 是"归档器",压缩只是外挂 tar 的全称是 tape archive(磁带归档)。它只干一件事——把一堆文件和目录"拼接"成一条连续的字节流(一个 .tar 文件)。真正的压缩,是 -z(gzip)、-J(xz)、--zstd(zstd)这些参数顺手调用的外部压缩程序干的。 把这句话刻进脑子,后面一切都顺了: tar 干的事,就是把一组文件变成"一条流",再把这条流安全地还原回去。压缩,只是给这条流套的一件"外衣"——可有可无,可以随时换。 2. 一张地图:用 tar 无非在回答五个问题 既然 tar 的全部工作都围绕"一条流",那么你这辈子敲的每一条 tar 命令,本质都在回答下面五个问题之一。这五个问题,就是本文的主线: 往流里装什么?(选哪些文件、要不要权限属性、排除什么) 怎么把流造出来?(打包) 怎么不拆开就读这条流?(查看) 怎么让流动起来、长期存活?(传输、备份) 怎么把流拆开、并且信得过它?(解压、校验、安全) 而"压缩“这件外衣,横跨上面每一站。下面我们就从最日常的操作,一路走到工程级用法。 3. 先拿到钥匙:读懂所有 tar 命令的骨架 动手之前先记住这个骨架,之后任何 tar 命令你都能一眼拆解、也不会写错: 动作 + 参数 + 包名 + 源/目的地路径 动作:-c(Create 造包)/-x(eXtract 拆包)/-t(lisT 查看),三选一,互斥。 参数:要不要压缩(-z/-J/--zstd)、详细输出(-v)等。 包名:永远跟在 -f 后面。 源/目的地:打包时是源目录,解包时是目标目录(-C 指定)。 这里有一个最容易踩、却最实用的顺序规则:包名永远紧跟 -f,并且排在源/目标路径之前——不能调换。 因为 -f 的"参数"就是包名,所以无论造包还是拆包,都是先写包名、再写源目录或目标目录: ...

June 20, 2026 · 5 min · 张诚

Python 垃圾回收机制:引用计数、循环引用和分代 GC

很多人第一次关心 Python 的垃圾回收,不是因为写了多复杂的代码,而是因为遇到了几个很实际的问题: del obj 之后,内存为什么没有立刻降下来? 明明没有全局变量引用某个对象,它为什么还活着? Python 不是有垃圾回收吗,为什么还会出现内存泄漏? gc.collect() 到底该不该在业务代码里手动调用? 这些问题背后其实是同一个机制:Python 的内存回收不是单一算法,而是几层机制一起工作。平时我们说的“Python 垃圾回收”,在最常见的 CPython 解释器里,主要由两部分组成: 引用计数:对象引用数变成 0 时,通常立刻释放。 循环垃圾回收器:专门处理引用计数解决不了的循环引用。 这篇文章主要讨论 CPython,因为日常用 python 命令启动的解释器,大多数情况下就是 CPython。PyPy、Jython、IronPython 等实现可以采用不同策略,所以本文细节不要直接套到其他实现上。 先从变量和对象的关系说起 在 Python 里,变量不是装对象的盒子,更像是贴在对象上的名字。 a = [1, 2, 3] b = a 这段代码里没有创建两个列表。它创建了一个列表对象,然后让 a 和 b 都指向它: a ─┐ ├──> [1, 2, 3] b ─┘ 执行: del a 删除的也不是列表对象本身,而是删除名字 a 到列表对象的那条引用。因为 b 还指向这个列表,所以对象仍然活着: b ───> [1, 2, 3] 这就是理解 Python 垃圾回收的入口:对象什么时候能被回收,取决于还有没有地方能继续访问它。 del 的含义也要顺手纠正一下:del name 删除的是名字绑定;del obj.attr 删除的是属性引用;del some_list[i] 删除的是容器里的一个引用。它们都不等于“立刻把某块内存还给操作系统”。 ...

June 20, 2026 · 5 min · Icyyan

Rust 里的 Box 到底是什么

学 Rust 时,Box<T> 很容易被一句话带过:它可以把数据放到堆上。 这句话没错,但如果只记住这一句,后面看到 Box<dyn Trait>、Box<List>、Pin<Box<T>>、Box::leak 时,还是会觉得它像一个突然冒出来的语法补丁。 更好的理解方式是:Box<T> 不是“逃离所有权系统”的工具,而是 Rust 所有权系统里最基础的一种拥有型指针。它让一个值住在堆上,同时让所有权仍然清清楚楚地归某个变量管理。 先看 Box 解决了什么问题 普通变量通常可以这样理解: let n = 42; n 这个值本身就放在当前栈帧里。栈很快,进入函数时分配,函数返回时回收,生命周期也很清楚。 但有些场景光靠栈不够舒服: 一个类型递归地包含自己,编译器算不出它的大小。 你想把不同具体类型放进同一个集合,只要求它们实现同一个 trait。 一个值很大,你希望移动时只移动一个指针。 某些 API 需要一个固定地址的拥有型对象,比如配合 Pin 使用。 这时 Box<T> 就出现了。 let n = Box::new(42); 可以把它粗略想成这样: 栈上变量 n -> 保存一个指针 -> 指向堆上的 42 Box<T> 本身在栈上,里面保存指向堆数据的指针。真正的 T 在堆上。变量离开作用域时,Box<T> 会自动释放堆上的 T。 所以 Box<T> 同时有两个特点: 它是指针,可以间接访问堆上的值。 它拥有这个值,离开作用域时负责释放它。 这和 C 里的裸指针很不一样。你不用手写 free,也不能随便复制出多个拥有者。Rust 仍然会检查所有权、移动和借用。 最基本的用法:Box::new 创建一个 Box<T> 最常见的方式是 Box::new: fn main() { let name = Box::new(String::from("cloudside")); println!("{name}"); } 这里 String 这个值由 Box 放到堆上管理,name 是一个拥有它的 Box<String>。注意,String 自己内部还会管理一块字符串缓冲区;这里说的是 String 这个结构本身(内部是指针、长度、容量三个机器字)的位置。 ...

May 24, 2026 · 5 min · Icyyan