发布订阅只向生效订阅广播实时事件;独立 XREAD 读者各自回放保留历史,同组成员分担新投递。精确 MAXLEN 2 删除最旧条目。图示来自原有实验,不是新服务运行结果。

Redis 发布订阅与流是两样工具

Redis 发布订阅与流解决的是不同的投递问题。发布订阅面向当前生效的订阅广播,不保留供以后回放的消息;流保存条目,读者可以在它们仍被保留时重新访问。选择哪种机制,首先要回答谁必须收到事件,以及消费者缺席期间应该发生什么。 发布订阅只向生效订阅广播实时事件;独立 XREAD 读者各自回放保留历史,同组成员分担新投递。精确 MAXLEN 2 删除最旧条目。图示来自原有实验,不是新服务运行结果。 先确定每个事件交给谁 对于后续状态可以替代的实时看板更新,只向在线用户广播也许已经足够。但在假设新听众已经到场之前,应等待订阅确认。重连恢复的是未来投递,不是错过的发布。PUBLISH 返回正数,表示按 Redis 命令语义统计到了接收者,不等于应用确认业务处理已经完成。 流有两种不同的消费方式。普通 XREAD 让独立读者分别指定自己的起始 ID,双方可以收到同一份保留历史。它既不自动分担工作,也不替每个连接保管检查点。同一个消费组中的 XREADGROUP 则在具名消费者之间分配新投递,并在未启用 NOACK 时记录待处理条目。 不同消费组允许账单、分析等独立工作流在同一条流上分别推进。组内增加成员是在分担工作,新建一组则是增加一套投递历史。消费者名字不同、工作进程变多,都不承诺负载均分或业务按序完成。应先确定需要的是广播、独立回放还是竞争处理,再选择对应的读取命令,不能把三种模式混为一谈。 保留窗口不是持久化设置 XADD 把数据保存在 Redis 里,并不自动保证每个已接受的写入都能熬过崩溃或故障切换。RDB/AOF 设置、复制行为、键淘汰及恢复操作共同决定持久性。发布订阅也不会因为启用了持久化,就突然拥有可回放的日志。因此,把两者概括成时效性与持久性的取舍,会掩盖各自的真实契约。 MAXLEN 删除的是最旧的条目,不是裁掉最新队尾、同时让早期历史一直保留。近似裁剪可能留下额外记录,精确裁剪则遵循指定数量;两者都不提供无限回放。默认裁剪还可能删除待处理引用对应的载荷,因此应围绕允许的最长停机时间与恢复后的追赶过程设计保留策略和容量。 ...

二月 6, 2028 · 3 分钟 · XB Field Notes
对于 >log 2>&1,第一个状态让描述符 1 指向日志、2 留在原处,第二个状态让两者都指向日志。对于 2>&1 >log,第一个状态让两者引用原标准输出目标,之后只有 1 移到日志。复制不会建立动态绑定。

重定向重新安排流向

重定向在命令执行前改变它的文件描述符。Shell 从左到右处理每一项,遇到打开文件或复制描述符的操作,就在那个位置完成相应变更。描述符零、一、二通常分别表示标准输入、标准输出和标准错误。把命令行读成对这些句柄的顺序操作,就能解释诊断为什么去了错误的位置,也能解释文件为什么还没有被程序读取就已经变空。 从左到右处理重定向。每次复制保留当时的目标,打开文件本身就可能在命令开始前改变数据。 打开文件本身就可能改变数据 >output 打开输出文件,并且通常把已存在的普通文件截断为零长度;>>output 使用追加方式,<input 则打开文件供读取。这些动作由 Shell 完成,不是命令所指程序的内部行为。如果必须打开的目标失败,命令不会执行,但此前已经成功的重定向仍可能创建或截断其他文件。不能把“程序没有启动”理解成“整条命令完全没有副作用”。 因此,sed expression input >input 会在转换程序读取数据之前破坏原文件。即便先写 <input 打开输入,后续重定向截断同一个底层文件,也不能保住那些内容。应写入独立临时文件,检查转换状态与结果,再根据文件归属、权限和所在文件系统,明确执行替换步骤。输入输出恰好同名时,不能指望 Shell 自动替你实现安全的原地编辑。 复制记住的是当下的目标 >log 2>&1 先让描述符一指向日志,再让描述符二引用同一个已打开目标。2>&1 >log 则先复制原来的标准输出,之后只改变描述符一,因此标准错误仍留在原目标,例如终端或负责捕获输出的管道。这里复制的是描述符引用,不是文件内容,也不是给两个数字建立一个会随之后续变化的动态连接。 Bash 的 &>log 可以简写同时重定向两条流,而 >log 2>&1 也适用于可移植 Shell。不要把它写成 >log 2>log:后者分别打开同一个文件,两份打开状态拥有独立偏移量,输出可能相互覆盖。即使描述符共享一个目标,应用程序自身的缓冲仍会影响消息真正写出的时间,所以合并日志也不自动保证业务事件严格按发生顺序排列。 ...

九月 29, 2027 · 1 分钟 · 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
有序流 ID 条划掉删除或裁剪的较旧记录,并括起仍保留的 3-0 与 4-0。

回放需要保留记录:示意。

Redis 流按顺序保管事件供消费

Redis 流把条目保存在有序 ID 下,读者可以重新读取尚未删除的历史。与发布订阅不同,生产者追加时读者不必在线。这个差别提供的是回放能力,不是无条件持久性:条目删除、保留限制及持久化设置仍决定延迟或故障后究竟还剩下什么。 区分写入顺序与事件时间 XADD 使用 * 时生成包含毫秒部分和序列部分的 ID。在同一条流内,ID 单调递增,Redis 必要时会补偿本机时钟回退。显式提供的 ID 也必须大于这条流最后生成的 ID。比较时应比较两个数值部分,不能把任意长度的字符串字典序当成数值顺序。 ID 排定的是写入该流的顺序,不会建立多条独立流之间的全局顺序,也不证明外部事件真正发生的时间。需要业务时间时,应单独保存相应字段。两次普通 XADD 即使字段完全相同,也会创建两个不同条目:自动 ID 标识记录,并不自动识别重复业务操作。 保存最后处理的位置 XRANGE 默认包含起止两端。把已经处理过的最后一个 ID 作为起点,会再次返回该条目;在边界前加左圆括号可以排除它。XREAD 则返回严格大于指定 ID 的条目。配合 COUNT 分批读取时,应保存最后返回的 ID,下次从这个位置继续。 普通 XREAD 不会替每个读者维护服务端偏移,也不会移除已投递条目。独立读者可以读取同一范围,重复请求也会重复返回仍被保留的数据。检查点由应用负责:副作用之前保存位置,故障后可能跳过尚未执行的工作;副作用之后保存,故障后可能重复已经完成的工作。不能接受这类差异时,需要把位置推进与幂等处理协调起来。 美元符号 ID 表示从 Redis 处理这次调用时的流末端之后开始。它适合首次进入实时订阅,不适合恢复已有进度。每次响应后继续用 $,会跳过两次调用之间追加的条目。收到数据后应使用真实的最后一个 ID。消费组另行增加投递与确认状态;XACK 并不是普通 XREAD 的偏移提交命令。 ...

五月 6, 2027 · 3 分钟 · XB Field Notes