请不要用 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 年 7 月,版本信息核对于 7 月 21 日),Redis Open Source 的最新稳定版是 8.8.0,2026 年 5 月 25 日 GA,预发布阶段亮相的新数据结构 Array 也在这一版正式落地;8.6 线的最新补丁是 8.6.4。 从底层数据结构这条线看,Redis 8.6 以来最值得关注的不是“又多背几个结构名”,而是大 Hash、大 ZSet 的内存继续被优化,以及 Stream 幂等写入、HOTKEYS、key 内存大小直方图、LRM 淘汰策略这些贴近日常排查的新能力。这些在后文对应的小节里都会具体展开。 对外类型和内部编码不是一回事 平时我们写 Redis 命令,面对的是对外类型: SET name bole LPUSH queue a b c HSET user:1 name bole age 18 SADD tags redis cache ZADD rank 100 alice 这些命令分别对应 String、List、Hash、Set、ZSet。但 Redis 在内存里不会只保存“这是一个 Hash”这么简单的信息。 ...

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 这里约定 bit 从右往左数:最右边是 bit 0,往左依次是 bit 1、bit 2……和二进制数的书写习惯一致。 ...

May 18, 2026 · 3 min · Icyyan

用 Pingap 部署反向代理,以及从 Pingora 迁移时容易踩的坑

如果你已经看过 Pingora 的最小反向代理示例,很容易产生一个后续问题:生产环境是不是也要自己继续写配置解析、证书加载、日志、热更新和管理界面?通常不需要。Pingap 就是基于 Pingora 做好的反向代理应用,它更接近一个可以直接部署的 Nginx 替代方案。 这篇文章只讲 Pingap:怎么安装,怎么写一个最小反代配置,怎么加 HTTPS,怎么用 Docker 或 systemd 部署,以及从裸 Pingora 或 Nginx 迁移过来时有哪些容易忽略的坑。 本文示例版本核对时间:2026-05-17。示例固定使用 Pingap v0.13.4。实际部署前建议再看一次官方 GitHub Releases 和文档,因为 Pingap 还在快速变化。 Pingap 和 Pingora 的关系 Pingora 是框架,你通过 Rust 代码实现代理逻辑。Pingap 是应用,它把常见反向代理能力包装成 TOML 配置和 Web 管理界面。 一个最小 Pingap 配置大概长这样: [upstreams.app] addrs = ["127.0.0.1:3000"] [locations.app] host = "app.example.com" path = "/" upstream = "app" enable_reverse_proxy_headers = true [servers.http] addr = "0.0.0.0:6188" locations = ["app"] 这段配置和一个最小 Pingora 代理做的是同一件事:监听 6188,把匹配 app.example.com 的请求转发到 127.0.0.1:3000。差别在于,你不用实现 ProxyHttp,也不用自己处理常见代理应用需要的周边能力。 ...

May 17, 2026 · 5 min · Icyyan

用 Pingora 写一个最小反向代理,并把它部署起来

Cloudflare 开源 Pingora 之后,很多人会问:它能不能替代 Nginx?如果问题只是“能不能把请求转发到后端服务”,答案是能,而且几十行 Rust 就够了。但 Pingora 不是一个读取 nginx.conf 的服务器,它更像一套用来写代理、网关和网络服务的 Rust 框架。 这篇只做一件事:用 Pingora 写一个最小反向代理,让 http://127.0.0.1:6188/ 转发到本机的 127.0.0.1:3000,然后把这个二进制按服务方式部署起来。 本文示例版本核对时间:2026-05-17。示例使用 pingora = "0.8"。 先看等价的 Nginx 配置 如果用 Nginx,最小反代大概长这样: server { listen 6188; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto http; } } Pingora 的思路不一样。你不是写配置块,而是实现 ProxyHttp trait:请求进来以后,代码决定它要转发到哪里,转发前要改哪些请求头,最后日志怎么记。 创建项目 cargo new mini-pingora-proxy cd mini-pingora-proxy Cargo.toml: [package] name = "mini-pingora-proxy" version = "0.1.0" edition = "2021" [dependencies] async-trait = "0.1" env_logger = "0.11" log = "0.4" pingora = { version = "0.8", features = ["proxy"] } 这里用的是 pingora 聚合 crate,并启用 proxy feature。Pingora 没有默认打开所有能力;如果后面要做负载均衡,需要再启用 lb;如果要监听 HTTPS 或连接 HTTPS upstream,还要选择对应的 TLS feature,比如 openssl、boringssl、s2n 或实验性的 rustls。 ...

May 17, 2026 · 4 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

Hugo + GitHub Actions:从本地 Markdown 到线上站点的全自动流水线

0. 背景与架构演进:为什么我们需要流水线? 手写命令当然也能把站点部署上去,但每更新一篇文章都要重复一遍构建、打包、上传,既枯燥又容易漏步骤。在进入机制细节之前,先讲清楚为什么这套流程值得交给自动化流水线。 Hugo 的运行机制 Hugo 是一个基于 Go 语言的静态网站生成器(Static Site Generator)。它的核心工作流是将我们编写的 Markdown 源文件,结合站点配置和主题模板,在编译阶段直接渲染成纯静态的 HTML、CSS 和 JS 文件(输出到 public/ 目录)。 由于是纯静态站点,线上的服务器不需要运行任何数据库或动态后端(如 PHP/Node.js),只需要一个 Web 服务器来托管这些文件即可对外提供访问。这种架构带来了极高的访问速度和安全性。 自动化构建的必然性 正因为 Hugo 所有的页面都是预先生成的,如果采用手动发布,我们每次更新文章就会陷入一个繁琐的循环:本地写 Markdown → 本地运行 hugo 构建出 public 目录 → 手动打包或通过 scp/rsync 把文件传到服务器。 这三步里有两步是枯燥的机械劳动,不仅容易出错(比如忘了构建就上传),而且如果更换电脑写作,还得重新配置 Hugo 环境。因此,搭建一条自动化的 CI/CD 流水线,把编译和部署工作交给云端,是大幅提升博客写作体验的必选项。我们最终期望的体验是:只管写 Markdown,推代码即自动部署。 为什么这样设计部署架构? 基于上述诉求,我们设计了当前的流水线架构: 构建层(GitHub Actions):文章源码天然需要使用 Git 进行版本控制并托管在 GitHub 上。GitHub Actions 提供了与代码仓库深度绑定的免费 CI/CD 环境。它可以监听 git push 事件,自动在云端运行容器,拉取子模块(主题)并执行 hugo build。这样无论我们在哪里写代码,只要能 push,云端就能帮我们完成构建。 传输层(rsync):在云端构建完成后,我们使用 rsync 经由 SSH 将生成的静态文件增量同步到阿里云 ECS。rsync 非常轻量高效,配合 --delete 参数能够确保服务器上的文件状态与最新编译结果完全一致(自动删除已移除的文件),不需要在服务器端部署任何复杂的 Agent。 网关与服务层(从 Nginx 到 Pingap):静态文件传到服务器的指定目录后,需要对外暴露。本站点早期用 Nginx 直接托管静态文件,后来迁移到 Pingap 统一入口。为了让 Hugo 生成的目录页、文章页和标签页都保持稳定的 index.html 行为,当前采用 darkhttpd 在本机监听静态目录,Pingap 负责 HTTPS、SNI、HTTP 跳转和反代。无论入口网关怎么演进,“云端编译静态文件并推送到服务器目录” 始终是整套发布流程的核心。 1. 最终部署架构图 本地写 Markdown ──git push──→ GitHub ──Actions──→ Hugo build ──rsync──→ ECS /var/www/hugo-site/ ↑ SSH deploy key GitHub:源码仓库 + CI 触发器 + 版本历史 GitHub Actions:Ubuntu runner 上跑 Hugo build,然后 rsync 阿里云 ECS:实际跑服务的地方,darkhttpd 承载静态文件,Pingap 对外反代 2. 准备:仓库初始化 假设你已经有一个空仓库克隆到本地: ...

May 13, 2025 · 5 min · Icyyan

从 Nginx 迁移到 Pingap:一次接近零停机的实战记录

0. 背景与动机:为什么要替换掉 Nginx? 在过去很长一段时间里,本站点的静态文件托管(Hugo)以及动态服务反代(SillyTavern)一直由 Nginx 承担。Nginx 久经考验、极其稳定,堪称业界标杆。但随着系统架构的演进和统一网关管理的考量,我们开始寻求更现代化的反向代理方案,并最终将目光投向了 Pingap。 为什么选择 Pingap? 现代化的底层基石:Pingap 基于 Cloudflare 开源的 Pingora 框架构建,采用 Rust 编写。Pingora 在 Cloudflare 内部已经受了万亿级请求的考验。得益于 Rust 的内存安全特性,它从根本上规避了传统 C/C++ 网关常见的内存越界和崩溃风险。 更直观的配置模型:Nginx 的配置语法虽然强大,但指令的上下文和隐式执行顺序往往让人头疼。Pingap 采用了基于 TOML 的声明式配置,将 Server、Location、Upstream 和 Plugin 彻底解耦,配置的心智模型更加清晰,更利于后期的维护与扩展。 原生的高级特性支持:Pingap 对 HTTP/1.x、HTTP/2、WebSocket、gRPC-web、证书动态加载和插件化网关能力支持更直接。需要注意的是,HTTP/3/QUIC 这类能力应以当前官方 release 和文档为准,迁移生产入口时不要默认假设已经完整可用。 本次实战的核心目标:先在隔离端口上把新代理跑起来并完整验证,再用一次原子切换无缝替换 Nginx,全程保留 Nginx 配置以便出现问题时能够秒级回滚。 通过这种“先验证后切换”的安全策略,我们最终实测的切换中断窗口仅为 174 ms,线上流量几乎无感。 2026-05-17 复核:我重新 SSH 到服务器核对了一遍真实状态。当前服务器仍运行 pingap 0.13.2,pingap.service 使用 daemon 模式,Hugo 静态文件由 darkhttpd 承载。本文以下配置以服务器现状为准;新装环境可以再按最新 release 调整。 1. 起点:现状盘点 服务器:阿里云 ECS(Alibaba Cloud Linux 3 / RHEL 兼容),跑 Nginx 1.20.1,承载两个站点: ...

May 13, 2025 · 6 min · Icyyan