Redis Cluster 按分配给各分片的哈希槽路由整个键。

Redis Cluster 把键分到不同的槽位

Redis Cluster 把键的落点与机器身份分成两层:键先映射到一万六千三百八十四个固定槽之一,再由集群元数据描述各槽的主节点和副本。增加机器不会改变槽哈希函数,运维需要重新分配槽,客户端则发现当前拓扑并处理路由变化。槽、分片、主节点与副本不是可以相互替代的概念。 键选择槽,不要求机器持有连续区间 普通映射使用键的 CRC16/XMODEM 值对一万六千三百八十四取模。如果第一个左花括号与其后第一个右花括号之间存在非空标签,则只哈希这部分。例如 profile:{42} 和 quota:{42} 的完整键名不同,却位于同一个槽。标签只是数据放置规则,不是权限控制或租户隔离机制。 一个主节点可以拥有多个不连续的槽段。拓扑稳定时,一个槽有权威主节点;整个键属于该槽,大 hash 或有序集合不会被自动拆到多台机器。CLUSTER SHARDS 能显示分片成员及其槽段。尤其不能把“同一主节点”误当成“同一个槽”:前者是当前机器分配关系,后者才是多键操作需要的条件。 客户端也参与路由 支持集群的客户端维护槽映射,并连接拓扑中公布的节点。访问错误的主节点可能收到带目标地址的 MOVED,而不是得到键不存在。MOVED 要求更新路由;ASK 是迁移期间的临时重定向,需要先向目标发送 ASKING 再发送被重定向的命令,不能因此把该槽永久改归目标节点。 迁移也不是所有参与方瞬间同步更新地图。不同客户端可能暂时持有不同拓扑,多键涉及的数据在迁移期间分散到两端时,还可能得到 TRYAGAIN。应使用维护中的集群客户端,不能假设随便连一个入口就会自动代理所有命令。公布的地址不仅要让 Redis 节点互相可达,也必须能从应用所在网络访问。 放置关系不等于事务范围 MSET 这类服务端多键命令要求所有键属于同一个槽,仅仅处在同一物理主节点上仍不够。哈希标签可以满足同槽条件,但把所有租户都放进同一个标签又会集中负载,削弱分布能力。某个客户端方法如果拆成多个独立命令执行,也不能被当成一个原子的多键命令。 分片分配数据归属,复制则为一个分片提供副本。复制是异步的,所以副本读取可能落后。故障转移提升的是受影响分片的副本,并不是把整个集群作为不可分割的一组一起切换。发生故障时仍可能丢失已经确认的写入;连接中断也可能让调用者无法确定某条写操作最终有没有执行。 坏掉的样子 槽覆盖不完整或可达主节点不足,都可能阻止服务。在 cluster-require-full-coverage yes 下,一个分片不可用会使其他仍健康的主节点也拒绝键命令。三个没有副本的主节点能够展示数据分布,却没有能力从根本不存在的副本中恢复故障节点的数据,不能据此宣布已经具备高可用。 应一起检查槽覆盖、集群状态、副本健康、地址通告和客户端重定向错误。只增加节点不会自动搬迁已有数据,也不会自动拆开单个热键;重新分片、副本放置与客户端重试语义分别需要设计。应测试准备容忍的具体故障,而不是把节点握手成功当成高可用验收结果。 快速验证 原有实验记录使用 macOS、Python 3.14.4、Redis 8.10.1 和 redis-py 8.1.0;本轮审阅没有重新执行服务实验,下面的结果说明保留原有观察范围。准备 POSIX 环境、C 编译器、make、curl、tar 与 Python 3.10 或更新版本。安装步骤仅用 libc 构建 Redis 核心,并把客户端装入临时虚拟环境。 ...

十一月 26, 2027 · 4 分钟 · XB Field Notes
Redis AOF 的同步策略,以及从基础文件与增量文件恢复状态。

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
观察、决策和执行面板,旁边是 MULTI、EXEC 与 WATCH 标题。

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

MULTI/EXEC 能阻止别的客户端命令插进队列的执行过程,却不会自动保护应用此前已经读到的值。如果两个购买方先读到相同库存,再各自写回计算结果,即使每次 SET 都放进事务,也仍然可能丢失一次更新。问题出在读取与提交之间的空隙,而不是 SET 执行得是否足够连续。 先监视,再读取依赖 WATCH 让 EXEC 变成有条件的执行:被监视的键从建立监视之后不能发生修改。因此,必须先 WATCH,再读取参与决策的数据。若键在提交前改变,Redis 拒绝整个队列并返回空响应;这只是不执行自己的队列,不会撤销竞争方已经完成的修改。 监视与事务状态都属于同一条连接。redis-py 流水线在 watch 之后保留连接,multi 之前的 GET 会立即发出,应用可以读取结果并做判断;multi 之后才转为缓冲命令,等待 execute。用上下文管理器包住一次尝试,可以在结束时重置流水线状态并释放专用连接。 凡是会影响决策的值或“不存在”条件,都要纳入监视范围。只监视库存,却忽略另一个键保存的额度,并不能保护额度判断。WATCH 也不只是比较最终字节:即便 SET 写回相同内容,也可以使监视失效;原来不存在的被监视键被创建,同样会导致冲突。本次版本中,由过期和内存淘汰引起的变化也计算在内。 重试整个决策,而不是旧计算结果 收到明确的冲突结果后,应建立新监视,重新读取依赖,再检查业务条件和计算写入值。继续使用旧库存,相当于把同一个错误判断再执行一次。EXEC 无论成功还是因监视冲突取消,都会清除监视;如果应用读完后决定放弃,也可以通过 UNWATCH 主动解除。 重试需要次数或时间上限,并让调用方知道何时已耗尽预算。热点键上的大量冲突会放大成本,即使最终没有丢数据也会增加延迟。退避可以缓和竞争,却补不上遗漏的监视键。若原生命令已经能表达所需的原子操作,应优先使用它;实验刻意拆开库存算术,是为了展示旧值覆盖问题。 监视检查通过,也不会让事务获得自动回滚能力。EXEC 内部某一条命令运行失败,其它成功命令仍可能已经生效。应校验相关数据模型并逐项检查响应,不要把数个互不相干的副作用放在一起,就称为业务上的全成全败。 坏掉的样子 先读后 WATCH,会漏掉建立监视前的那段竞争窗口。用一条连接监视、另一条连接提交,不会保护后者的队列。在重试循环中发邮件或发起支付,则可能在 Redis 拒绝本次尝试之前,就已经产生了无法随队列取消的外部效果。 还必须区分服务端明确报告的冲突与传输故障。redis-py 8.1.0 在监视期间遇到连接异常,也可能抛出 WatchError;仅凭异常类型,不能断言 EXEC 从未执行。提交之后应答丢失,需要核对业务结果或依赖幂等协议,不能无条件重新执行不可逆动作。 ...

七月 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 也支持可选的 XREADGROUP CLAIM,本实验未启用。转移归属无法阻止迟到的旧工作进程重复外部动作,因此仍需要与业务风险匹配的去重策略。 可以用 XINFO GROUPS 对比 last-delivered-id、pending 和 lag。lag 估计从组当前位置起仍待投递的条目,不是尚未完成的业务动作。某些重设位置或删除操作后,该指标可能无法计算;空值不等于零。lag 为零而 pending 非零,表示新消息已投递到末端,但仍有投递等待确认。 ...

七月 13, 2027 · 3 分钟 · XB Field Notes
观察、决策和执行面板,旁边是 Redis TTL 截止时间标题。

Redis TTL 让每个键自带时钟

Redis 过期机制回答的是一个键什么时候应当停止可见,而不是整个服务最多使用多少内存。截止时间属于键,后续命令可能保留、替换或者移除它。因此,可靠地使用 TTL,既要检查首次写入,也要覆盖更新路径。看到写命令成功,不能证明之前设置的截止时间仍然存在,更不能直接推断数据最终会按原计划消失。 截止时间不是自动滑动计时器 EXPIRE 使用秒,PEXPIRE 使用毫秒。Redis 将过期信息保存为绝对 Unix 时间戳,所以服务器停止运行期间,时间仍然继续流逝。系统时钟必须稳定,跨机器恢复数据时尤其需要注意时钟差异。它不是独立于系统时间变化的单调时长承诺。下面的实验只使用正常流逝的时间,不调整本机系统时钟。 GET 不会刷新过期时间。INCR 这类原地修改保留键已有的超时;普通成功的 SET 则替换值,并清除原来的超时。SET KEEPTTL 可以保留原期限,再次显式设置过期命令则会改变截止时间。需要滑动会话的应用必须主动实现续期策略,不能以为用户每次读取数据都会自然延长访问窗口。 当值和初始寿命属于同一次操作时,应使用 SET EX 或 SET PX 一起写入。分成 SET、EXPIRE 两条命令,会留下一个值已经存在、期限却尚未写入的失败窗口。客户端如果恰好在两条命令之间崩溃,就可能留下持久条目。这是写入协议缺陷,不是后台过期周期执行得太慢,调整清理频率也修复不了它。 检查真正存在的状态 TTL 返回剩余秒数;键存在但没有过期时间时返回 -1,键不存在时返回 -2。PTTL 提供毫秒分辨率,PEXPIRETIME 则返回绝对截止时间。要验证一次读取有没有续期,比较绝对期限比比较两次倒计时更可靠,因为两条查询之间时间正常流逝,本来就可能让剩余数值减少。查询结果只是当时的快照,不保证键一定能存活到那个时刻。 EXPIRE NX 只在没有期限时添加期限。GT 只接受更晚的截止时间,其中持久键被当成无限寿命处理。PERSIST 则显式移除过期设置。调用方需要检查命令返回的结果,不能把“命令被接受”理解成“条件成立且状态已经改变”。相对过期时间为零或负数时,键会立即被删除,并不是改成永久保存的另一种写法。 坏掉的样子 被动过期在访问时检查截止时间,主动过期负责处理那些过期后没有再被访问的键。物理清理延迟,不等于普通主节点 GET 可以持续交付已经过期的值。反过来,条目被移除,也不保证分配器马上把页面交还操作系统。读取可见性、数据结构清理与进程 RSS 是不同观察对象,不能只看其中一个就替其他两个下结论。 ...

六月 29, 2027 · 2 分钟 · XB Field Notes
RDB 快照与 AOF 文件集合保存不同的可恢复状态。

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

RDB 与 AOF 保存的是 Redis 状态的不同表示。选择之前应回答两个问题:允许丢失多少近期数据,以及可以接受多长恢复时间?单独一个持久化开关无法回答这些问题。真正需要的是可恢复的文件、明确的写入确认策略,以及验证过具体数据值的恢复流程,而不只是能够启动的进程。 快照记录一个时点,不保证固定间隔 RDB 是紧凑的时点数据集。BGSAVE 派生子进程,先写临时文件,完成后再替换旧快照;父进程继续处理请求,但派生进程、写时复制增加的内存以及磁盘工作仍有成本。SAVE 则同步保存并阻塞其他客户端,本文只把它用于很小的隔离测试数据,不应照搬到繁忙服务上。 例如 save 60 1000 同时要求经过指定时间并达到修改次数条件,不是“无论有多少修改,每六十秒都必定生成一份快照”,也不保证保存能按时成功完成。实际恢复暴露窗口取决于最近一份可用快照的状态有多旧;漏掉或失败的保存会扩大这个窗口。因此,要检查成功保存的记录与结果,不能只看配置中的时间数字。 AOF 改变恢复数据的表示 AOF 记录数据变更,并按 appendfsync 策略同步。everysec 通常暴露大约最近一秒的写入,no 把刷盘时机交给操作系统;always 在有效配置允许的情况下先同步再回复,代价是额外的同步开销。这些策略不保护整块存储设备丢失,也不能保证故障切换时被提升的副本包含每一条已经在原节点落盘的写入。 现代 AOF 是 manifest、基础文件和增量文件组成的集合,基础文件可以使用 RDB 格式。因此,“AOF 启动必定从头回放庞大的文本历史”不准确;重写还会去掉重建当前状态不再需要的历史操作。文件大小、启动耗时、重写开销和请求延迟,应结合实际数据集测量,不能仅凭 RDB 或 AOF 的名称排序。 两者同时启用,不等于自动合并 在没有额外启动覆盖的通常配置下,启用了 AOF 的 Redis 会选择 AOF 恢复,而不是先独立加载 dump.rdb,再把另一份日志叠加上去。AOF 内部的 RDB 格式基础文件属于其自身文件集合。另行保留的 RDB 仍可用于备份和明确选择的恢复操作,但不会自动补齐缺损的 AOF 文件。 ...

六月 15, 2027 · 3 分钟 · XB Field Notes
诊断碎片前,先比较 Redis 分配量、常驻页面与分配器指标。

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

即使应用键已经删除,Redis 进程仍可能保留较大的常驻内存。这个现象值得解释,但它本身既不能证明内存泄漏,也不能直接证明分配器碎片。真正要回答的是哪些字节仍被分配、哪些页面仍驻留,以及这种占用是否威胁实际容量预算。只看一个名字里带“碎片”的比率,很容易选错处理对象。 把不同测量对象分开 INFO memory 中的 used_memory 表示 Redis 统计的分配量,used_memory_rss 则是操作系统观察到的进程常驻内存。它们都不等于应用负载长度的简单相加,键、对象、元数据、客户端及其他开销也要占空间。MEMORY USAGE 可以估计单个键及其值的分配量,包括相关开销,但不测量这个键独立贡献了多少进程 RSS。 mem_fragmentation_ratio 大体比较常驻内存和 Redis 统计的分配量,两者差额还可能包括代码、共享库、栈以及分配器保留的页面。删除数据后分母下降,即使 RSS 几乎不变,比率也可能迅速升高。因此必须同时检查绝对字节数;一个很小的空实例出现较高比率,不等于它正在占用危险数量的内存,更不是必须立即重启的充分理由。 used_memory_peak 有助于解释此前负载峰值留下的占用。used_memory_dataset 是另一个统计视角,不承诺空键空间对应零进程分配。各项数据也并非全都在 INFO 执行的同一瞬间采样。写入活跃时,不应要求经过四舍五入的比率,精确等于独立读取的两个数字相除,否则可能把采样差异误当成实现错误。 先核实分配器和版本 分配器专用指标有助于区分碎片、保留页面和非堆开销,但含义取决于实际构建。Redis 8.10.1 源码通过小尺寸分配桶的碎片计数计算相关指标,并对 Lua 部分作调整。不能把所有版本的指标都简单重算成 active 除以 allocated;通用文档公式与实际行为不符时,应检查对应版本实现,而不是让旧解释覆盖当前事实。 本实验有意使用 libc 编译。该构建没有 jemalloc 的详细统计,Redis 会用其他测量值补算部分分配器字段,碎片计数则保持为零。因此 allocator_frag_ratio 等于一,不证明进程真的完全没有碎片。解释专门字段或选择处理方式之前,应先记录 mem_allocator,确认数字来自真实分配器统计,还是当前构建能够提供的有限信息。 ...

六月 3, 2027 · 3 分钟 · XB Field Notes
Redis 键名描述归属和数据形状,但不自动约束类型与权限。

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