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

很多人第一次关心 Python 的垃圾回收,不是因为写了多复杂的代码,而是因为遇到了几个很实际的问题: del obj 之后,内存为什么没有立刻降下来? 明明没有全局变量引用某个对象,它为什么还活着? Python 不是有垃圾回收吗,为什么还会出现内存泄漏? gc.collect() 到底该不该在业务代码里手动调用? 这些问题背后其实是同一个机制:Python 的内存回收不是单一算法,而是几层机制一起工作。平时我们说的“Python 垃圾回收”,在最常见的 CPython 解释器里,主要由两部分组成: 引用计数:对象引用数变成 0 时,通常立刻释放。 循环垃圾回收器:专门处理引用计数解决不了的循环引用。 这篇文章主要讨论 CPython,因为日常用 python 命令启动的解释器,大多数情况下就是 CPython。PyPy、Jython、IronPython 等实现可以采用不同策略,所以不要把本文所有细节直接套到每一种 Python 实现上。 先从变量和对象的关系说起 在 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

请不要用 Redis 做任何缓存之外的事

Redis 是程序员工具箱里很容易被“过度信任”的东西。 它太顺手了。 想存个值,SET 一下。想自动过期,EXPIRE 一下。想做计数器,INCR 一下。想搞队列,LPUSH / BRPOP 一下。想搞锁,SET NX PX 一下。再看一眼 Stream,好像连消息系统也能顺手安排。 于是很多系统就这么一路滑坡: 第一天:Redis 只是缓存。 第二天:这个状态先放 Redis 吧。 第三天:队列也先放 Redis 吧。 第四天:库存扣减也先放 Redis 吧。 第五天:Redis 挂了,大家开始翻日志考古。 这篇文章想讲的观点很简单: Redis 能做很多事,但不要让它单独拥有真相。 如果一份数据不能丢、不能错、不能重复处理、不能悄悄过期、不能被内存策略淘汰,那它就不应该只待在 Redis 里。 Redis 最舒服的位置是缓存。它可以让系统跑得更快,但不应该决定系统还活不活得明白。 先看边界:缓存可以重建,事实不能只靠 Redis 先把话说在前面:这不是 Redis 不行。 Redis 很强,而且越来越强。它有 RDB,有 AOF,有复制,有 Sentinel,有 Cluster,有 Streams,还有很多高级数据结构。Redis 8 之后,JSON、Search、Time Series、概率数据结构等能力也被合进 Redis Open Source。 所以问题不是“Redis 能不能干更多活”。 问题是:你把活交给 Redis 之后,能不能接受它的失败方式。 缓存的失败方式很朴素: Redis 没了 -> 缓存 miss -> 回源数据库 -> 重新写缓存 这很烦,但不至于让业务当场失忆。 ...

May 23, 2026 · 3 min · Icyyan

Redis 底层数据结构:一个 key 背后到底藏着什么

很多人第一次学 Redis,记住的是五种常用类型:String、List、Hash、Set、ZSet。再往后背面试题,又会遇到 SDS、quicklist、listpack、intset、skiplist、hashtable。 这些名字放在一起很容易让人迷糊:Redis 不是 key-value 数据库吗?为什么一个 Hash 后面还会有 listpack 和 hashtable 两种实现?为什么同样是 Set,有时是 intset,有时又变成哈希表? 真正要理解的不是“Redis 有哪些数据结构”这张清单,而是一个更具体的问题: Redis 为什么要让同一种对外类型,在不同场景下切换不同的内部编码? 顺着这个问题往下看,Redis 的底层数据结构会清楚很多。它不是为了把实现弄复杂,而是在内存、CPU、查询速度、扩容成本之间做了一组很工程化的取舍。 本文写作和更新时的版本背景是:截至 2026 年 5 月 23 日,Redis Open Source 最新稳定版是 8.6.3,这是 2026 年 5 月发布的安全修复版本;Redis GitHub releases 里还能看到 8.8-RC1,但它是预发布版,官方说明不适合生产环境使用。 从底层数据结构这条线看,Redis 8.6 最值得关注的不是“又多背几个结构名”,而是几类更贴近日常排查的问题: 大 Hash 和大 ZSet 的内存占用继续被优化。 Stream 增加了 XADD IDMP / IDMPAUTO,用于幂等写入。 新增 HOTKEYS,能直接辅助定位热点 key。 新增 key 内存大小直方图相关能力,排查 List、Set、Hash、ZSet 的规模分布更方便。 新增 volatile-lrm 和 allkeys-lrm 淘汰策略,按“最近修改时间”参与淘汰。 旧版本里经常出现的 ziplist,在 Redis 7 之后大多已经被 listpack 替代,所以不要再只背“压缩列表”那套旧答案。 ...

May 23, 2026 · 6 min · Icyyan

布隆过滤器:用少量内存判断一个元素是否可能存在

如果一个接口被大量查询不存在的数据,比如用户不断请求不存在的商品 ID、缓存里没有的用户 ID,系统很容易被拖进一个尴尬局面:缓存没命中,请求继续打到数据库;数据库查不到,下一次同样的请求又会重复发生。 这类问题的关键不是“如何更快地查到数据”,而是先回答一个更便宜的问题:这个东西是不是一定不存在? 布隆过滤器解决的正是这个问题。它不能像哈希集合那样给出完全精确的答案,但它能用很少的内存,在极快的时间内告诉你: 如果结果是“不存在”,那它一定不存在。 如果结果是“存在”,那它只是可能存在。 这个“可能”就是布隆过滤器最重要的取舍。它用少量误判,换来了很高的空间效率。 先从一个普通集合说起 假设我们要记录 1 亿个已经注册过的用户 ID,最直接的做法是把这些 ID 放进一个集合: registered_users = set() registered_users.add("user:10086") if "user:10086" in registered_users: print("exists") 这个方案很好理解,也很精确。但问题是:集合为了支持快速查询,通常不只存储原始元素,还要维护哈希表、桶、指针、扩容状态等额外结构。数据量一大,内存会明显上去。 如果我们的目标只是提前过滤掉明显不存在的请求,真的需要保存完整的字符串吗? 布隆过滤器的答案是:不需要。它只保存一些 bit。 布隆过滤器的直觉 可以把布隆过滤器想成一个很大的位图,也就是一串只包含 0 和 1 的数组: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 普通位图通常要求元素能直接映射成整数位置,比如数字 13 就对应第 13 个 bit。但现实里的元素可能是字符串、URL、邮箱、订单号,不一定能直接当数组下标。 布隆过滤器在中间加了一层哈希函数。 对于一个元素,它不用一个位置表示,而是用多个哈希函数算出多个位置: element = "user:10086" hash1(element) -> 3 hash2(element) -> 9 hash3(element) -> 14 插入这个元素时,就把这些位置都置为 1: ...

May 21, 2026 · 5 min · Icyyan

位图 Bitmap:用一个 bit 记录海量状态

如果让你记录“今天哪些用户登录过”,最直接的想法可能是放进一个集合: logged_in_users = set() logged_in_users.add(10086) 这样当然能用。但如果用户 ID 范围很大、查询又特别频繁,集合会带来不少额外开销。有没有一种更省空间的办法? 位图,也就是 Bitmap,解决的就是这类问题:用一个 bit 表示一个状态。这个状态通常只有两种结果:有或没有、是或否、出现过或没出现过。 位图到底是什么 先从一个字节说起。 一个字节有 8 个 bit: 0 0 0 0 0 0 0 0 每个 bit 都可以表示一个编号是否存在。比如我们用 bit 记录数字是否出现过: bit 0 表示数字 0 是否出现 bit 1 表示数字 1 是否出现 bit 2 表示数字 2 是否出现 ... bit 7 表示数字 7 是否出现 如果数字 3 出现过,就把第 3 个 bit 置为 1: 0 0 0 0 1 0 0 0 ↑ 数字 3 当编号超过 7 时,就放到下一个字节里。比如数字 13: ...

May 18, 2026 · 3 min · Icyyan

Pingora 深度解析:为什么 Cloudflare 用 Rust 重写了 Nginx 时代的代理架构

Cloudflare 公开 Pingora 时,最抓眼球的数字是:在相同流量下,新的代理系统比旧系统少用了约 70% CPU 和 67% 内存。 看到这个数字,很多人的第一反应可能是:是不是 Nginx 不行了?是不是 Rust 天生就比 C 快很多? 都不是。 我们真正要回答的是:Cloudflare 为什么会觉得原来的 NGINX/OpenResty 代理不够用了?Pingora 又改掉了哪些最贵的地方? 后面就顺着这个问题往下走。先看 Pingora 是什么,再看 Nginx 在普通场景里为什么很好用,最后再看 Cloudflare 这种规模下,连接池、线程模型和业务逻辑会怎样把成本放大。 可以先带着三句话往下读,后面会一层层展开: Cloudflare 替换的是他们内部深度改造过的 NGINX/OpenResty 代理,不是说 Nginx 在所有场景下都慢。 Pingora 开源版更像“用 Rust 写代理服务的工具箱”,不是一个直接读取 nginx.conf 的 Nginx 替代品。 重点不是“Rust 打败 Nginx”,而是连接复用、线程共享和复杂代理逻辑这些具体问题。 1. Pingora 不是一个新的 Nginx 如果你平时用 Nginx,可能会自然地把 Pingora 想成“另一个服务器软件”。但这样理解会有点偏。 Pingora 更像一套用 Rust 写代理服务的工具箱。它帮你处理连接、TLS、HTTP 协议、负载均衡、连接池、优雅升级这些通用能力;你的业务逻辑则写在 Rust 回调里。 它不是单个二进制 Web 服务器,而是一组 crate: crate 作用 pingora-core 底层协议、服务运行、监听器、连接器、优雅升级 pingora-proxy HTTP 代理生命周期和 ProxyHttp 回调接口 pingora-pool 高并发连接池 pingora-runtime Tokio runtime 封装,支持 work-stealing 与 no-steal 模式 pingora-load-balancing 负载均衡、健康检查、选择算法 pingora-cache / pingora-memory-cache 缓存相关能力 这些名字不用急着都记住。先有一个印象就够了:Pingora 把“写代理服务”拆成了一组可以组合的 Rust 库。 ...

May 17, 2026 · 5 min · Icyyan