观察、决策与行动三块面板,旁边的标题说明穿透读取缓存会在未命中时加载。

读穿透缓存:未命中时取数回填

读穿透缓存给调用者提供统一的读取接口:命中时返回已存值,未命中时调用加载函数,再把结果保存起来。关键边界是由谁拥有这个 loader。把它放到缓存接口后面,可以减少重复的查找逻辑,但并不会自动解决并发、写入、访问授权或不同缓存副本之间的一致性。模式名称不是这些能力已经具备的证明。 找到真正负责加载的一层 在 cache-aside 模式中,应用显式查询缓存,未命中后访问数据源并回填;读穿透则让缓存组件拥有这条未命中路径。它可以是应用进程里的库,也可以是远程缓存服务。两种部署方式各有更新边界:修改进程内库的 loader,仍然需要把变更部署到使用它的进程,不能笼统宣称所有读者都会免部署地自动获得新逻辑。 单独一个 Redis GET 也不是读穿透加载器。字符串键不存在时,它返回 nil,不会自动发现并查询你的数据库。客户端库、缓存服务或应用适配层必须另外提供加载逻辑。下面的实验用带记忆功能的函数包装器提供统一读取接口,缓存和数据源都在当前进程内;这不是 Redis 服务端加载功能的演示。 读取模式没有定义写入模式 读穿透本身没有规定写操作经过哪里,写穿透是另一项安排。即便读写共用一个组件,仍需要定义数据源写入和缓存更新分别失败时的行为。绕过组件的其他写入方,可能让缓存保持旧值。应沿真实业务更新路径检查参与者,不能因为使用了一个模式名称,就假定所有写入都会配合清理。 TTL 限制的是某个缓存条目的复用时间,并不保证下一次重载从滞后副本或有缺陷的 loader 中取到最新真值。过期也不一定立即从内存中释放字节。寿命应依据数据需求选择,而非套用统一的几秒或几分钟;用户数据的 TTL 再短,也不构成权限隔离。命中率很高同样不能证明内容正确。 坏掉的样子 记录不存在与数据源不可用,是两个不同结果。缓存一次合法的“不存在”可以减少重复查询,但如果随后创建了记录,这个否定结果也可能继续遮住新记录,直到到期或失效。若把超时转换成一次正常返回的空值,就可能把故障写成错误的缓存内容。必须明确哪些结果允许保存,以及调用者如何区分缺失和失败。 并发加载是另一项独立契约。Cachetools 明确说明:锁保护缓存对象的访问,但被包装的函数在锁外运行,因此只有锁并不能抑制重复加载。它的 condition 选项另行处理同键的并发执行。下面的实验只运行单线程,刻意不使用这两个参数,不能把代码当成已经具备线程安全或分布式协作的实现。 失效还可能与一个已经读到旧源版本的加载操作竞争。删除某个键,并不天然阻止那个在途结果稍后重新写入。应根据真实的写入和加载协议选择协调方式,并测试操作顺序。也不要把大范围按键模式删除当作默认修复:它会让更多条目变冷,却没有证明源头缺陷已经解决。 快速验证 需要 Python 3.10 或更新版本;本次使用 Python 3.12.14 和 cachetools 7.1.8。安装固定依赖后,用 "$readthrough_lab/venv/bin/python" 执行 Python 代码。缓存存储、到期和函数记忆行为都由现有库负责;测试手动推进专用时钟,无需睡眠,也不修改系统时间。实验不联系 Redis 或外部数据库,只有依赖安装需要联网。 ...

五月 26, 2027 · 2 分钟 · XB Field Notes
观察、决策和执行面板,旁边是 Redis 流保留事件的标题。

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
在一个 Redis 值里紧凑地存下海量是/否标记的方式。

Redis 位图用位而不是存储数字

Redis 位图是把字符串按单个位解释的用法,并不是一种独立的键类型。它适合稠密的是非标记,例如按用户记录每日活跃状态。一千万个连续标志需要一百二十五万个载荷字节,但这个估算不包括键名、对象元数据、分配器额外开销,也不包括业务标识到位偏移的映射,不能直接当成实例总内存。 先定义偏移的契约 SETBIT 在从零开始的偏移处写入零或一,并返回写入前的位值。已经为一的位置再次写一,不会增加成员数量。偏移零对应第一个字节的最高位,偏移八才进入第二个字节。对同一个键执行 GET,读到的是底层字节,它们未必是有效文本,所以二进制数据不应默认按 UTF-8 解码。 GETBIT 对缺失键或超出现有字符串长度的位置返回零,而且不会因为读取就扩容。因此,零不能区分“明确不活跃”与“尚未记录”,需要先决定业务是否允许合并这两种状态。SETBIT 与 GETBIT 要求键的值是字符串;如果键实际是哈希,会报类型错误,不会自动把哈希字段转换成位。BITOP 则会在目标键写入新的字符串。 内存取决于写入过的最高偏移,而不是当前有多少个位为一。写偏移 n 至少需要 floor(n / 8) + 1 个载荷字节,即使要写入的是零也一样。后来把高位清零,并不会缩短字符串。稀疏外部编号因此可能用极少的有效标记触发巨大分配。应验证允许的偏移范围,并采用稳定且足够稠密的编号映射,而不是直接信任任意业务 ID。SETBIT 要求偏移小于二的三十二次方,对应最大五百一十二 MiB 字符串。这个协议上限不是合理的单次请求分配预算,应用应按业务规模设置更小的允许范围。 计数和组合都要明确范围 BITCOUNT 默认检查全部字节。可选的 start 和 end 是包含两端的字节位置;从 Redis 7.0 开始,可以用 BIT 修饰符把它们改为位位置。切换单位只改变查询区间,不会改变键里实际存储的表示。返回值是为一的位数,不是最大的成员编号,也不是位图所能容纳的全部用户数。 BITOP OR 计算成员并集,AND 计算交集。较短的输入和不存在的键,会被视为补零到最长输入的长度。目标键会被新字符串覆盖,原本附在目标键上的过期时间也会移除。因此,派生结果需要明确的寿命策略,不能以为之前给结果键设置过一次期限,以后的重算就会自动继承。 ...

四月 29, 2027 · 3 分钟 · XB Field Notes
一条可从头尾两端收发值的双向序列。

Redis 列表既是队列,也能当栈

Redis 列表是有顺序的字节值序列,可以从两端插入和取出。选择哪一端写、哪一端读,决定了它表现为队列还是栈。阻塞操作只是等待数据到达,并不自动提供分布式锁、消息确认或完整的任务处理协议。这些能力必须根据业务需要另外明确,不能从“支持阻塞”直接推导出来。 先把两端的约定说清楚 RPUSH 配合 LPOP 按先进先出顺序移除元素,早进入的元素先离开;RPUSH 配合 RPOP 则是后进先出。一条 LPUSH 带多个值时,会依照参数顺序逐个往左插入,因此传入 a、b、c 后,从左到右看到的是 c、b、a。设计队列时要验证生产者真正执行的命令,而不是只看图里画了哪一端。 列表允许重复值,相同消息插入两次就占两个位置,不会自动完成去重。当两次投递必须能够区分时,应使用不同的任务标识。LRANGE 的起止位置都包含在返回范围里,-1 表示最后一个元素。最后一个元素被移除时,整个列表键也会消失;对不存在的列表弹出会返回空结果,而不是创建一个空列表占位。 两端操作的单元素成本较低,但返回很大的范围或一次删除许多元素,仍然需要相应的工作量。LTRIM 可以保留最近一段窗口,不过用它裁剪待处理任务,就等于主动丢弃被裁掉的条目。生产者写入和随后的单独裁剪是两条操作,不能当成原子提交。保留哪一段应由数据用途决定,不能把日志窗口策略直接套到任务队列上。 等待的是连接,不是整个服务器 BLPOP 在数据到来或等待超时前阻塞当前连接,其他连接仍可正常执行,生产者也能照常投递。在 redis-py 中,成功结果包含键名和取出的值,超时返回 None;超时参数为零则表示无限等待。客户端的套接字超时需要容纳预期的服务器等待时间及响应时间,否则可能先在客户端超时。 消费者没有业务工作时,阻塞调用仍会占用连接。因此应给消费者设置有界的连接预算,并避免让生产者与正在等待的消费者共用同一条连接。还要区分移除顺序与完成顺序:FIFO 保证先移走较早的元素,多个工作者却可能先完成后来的任务。仅凭阻塞等待,无法保证某个业务资源只有一个持有者。 坏掉的样子 LPOP 和 BLPOP 都在业务处理完成之前把任务从列表里移除。消费者刚取到任务就崩溃,或者服务器弹出后响应丢失,该任务都已经不在待处理列表中。Redis 不会自动把它恢复回来,也不要求消费者确认完成。单独配置持久化,并不能修补这类应用层交接窗口。 Redis 6.2 开始提供 LMOVE,可以在一条原子命令中把任务从待处理列表转移到处理中列表;BLMOVE 是对应的阻塞版本。使用 Redis Cluster 时,两个列表键必须落在同一哈希槽;名称看起来相关,并不能保证满足这一条件。成功处理后,可通过 LREM 按唯一任务标识移除一个条目,作为应用约定的确认。对于长时间没有完成的任务,应用仍需定义恢复策略,因为列表不会自动为每个元素维护租约、时间戳或重试次数。 ...

四月 18, 2027 · 3 分钟 · XB Field Notes
观察、决策和执行面板,旁边是排队命令不会自动回滚的标题。

Redis 事务隔离执行,但不自动回滚

Redis 事务先在一条连接上积累命令队列,再进入一个隔离的执行窗口。MULTI 开始排队,EXEC 按顺序执行已接受的命令。这里的关键保证是其它客户端的命令不能插进执行过程,而不是后面一条失败时,前面所有成功写入都能自动撤销。 入队与执行是两个阶段 QUEUED 表示服务端接受了命令,等待后续执行,并不表示这条命令想完成的写入已经成功。Redis 在接受命令时就会检查命令是否存在、参数数量是否正确;依赖当前数据的错误,例如向字符串键执行列表操作,则可能直到 EXEC 才暴露。不能笼统地把它们都称为语法错误,因为两种失败对应的队列处理方式并不相同。 在队列中发送 GET,同样先收到 QUEUED,真正的值出现在 EXEC 的结果数组里。客户端不能先读取这个排队 GET 的实际结果,再据此决定本次事务后面的命令。需要在应用内判断时,可先用 WATCH 保护读取依赖;必须在读取与写入之间分支时,可考虑有界的服务端脚本。 客户端缓冲又增加了一层。redis-py 的普通事务流水线先把命令放进本地列表,直到 execute 才发送 MULTI、各条命令与 EXEC。set 方法返回流水线对象,并不等于服务端已确认入队。下面使用专用连接读取原始响应,是为了观察协议的实际阶段,而不是把客户端的链式调用误当成服务器执行结果。 隔离从执行时开始 MULTI 不会替调用方提前锁住相关键。一个客户端仍在构造队列时,另一个客户端完全可以更新这些键。队列中的 GET 读取的是它执行时的值,不是 MULTI 开始时保存的快照;后面的 GET 也能看到本事务前面 SET 的写入。把不被插队说成启动时的一致快照,会误导依赖旧值的业务判断。 事务必须始终使用同一条连接,而且这条连接不能混入无关业务。如果连接池为 MULTI 和 EXEC 分别分配连接,它们就不属于同一个事务状态。应用代码应优先使用客户端提供的事务抽象;实验直接访问连接,只用于核验已固定版本的协议行为。 ...

三月 31, 2027 · 3 分钟 · XB Field Notes
一个常驻有序、以分数决定先后次序的集合。

有序集合按分数维持成员次序

Redis 有序集合保存唯一的字节串成员,每个成员关联一个数值分数。分数决定排序,但分数本身可以重复。这个模型可以用于排行榜和时间索引,却不会把分数自动解释成过期时间。成员是否存在、处于哪个名次、什么时候清理,是需要分别定义边界的三件事,不能因为数据有序就假定这些动作已经一并完成。 成员不能重复,分数可以相同 ZADD 在同一条命令内插入新成员,或者更新已有成员的分数。重复使用同一个成员名不会产生第二条记录。默认返回值统计新增成员数,所以返回零也可能表示分数已经成功更新。加上 CH 后,返回值才统计新增成员以及确实改变了分数的更新;不能把原分数不变的重复写入算作一次实际变化。 NX 只允许新增成员,XX 只允许更新已有成员。GT 限制替换后的分数必须更高,但单独使用它仍然允许插入新成员。如果契约是“只提高已有成员的分数”,就需要同时指定 XX 和 GT,并检查执行结果,而不是把命令没有语法错误等同于排行榜发生了变化。是否允许新成员入榜和是否允许降分,是两项不同的条件。 ZRANGE 默认按分数从低到高排列,同分时再按成员的二进制字节字典序排序,不是按插入先后,也不是按本地语言的排序规则。REV 会同时反转分数顺序和同分成员顺序。名次从零开始,按名次查询的两端都包含在结果内,因此从零到九最多得到十个成员。ZRANK 和 ZREVRANK 分别报告对应方向上的位置。 名次和分数是两套坐标 按名次查询选择的是位置,ZRANGE BYSCORE 选择的是数值范围。分数边界默认包含自身,加左括号前缀后才排除该端点;按分数查询再使用 REV 时,需要先给上界、再给下界。无论采用哪一种坐标,查询都不会预留成员,也不会冻结后续分页。两次正常读取之间插入一个成员,就可能让后面的所有名次发生偏移。 分数使用双精度浮点数。从负的二的五十三次方到正的二的五十三次方,这个闭区间内的整数都能被精确表示;超过范围后,相邻整数就可能映射到同一个分数。因此,如果要求每一纳秒都可区分,就不能直接把原始纪元纳秒时间当分数。精确标识可以放在成员数据中,分数采用什么单位、允许多大范围,应由业务精度要求明确决定。 坏掉的样子 按分数查询只是读取。存入时间戳,再查询最近一段窗口,不会自动删除更老的成员,也不会创建成员 TTL。ZREMRANGEBYSCORE 才会显式删除指定分数区间,键级过期则会删除整个集合。应用必须安排清理触发点,并明确截止分数本身是保留还是删除;如果没有清理动作,旧成员完全可能持续积累。 有序访问也不意味着返回多少结果都耗费相同时间。ZRANGE 的文档复杂度为 O(log N + M),其中 M 是返回成员数;按分数范围删除的成本同样取决于实际删除数量。超大响应和集中清理依然占用执行时间,不能用“已经排好序”忽略它们。分页不是快照,单条 ZADD 的原子性也不会把另外几条读取、判断和写入自动合成一个事务。 快速验证 原有实验记录使用 macOS、Python 3.14.4、Redis 8.10.1 和 redis-py 8.1.0;本轮审阅没有重新执行服务实验,下面的结果说明保留原有观察范围。准备 POSIX 环境、C 编译器、make、curl、tar 与 Python 3.10 或更新版本。安装步骤仅用 libc 构建 Redis 核心,并把客户端装入临时虚拟环境。 ...

三月 21, 2027 · 3 分钟 · XB Field Notes
一个 Redis 键组织相关字段,供按需读取与局部更新。

Redis 哈希把相关字段收拢在一个键下

Redis 哈希把多个字段名及其字节值放在一个键下,适合按属性独立读取或修改的对象,例如拥有姓名和积分的用户资料。HGET 与 HSET 各自都只需要一条命令,并不会因为多了一层字段组织,就天然比字符串多一次网络往返。类型选择的依据是实际访问粒度,而不是把内部结构想象成额外的远程查询。 只读取和更新目标字段 HGET 获取一个字段,HMGET 按请求顺序获取指定字段,HGETALL 则返回所有字段和值。Redis 不会自动把某个字段值解释为嵌套对象,不会把带点的字段名当作对象路径,也不会自动为任意属性组合建立查询索引。字段如何命名、值如何序列化,仍然由应用负责。 HSET 可以在一条命令里写多个字段,未指定的字段不会被替换。它的整数返回值统计新增字段数,而不是所有发生变化的值:更新已有字段即使成功,也可以返回零。HDEL 返回实际删除的字段数;最后一个字段删除后,整个哈希键也会消失。因此,不能用一个没有字段的哈希作为长期存在的占位对象。 字段缺失和空值是两种状态。redis-py 关闭自动解码时,HMGET 对缺失字段返回 None,对已存在但内容为空的字段返回空字节串。直接用真假值判断会混淆两者,应按明确的返回值区分。HGETALL 对小对象很方便,但处理量和响应大小会随整个哈希增长;调用方只需要几个属性时,应该直接读取这些字段,避免每次搬运全部数据。 过期时间分为两层 键级过期会删除整个哈希,HSET 会保留该键已有的截止时间。从 Redis 7.4 开始,还可以通过 HEXPIRE 和 HPEXPIRE 给单个字段设置期限。普通 HSET 覆盖字段时会清除这个字段自己的过期时间,HINCRBY 则保留被修改字段的已有期限。字段 TTL 返回 -1,只表示它没有独立的字段期限,不代表它能逃过父键到期后整体删除。 Redis 8.0 及更新版本提供 HSETEX,可以把字段值与字段期限放在同一条命令里写入;其 KEEPTTL 选项能在替换字段值时保留已有字段截止时间。先 HSET、再 HPEXPIRE 仍然是两条操作,中间失败会产生值已经变更、期限尚未附加的窗口。应根据希望保留、重置还是新建期限来选命令,并在采用字段过期能力前核对实际部署版本,不能把键级与字段级期限混为一谈。 ...

三月 13, 2027 · 3 分钟 · XB Field Notes
生效的订阅接收实时广播,错过的发布没有回放日志。

Redis 发布订阅广播消息但不保留回放

Redis 发布订阅面向消息发布时已经生效的订阅广播,不会为断线的听众保留收件箱。它适合可以被后续状态替代的实时通知,但 PUBLISH 返回正数,并不证明某个工作进程完成了任务,也不表示离线消费者回来后能够补领消息。 订阅是一次握手 SUBSCRIBE 会先返回订阅确认,连接随后才能收到后续发布的消息。协调发布者与新听众时,应等待这份确认。仅仅把订阅请求发送出去还不够:Redis 处理请求之前,另一个连接就可能已经发布。重新连接和订阅只恢复未来的投递,不会填补之前的历史空白。 在持续在线的连接上,听众按发布顺序接收消息。这里的顺序反映 Redis 的处理顺序,不是多个独立生产者携带的业务时间戳。即使接收有序,把事件分发给并行处理器后,完成顺序仍可能改变。因此,声称发布订阅完全没有顺序是错误的;把有序接收扩大为业务副作用也必然有序,同样不成立。 多个听众各自收到广播,并不是竞争弹出同一个列表元素。频道订阅与匹配的模式订阅是两条独立投递路径。同时订阅 events 和 event*,一次发布就可能产生一份 message 和一份 pmessage;多个模式重叠也会如此。最多一次描述的是没有重试保证,不是跨订阅路径的业务去重承诺。 严格解释返回值 在本次验证的单机版本中,PUBLISH 分别统计频道和模式匹配的投递次数,因此两个客户端连接可以得到返回值三。固定版本的源码确实在两条路径分别递增计数。对于 Redis Cluster,官方说明计数只包含连接到发布节点的听众,因此它也不是整个集群的受众总数。 该响应不会等待处理器提交数据库事务、完成界面更新或确认业务完成。返回零只说明当时没有计入匹配接收者,返回正数不能证明处理成功。如果应用需要识别重叠订阅或生产者重复尝试造成的重复事件,应在消息中放入稳定的事件标识,不能用返回的数字替代业务确认。 坏掉的样子 订阅中断期间发布的消息会被错过。重连循环无法请求某个偏移之后的内容,因为发布订阅既无保留日志,也无回放游标。用于缓存失效时,一次通知丢失可能让旧值持续存在,直到另一个刷新机制介入;用于账单或任务派发时,则可能直接丢掉必须执行的工作。需要回放就应选择保留消息的队列或流,并明确恢复与持久化策略。 慢听众并不是一变慢就被随机丢掉几次读取。Redis 会缓存待发送数据,客户端输出缓冲触达配置的硬限制,或持续超过软限制后,连接可能被关闭。这限制了客户端缓冲占用,却会造成无法补读的订阅空白。应观察断连重连频率及缓冲占用,限制回调执行的工作量;调大缓冲并不等于获得送达保证。 快速验证 原有实验记录使用 macOS、Python 3.14.4、Redis 8.10.1 和 redis-py 8.1.0;本轮审阅没有重新执行服务实验,下面的结果说明保留原有观察范围。准备 POSIX 环境、C 编译器、make、curl、tar 与 Python 3.10 或更新版本。安装步骤仅用 libc 构建 Redis 核心,并把客户端装入临时虚拟环境。 ...

二月 17, 2027 · 3 分钟 · XB Field Notes