原有实验把四个等长槽段分配给 A、B、A、C。访问错误节点返回 MOVED。相同标签的键同槽,但同主节点不同槽的 MSET 失败且不改值。没有副本的三个主节点无法替代故障主节点。

Redis Cluster 把键分到不同的槽位

Redis Cluster 把键的落点与机器身份分成两层:键先映射到一万六千三百八十四个固定槽之一,再由集群元数据描述各槽的主节点和副本。增加机器不会改变槽哈希函数,运维需要重新分配槽,客户端则发现当前拓扑并处理路由变化。槽、分片、主节点与副本不是可以相互替代的概念。 原有实验把四个等长槽段分配给 A、B、A、C。访问错误节点返回 MOVED。相同标签的键同槽,但同主节点不同槽的 MSET 失败且不改值。没有副本的三个主节点无法替代故障主节点。 键选择槽,不要求机器持有连续区间 普通映射使用键的 CRC16/XMODEM 值对一万六千三百八十四取模。如果第一个左花括号与其后第一个右花括号之间存在非空标签,则只哈希这部分。例如 profile:{42} 和 quota:{42} 的完整键名不同,却位于同一个槽。标签只是数据放置规则,不是权限控制或租户隔离机制。 一个主节点可以拥有多个不连续的槽段。拓扑稳定时,一个槽有权威主节点;整个键属于该槽,大 hash 或有序集合不会被自动拆到多台机器。CLUSTER SHARDS 能显示分片成员及其槽段。尤其不能把“同一主节点”误当成“同一个槽”:前者是当前机器分配关系,后者才是多键操作需要的条件。 客户端也参与路由 支持集群的客户端维护槽映射,并连接拓扑中公布的节点。访问错误的主节点可能收到带目标地址的 MOVED,而不是得到键不存在。MOVED 要求更新路由;ASK 是迁移期间的临时重定向,需要先向目标发送 ASKING 再发送被重定向的命令,不能因此把该槽永久改归目标节点。 迁移也不是所有参与方瞬间同步更新地图。不同客户端可能暂时持有不同拓扑,多键涉及的数据在迁移期间分散到两端时,还可能得到 TRYAGAIN。应使用维护中的集群客户端,不能假设随便连一个入口就会自动代理所有命令。公布的地址不仅要让 Redis 节点互相可达,也必须能从应用所在网络访问。 ...

十一月 26, 2027 · 4 分钟 · XB Field Notes
清单引用 RDB 格式的 AOF 基础文件与后续增量 AOF。原文实验重写完成时计数为 4,后加一次递增,进程重启后恢复为 5。

Redis AOF 用速度换持久性

AOF 记录 Redis 的数据变更,供启动时恢复,但选择 AOF 并不等于承诺任何已确认写入都绝不会丢失。持久性取决于 fsync 策略、实际生效的配置、底层存储,以及讨论的是进程退出、操作系统崩溃还是整块磁盘损坏。应该先明确恢复目标,再决定日志策略,不能仅凭“追加日志”这个名称推导保证。 执行、记录与确认不是同一步 Redis 先在内存数据集上执行命令,再把需要持久化的效果传播到 AOF。它不是“每条命令必须先落盘才能修改内存”的执行模型。使用 appendfsync always 时,Redis 在发送回复前同步追加的数据;一起处理的命令可以共用一次写入和 fsync,并不必然每个客户端命令都对应一个独立系统调用。 执行成功、完成同步、客户端收到回复仍是不同的时刻。连接在回复到达前断开,客户端不能据此判断操作没有发生,也不能因为配置了 always 就放心重复执行扣减或计数操作。持久化协议并没有替业务提供重试去重。 AOF 是用于重建状态的表示,不是永久保存所有原始请求的审计流水。Redis 可以转换用于持久化的命令,重写也只保留重建当前数据所需的信息,而非完整历史。因此,已经删除的值并不保证还能从重写后的文件中找回来。 按确认语义选择同步策略 appendfsync everysec 大约每秒安排同步,通常以约一秒的写入暴露窗口换取性能,但不能把它当成任意存储阻塞下都成立的严格丢失上界。appendfsync no 则把刷盘时机交给操作系统;它表示 Redis 不主动请求 fsync,不表示数据永远不会落到磁盘。这两种策略下,一条成功回复都不等于该写入已经进入稳定存储。 appendfsync always 给出更强的本机写入确认约定,同时增加同步开销,影响延迟与吞吐。它仍不能保护磁盘整体丢失、存储栈失信或应用主动误删。还要检查 no-appendfsync-on-rewrite:如果将其打开,后台持久化工作期间会暂停通常的 fsync,即使主要策略写着 always,实际保证也会变弱。检查有效配置比只检查某一个配置项更重要。 重写产物是一组文件 Redis 7 起,AOF 使用 manifest 清单、基础文件和增量文件。启用 aof-use-rdb-preamble 时,基础文件采用 RDB 格式,其后的修改放在增量 AOF 中。这个基础文件属于 AOF 自己的文件集合,不是另外定期生成的 dump.rdb,二者不能因为扩展名相似就混为一谈。 ...

十一月 17, 2027 · 3 分钟 · XB Field Notes
原有库存实验中,竞争方把库存从 2 改成 1,监视冲突使旧队列被拒绝,标记仍不存在。重新监视并读到 1 后,第二次尝试一起提交 0 与标记。仅靠 MULTI/EXEC 无法修复此前的旧读取。

WATCH 保护 Redis 中依赖读取的更新决定

MULTI/EXEC 能阻止别的客户端命令插进队列的执行过程,却不会自动保护应用此前已经读到的值。如果两个购买方先读到相同库存,再各自写回计算结果,即使每次 SET 都放进事务,也仍然可能丢失一次更新。问题出在读取与提交之间的空隙,而不是 SET 执行得是否足够连续。 原有库存实验中,竞争方把库存从 2 改成 1,监视冲突使旧队列被拒绝,标记仍不存在。重新监视并读到 1 后,第二次尝试一起提交 0 与标记。仅靠 MULTI/EXEC 无法修复此前的旧读取。 仅为原有实验示意,本轮未重跑。 先监视,再读取依赖 WATCH 让 EXEC 变成有条件的执行:被监视的键从建立监视之后不能发生修改。因此,必须先 WATCH,再读取参与决策的数据。若键在提交前改变,Redis 拒绝整个队列并返回空响应;这只是不执行自己的队列,不会撤销竞争方已经完成的修改。 监视与事务状态都属于同一条连接。redis-py 流水线在 watch 之后保留连接,multi 之前的 GET 会立即发出,应用可以读取结果并做判断;multi 之后才转为缓冲命令,等待 execute。用上下文管理器包住一次尝试,可以在结束时重置流水线状态并释放专用连接。 ...

七月 22, 2027 · 3 分钟 · XB Field Notes
账单组和分析组指向同一条流的不同位置;待处理条目与两组游标仍是不同状态。

投递不等于完成:示意。

Redis 流消费组记录投递位置

Redis 流不会自动为每个连接维护独立游标。普通 XREAD 使用应用提供的位置;消费组则在服务端保存一份组级最后投递位置,加上待处理记录。混淆这两类进度,可能让重启后的消费者跳过未完成任务,也可能重新执行早已完成的业务操作。 消费组在投递时推进位置 XGROUP CREATE 使用 0-0 时从保留历史之前开始,使用 $ 则从当前末端之后开始。每个组在同一份流记录上维护独立投递状态:账单组可能已投递两条,分析组却只投递一条。同一组内的具名消费者分担新工作,并不是每人都得到每一条消息。 在未启用 CLAIM 或 NOACK 的普通 XREADGROUP 中,> 请求组投递位置之后的条目,并把新投递加入待处理列表 PEL。这一位置在业务处理和确认之前就已经前移。XACK 只移除某一组里指定的待处理 ID,不删除流记录,也不推进该组最后投递 ID。 确认可以乱序到达。第二条完成,不代表第一条已经完成;组的投递位置也可以到达流末端,同时仍有旧条目处于待处理状态。除非应用契约确实保证连续完成,否则不能用最大的已投递 ID 或已确认 ID,反推它之前所有业务都已成功执行。 待处理历史属于具名消费者 XREADGROUP 使用 0-0 时,请求的是当前名字对应的待处理历史,不是随意回放整条流。一个新名字看不到另一位消费者名下的待处理条目。重启时换个名字,只读 >,不会恢复之前遗留的投递;服务可能不断处理新任务,看起来正常运行,旧任务却再也没有推进。 恢复需要读取原消费者的待处理历史,或显式转移符合条件的任务,例如使用 XAUTOCLAIM。空闲阈值必须考虑正常处理时长。Redis 8.4 及更新版本也支持可选的 XREADGROUP CLAIM,本实验未启用。转移归属无法阻止迟到的旧工作进程重复外部动作,因此仍需要与业务风险匹配的去重策略。 可以用 XINFO GROUPS 对比 last-delivered-id、pending 和 lag。lag 估计从组当前位置起仍待投递的条目,不是尚未完成的业务动作。某些重设位置或删除操作后,该指标可能无法计算;空值不等于零。lag 为零而 pending 非零,表示新消息已投递到末端,但仍有投递等待确认。 ...

七月 13, 2027 · 3 分钟 · XB Field Notes
带绝对截止时间的键分为两条命令路径:GET、INCR 与 SET KEEPTTL 保留同一个截止时间;普通 SET 清除期限,使 TTL 为 -1。读取不会自动续期。

Redis TTL 让每个键自带时钟

Redis 过期机制回答的是一个键什么时候应当停止可见,而不是整个服务最多使用多少内存。截止时间属于键,后续命令可能保留、替换或者移除它。因此,可靠地使用 TTL,既要检查首次写入,也要覆盖更新路径。看到写命令成功,不能证明之前设置的截止时间仍然存在,更不能直接推断数据最终会按原计划消失。 部分更新保留键期限,普通 SET 则清除期限。原文实验属于历史记录;可见性、清理和 RSS 必须分开。 截止时间不是自动滑动计时器 EXPIRE 使用秒,PEXPIRE 使用毫秒。Redis 将过期信息保存为绝对 Unix 时间戳,所以服务器停止运行期间,时间仍然继续流逝。系统时钟必须稳定,跨机器恢复数据时尤其需要注意时钟差异。它不是独立于系统时间变化的单调时长承诺。下面的实验只使用正常流逝的时间,不调整本机系统时钟。 GET 不会刷新过期时间。INCR 这类原地修改保留键已有的超时;普通成功的 SET 则替换值,并清除原来的超时。SET KEEPTTL 可以保留原期限,再次显式设置过期命令则会改变截止时间。需要滑动会话的应用必须主动实现续期策略,不能以为用户每次读取数据都会自然延长访问窗口。 当值和初始寿命属于同一次操作时,应使用 SET EX 或 SET PX 一起写入。分成 SET、EXPIRE 两条命令,会留下一个值已经存在、期限却尚未写入的失败窗口。客户端如果恰好在两条命令之间崩溃,就可能留下持久条目。这是写入协议缺陷,不是后台过期周期执行得太慢,调整清理频率也修复不了它。 ...

六月 29, 2027 · 2 分钟 · XB Field Notes
两组独立的历史启动测试恢复不同计数器值:仅 RDB 返回快照中的 1,启用 AOF 返回稍后写入的 2。独立 RDB 与另一份日志不会自动合并。实验测试了进程终止,没有测试断电。

RDB 与 AOF:两种保住 Redis 的方式

RDB 与 AOF 保存的是 Redis 状态的不同表示。选择之前应回答两个问题:允许丢失多少近期数据,以及可以接受多长恢复时间?单独一个持久化开关无法回答这些问题。真正需要的是可恢复的文件、明确的写入确认策略,以及验证过具体数据值的恢复流程,而不只是能够启动的进程。 原文历史实验中,仅 RDB 与启用 AOF 恢复不同的精确值。AOF 使用自身文件集合;本轮没有新增运行,也不证明断电可恢复。 快照记录一个时点,不保证固定间隔 RDB 是紧凑的时点数据集。BGSAVE 派生子进程,先写临时文件,完成后再替换旧快照;父进程继续处理请求,但派生进程、写时复制增加的内存以及磁盘工作仍有成本。SAVE 则同步保存并阻塞其他客户端,本文只把它用于很小的隔离测试数据,不应照搬到繁忙服务上。 例如 save 60 1000 同时要求经过指定时间并达到修改次数条件,不是“无论有多少修改,每六十秒都必定生成一份快照”,也不保证保存能按时成功完成。实际恢复暴露窗口取决于最近一份可用快照的状态有多旧;漏掉或失败的保存会扩大这个窗口。因此,要检查成功保存的记录与结果,不能只看配置中的时间数字。 AOF 改变恢复数据的表示 AOF 记录数据变更,并按 appendfsync 策略同步。everysec 通常暴露大约最近一秒的写入,这并非严格的丢失上限;no 把刷盘时机交给操作系统。always 通常先同步再回复,代价是额外的同步开销。若设置 no-appendfsync-on-rewrite yes,Redis 会在 BGSAVE 或 BGREWRITEAOF 期间跳过主进程的 AOF fsync,使 always 和 everysec 的持久性都减弱。固定版本的配置默认将此例外设为 no。这些策略不保护整块存储设备丢失,也不能保证故障切换时被提升的副本包含每一条已经在原节点落盘的写入。 ...

六月 15, 2027 · 3 分钟 · XB Field Notes
共同比例条形图对比历史 libc 近似观察:加载后分配量约 16.7 MiB,删除所有键后约 1.04 MiB,清空后的 RSS 约 20.8 MiB,最终比率约 20。这些观察不是目标值,也不证明泄漏。

Redis 内存比率需要结合分配背景判断

即使应用键已经删除,Redis 进程仍可能保留较大的常驻内存。这个现象值得解释,但它本身既不能证明内存泄漏,也不能直接证明分配器碎片。真正要回答的是哪些字节仍被分配、哪些页面仍驻留,以及这种占用是否威胁实际容量预算。只看一个名字里带“碎片”的比率,很容易选错处理对象。 历史 libc 近似观察值与分配量断言关系需分开。更高比率本身不证明泄漏、碎片或回收有效。 把不同测量对象分开 INFO memory 中的 used_memory 表示 Redis 统计的分配量,used_memory_rss 则是操作系统观察到的进程常驻内存。它们都不等于应用负载长度的简单相加,键、对象、元数据、客户端及其他开销也要占空间。MEMORY USAGE 可以估计单个键及其值的分配量,包括相关开销,但不测量这个键独立贡献了多少进程 RSS。 mem_fragmentation_ratio 大体比较常驻内存和 Redis 统计的分配量,两者差额还可能包括代码、共享库、栈以及分配器保留的页面。删除数据后分母下降,即使 RSS 几乎不变,比率也可能迅速升高。因此必须同时检查绝对字节数;一个很小的空实例出现较高比率,不等于它正在占用危险数量的内存,更不是必须立即重启的充分理由。 ...

六月 3, 2027 · 3 分钟 · XB Field Notes
SCAN MATCH 将前缀加星号的模式匹配到示例兄弟键;DEL 仅指向名称中含字面星号的那个精确键。

Redis 的键名要起得清楚

Redis 键是平面键空间中的二进制安全名称,不是文件路径,也不是数据库表结构声明。冒号可以按约定分隔业务归属、环境、实体和标识,例如 cart:prod:string:42 能表达意图。但 Redis 不会把这些字段解释成类型检查或访问限制,不能仅凭名字就推断服务端已经替应用建立了约束。 让约定没有歧义 写入方、读取方、维护任务和迁移逻辑,应采用相同字段顺序与标识编码。不同前缀可以让两个服务分别存储同一个业务编号;若最后编码出来的完整名字相同,仍然会碰撞。命名规范帮助人和代码协作,并没有创建由 Redis 独立管理的命名空间。 类型标签也只是说明。SET 可以在名字带 hash 的键下写入字符串,之后 HSET 就会报 WRONGTYPE。排查时应用 TYPE 查看真实数据类型,序列化格式与类型要求则属于应用契约。不能把名字中的“列表”“哈希”当作当前数据已经符合预期的证据。 名字宜简短清晰,不必压缩成难懂缩写,也不要把整段文档塞进去。键名占内存并随命令传输,但对象开销和负载本身同样重要;没有测量就不能宣称一次改名显著加快查询。标识编码还应避免分隔符、通配符和集群花括号取得本来不希望赋予的含义。 模式匹配不等于模式删除 SCAN MATCH 接受 glob 模式。从游标零开始,把每次返回的新游标用于下一次调用,直到再次返回零。COUNT 是工作量提示,不保证精确返回多少条,也不是严格内存上限。某一页可能没有匹配项,但游标还不为零,不能据此提前结束。每次调用仍需要服务端工作,增量扫描不等于完全没有阻塞成本。 完整迭代保证能看到在迭代全过程始终存在、且符合模式的键,但可能重复返回;迭代中新增或删除的键没有同样保证。应用应按需要去重,或让处理逻辑可以安全重复。扫描不是快照,更不能把一个空页面当作集合已经遍历完毕。 DEL 的语义不同:它接收精确键名,不接收模式。参数末尾带星号,不会删除所有同前缀兄弟键;只有真的存在名字包含这个星号的键,才会删除那一个。命令行工具之外还有 shell 展开这一层,应正确引用参数,不要把终端的展开行为误认为 Redis 自己支持通配删除。 坏掉的样子 维护脚本若假定 DEL 会展开前缀,可能实际什么也没删,却报告清理完成。相反,先用宽泛模式收集 SCAN 结果,再无条件删除每个匹配,也可能误伤不属于自己的数据。必须核对精确目标与修改策略;而且发现与删除之间,同名键可能已经被重建,之前审阅过的名字不一定还指向同一个逻辑对象。 前缀也不是安全边界。访问限制应通过合适的 ACL 键模式和命令权限落实,必要时使用独立部署边界。键模式权限无法约束不携带键参数、直接作用于整个数据库的命令,所以给账户一个前缀模式不等于万事安全。服务名前缀也不会自动预留内存,更不构成淘汰策略隔离。 ...

六月 1, 2027 · 3 分钟 · XB Field Notes