profile 哈希含 name 值 Ada L、visits 值 0 及空字节串字段。箭头用 HGET 选取 visits,其余字段仍在记录中。

Redis hash 用一个键装多个字段

Redis hash 是一个键下面可按字段访问的记录,不是自动校验模式的对象,也不是天然支持嵌套路径的文档。当调用方只需要部分属性、又不想替换整条记录时,它很有用。但是否划算取决于访问模式、字段大小和返回值语义,而不是只要选择 hash 类型,就自动得到廉价读写或完整业务一致性。 只读取调用方真正需要的字段 HGET 获取一个字段,HMGET 按请求顺序获取选定字段,HGETALL 则返回整个映射。在 redis-py 的二进制响应模式下,缺失字段表示为 None,存在但为空的值表示为空字节串。直接判断真假会把这两种情况错误地合并;对不存在的键执行 HMGET,同样得到对应位置的缺失值,并不会自动创建 hash。 字段名和字段值本质上都是字节。名字中含点号仍是字面名称,Redis 不会把 name.first 解释为进入嵌套对象再找 first。同样,包含 JSON 文本的字符串键还是字符串,HGET 不会替你解析 JSON。序列化和内容校验属于应用职责,“没有提供属性”和“明确写入空值”也应保留区别。 更新操作有精确的返回语义 HSET 只修改指定字段,未指定的字段保持不变。一次多字段 HSET 是一个命令,但其整数回复统计新增字段数量,而不是成功修改的总数。替换已有 name 完全可能成功返回零;如果把零视为失败,就会制造多余重试、错误告警,甚至触发不必要的补偿动作。 HINCRBY 在一个命令内完成整数递增,字段不存在时从零开始,拒绝非整数值以及超出有符号六十四位范围的结果。它避免的是这一项递增的读改写竞争,并不把外围的业务检查也变为原子操作。分别执行 HGET、在客户端计算、再执行 HSET,仍可能覆盖其他客户端已完成的修改。 HDEL 的返回值统计实际删除的字段,删除最后一个字段会让整个键消失。因此,不能靠空 hash 长期占位,表示某条实体存在。键级过期同样会删除其字段;当前 Redis 还支持字段级截止时间,不能沿用“hash 字段绝对不能单独过期”的旧结论。下面的实验没有设置任何过期时间,也不把结果作为生命周期验证。 ...

二月 24, 2028 · 3 分钟 · XB Field Notes
发布订阅只向生效订阅广播实时事件;独立 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
流载荷、组位置与 PEL 分别变化。投递在确认前推进位置;确认只移除待处理引用。XAUTOCLAIM 转移归属,可能重复模拟副作用,也找不回已删除的载荷。图示来自原有实验。

Redis Streams 让一个消费组分担工作

Redis Streams 消费组把新条目分配给具名消费者,并记录尚未确认的投递。它不保证业务副作用恰好发生一次,不会阻止保留策略删除数据,也不会自行恢复被遗弃的任务。将其用作工作队列前,应区分组的读取位置、待处理条目与流里实际保留的数据。 流载荷、组位置与 PEL 分别变化。投递在确认前推进位置;确认只移除待处理引用。XAUTOCLAIM 转移归属,可能重复模拟副作用,也找不回已删除的载荷。图示来自原有实验。 读取位置与待处理任务不同 XGROUP CREATE jobs workers 0-0 从现有历史之前起步;使用 $ 则从当前末端之后开始。同组成员分担新投递,不同消费组拥有各自独立的投递状态。并发工作进程应使用不同的消费者名字。最后投递位置属于整个组,并不是每个消费者分别提交一个偏移。 未使用 CLAIM 或 NOACK 的普通 XREADGROUP 以 > 请求新条目,投递后将它们记入待处理列表 PEL。组的位置在新条目投递时就推进,发生在 XACK 之前。XPENDING 统计的是已经投递但尚未确认的条目,不是从来没有投递过的积压。因此,待处理计数为零时,组里仍可能有大量未读工作。 XACK 把指定 ID 从该组 PEL 移除,不删除流里的记录,也不推进最后投递位置。确认可以乱序完成,其他组依然可以读取保留下来的条目。XREADGROUP 使用 0-0 时读取的是这个具名消费者自己的待处理历史,不是在请求新工作,也不会把另一位消费者名下的待处理消息交给它。 恢复需要显式决策 反复执行普通 > 读取,不会重试被遗弃的 PEL 条目。XAUTOCLAIM 能把满足条件的待处理条目转给请求方,参数必须包含流、组、消费者、最小空闲毫秒数和起始 ID。之后应使用返回游标继续扫描,直到返回 0-0。某一批认领结果为空,并不代表扫描已经完成;过一段时间再次扫描,原来尚未达到空闲阈值的条目也可能变得可认领。 ...

二月 5, 2028 · 3 分钟 · XB Field Notes
以经度 15、纬度 37 为中心的 200 公里示意圆包含 Catania 与 Palermo;ASC COUNT 1 选取 Catania。图不是按比例道路地图。

Redis 按距离处理地理位置

Redis 地理命令为带名称的位置建立索引,可以直接回答附近有哪些点,不必把全部坐标传回应用。它适合能够接受球面距离模型的场景,但不会提供道路路线、考虑海拔的空间距离、任意多边形分析,也不会保证查询到的每个位置仍然新鲜。这些不是查询速度更快就能顺带解决的问题。 经度必须放在纬度前面 GEOADD 按经度、纬度、成员名的顺序接收参数。有效经度从负一百八十度到正一百八十度,纬度则限制在大约负 85.05112878 度到正 85.05112878 度之间,极区不属于该索引支持的范围。如果把两个各自仍合法的数值交换位置,命令可能正常成功,却把成员放到完全不同的地方。 索引底层是有序集合,分数保存五十二位地理哈希编码,不能用普通排行榜分数覆盖这些位置编码。成员是唯一的:更新同名成员只会改变位置,不会增加一份记录。GEOADD 默认统计新增成员数,加 CH 才统计新增及实际变化。删除位置使用 ZREM,读取存储坐标则使用 GEOPOS;不要把一次更新返回零当成更新失败。 坐标编码具有有限精度,因此 GEOPOS 不一定原样返回输入的小数。GEODIST 使用理想球面地球计算表面距离,不是穿过地球内部的直线弦长,更不是驾车路线。默认单位为米,也可以选择公里、英尺和英里。任何一个端点缺失时,返回空结果,而不是把不存在的位置当作距离为零的重合点。 把查询条件写成明确契约 Redis 6.2 开始提供 GEOSEARCH,可以围绕已有成员或显式经纬度查询。BYRADIUS 指定圆形范围,BYBOX 则按所选单位指定轴向对齐矩形的宽和高。新的查询应使用该命令,而不是已弃用的 GEORADIUS 系列。请求中的单位与结果解释必须一致,否则正确的数值也会被应用解读成错误的距离。 ASC 按距离从近到远排列,COUNT 限制返回数量。要找最近的若干个结果,应使用 ASC 和 COUNT,避免 ANY;ANY 允许找到足够数量后提前结束,不能保证拿到查询区域内全局最近的那些点。另外,返回数量少不代表查询一定便宜:范围巨大且点位密集时,即使只要求少量结果,也可能需要检查很多候选。 FROMMEMBER 查询可以包含作为中心的成员自身。如果业务要找“附近的其他人”,就需要处理这个结果,而不是直接把第一条当作别人。WITHDIST 返回所选单位下的距离,WITHCOORD 返回坐标。结果反映的是查询时索引里存放的数据,并不等于已经独立确认某台设备或某个商户此刻就在该位置。 坏掉的样子 文档说明,在边界情况下,球面近似与地球实际形状之间的误差可以达到约百分之零点五。因此,“城市尺度”并不自动意味着误差小于一米。应用应明确可接受误差及距离边界如何判定;测绘、安全关键接近判断、道路规划或严格地理围栏,可能需要更合适的空间模型,不能只凭一次示例距离看起来合理就验收精度。 ...

一月 25, 2028 · 3 分钟 · XB Field Notes
两组请求回复梯形图对比每条命令都等待与先成批发送再收集回复。

用流水线减少等待:示意。

测量 Redis 流水线的实际收益

流水线减少的是发送 Redis 命令之间的等待,实际收益必须放到具体负载里测量。远程同步循环可能主要等待网络延迟,本地循环也可能主要消耗在客户端处理、进程调度和套接字读写。两种情形都不能直接推出“任何环境都会快几倍”的结论,更不能用一次本机数字替线上系统承诺容量。 比较等价的工作 对于 N 条命令、每批 B 条,简单同步模型把约 N 次请求响应等待减少为向上取整的 N/B 次批次等待。但命令执行、响应字节、编码和解析并没有消失;一批数据仍可能跨越多次套接字写入和多个网络包。因此模型中的等待次数,不是抓包数量,也不是对真实链路 RTT 的测量。 改变 B 时,要固定命令、键分布、负载大小、连接数、客户端版本和持久化设置。否则看起来更快,可能只是做了更少工作、关掉了持久性,或者增加了并发客户端。若要测普通流水线,应显式使用 transaction=False,因为 redis-py 默认流水线还会加入 MULTI/EXEC。 下面批量一使用普通同步客户端循环,不是每次只装一条命令的流水线包装器。这样比较回答的是“应用当前请求响应循环能否从批处理受益”,而不是测出 Redis 的最大可持续吞吐,更不是剥离客户端后得到服务器的固有命令成本。 计入应用实际支付的成本 计时应覆盖命令准备,直到最后一个结果收齐。忽略构建请求或解析响应,会低估大批次给应用带来的成本。示例把缓冲、执行和结果收集都放进计时区间,而把重置数据与正确性断言放在外面;两种路径遵守相同的计时边界,才方便比较。 每个配置先预热,再变换测试顺序并重复测量,同时报告波动。这里的三次正式运行只是一个容易复现的小型比较,不是严谨的容量研究。笔记本上的调度和其它工作仍会影响结果,单次中位数不能直接升级成服务等级目标。 吞吐量也不是单条请求延迟。总耗时除以命令数,得到的是平均完成成本,不是百分之九十五或九十九请求的延迟分布。等待凑满一批,可能让最早到达的请求在执行前就排队;本例立即构造批次,没有测量真实到达节奏带来的积攒延迟。 坏掉的样子 巨型批次可能提高每秒命令数,却同时带来难以接受的停顿和内存增长。应限制条数、请求与响应字节,以及等待成批的时间;这些上限应来自代表性负载,而不是不分命令类型就照搬几百或几千条。 如果继续增大批量已经没收益,要检查客户端 CPU、响应解码、服务端命令成本、带宽和竞争负载。流水线能够通过分摊读写开销提高实际服务吞吐,但并不消除所有瓶颈。分片是可能的架构选择之一,不是曲线变平之后唯一合理的答案。 计速度也必须检查失败。丢掉回复或把错误当成功,可以轻易制造漂亮但无意义的速率。下方关闭网络自动重试,并核对每一个递增结果的完整序列。真实断连可能留下不确定的执行范围;从最后一条已知回应之后重发,对递增也不是天然安全的恢复方式。 测试负载只应留在它自己的实例中。代码会反复重置并递增一个计数器,若把目标改成共享生产库,操作风险就发生了变化。这里使用本地 Unix 套接字,也有意排除了 TCP、TLS 和远程网络路径,所以读数不能冒充跨机房链路测量。 假想梯形图解释逻辑等待。夹具批次计数是算术,不是实测 RTT、延迟或容量;历史速度测量仍未核实。 快速验证 原文报告的环境是 macOS、Python 3.14.4、Redis 8.10.1 和 redis-py 8.1.0;本轮查阅的证据中未找到匹配的执行日志,也没有重新运行。需要 POSIX、C 编译器、make、curl、tar 和 Python 3.10 或更新版本。必须启用断言,不能使用 -O 或 PYTHONOPTIMIZE,否则检查及其中必需的调用都会被跳过。 ...

一月 20, 2028 · 2 分钟 · XB Field Notes
原有 Lua 实验中,旧 owner-a 请求删除时保留当前 owner-b;owner-b 匹配时才删除,键不存在则返回零。未捕获的 redis.call 错误保留此前写入但停止后续语句;明确的 NOSCRIPT 表示代码缺失。

Redis Lua 脚本执行时不被穿插

Redis Lua 脚本可以先读取数据、判断条件,再更新数据,而不让另一个客户端的命令插进这些步骤之间。但这不意味着每个语句都会成功,也不意味着后面发生错误时会撤销前面的写入,更不意味着执行结果自动具备持久性。原子执行与失败回滚,是必须拆开理解的两个问题。 原有 Lua 实验中,旧 owner-a 请求删除时保留当前 owner-b;owner-b 匹配时才删除,键不存在则返回零。未捕获的 redis.call 错误保留此前写入但停止后续语句;明确的 NOSCRIPT 表示代码缺失。 仅为原有实验示意,本轮未重跑。 把判断放在数据旁边 例如,仅当令牌仍属于当前持有者时才删除它。客户端先 GET、稍后再 DEL,中间可能已经被新持有者替换了值;随后删除就误伤了别人。脚本可以在同一次执行中比较当前值,匹配才删除。因此旧持有者即使带着之前读到的令牌来操作,也不会直接删除新持有者的值。 脚本应通过 redis.call 访问 Redis 数据,这正是脚本接口的用途。沙箱不提供普通文件系统或网络访问,不能把邮件、支付或文件读写搬进脚本。所有会访问的键都应通过 KEYS 传入,普通值用 ARGV,不要根据数据库里读出的内容临时拼接键名。Redis Cluster 中的常规多键使用还要求相关键位于同一哈希槽。 ...

一月 11, 2028 · 3 分钟 · XB Field Notes
列表按三个位置保留 b、a、b;有序集合按分数 10 与 20 排列唯一成员 a 和 b。

位置与分数:示意。

Redis 列表与有序集合,一字之差用途不同

Redis 列表保存允许重复值的有序序列;有序集合保存唯一成员,并按数值分数排序。真正有用的区别不是谁总是更快或更省内存,而是应用需要按位置处理序列、维持唯一成员、查询分数范围,还是需要另一种数据模型才能表达的组合语义。 先定义什么算不同条目 使用 RPUSH 时,列表按追加顺序保留值,也保留重复项。随后 LPOP 移除最旧值,RPOP 则移除最新值。换一种推入与弹出端的组合,顺序也会变化,因此不能不看生产者命令就断言列表天然是先进先出。多个工作进程并行执行时,移除顺序也不保证业务完成顺序。 ZADD 为每个成员关联一个分数。再次加入同名成员,是更新分数,不是增加第二条记录。默认返回值统计新增成员数量,所以返回零也可能是成功更新。如果直接拿重复的事件载荷当成员名,多次出现会合并成同一个成员;当每次出现都代表独立任务时,这样建模就会丢掉业务区别。 分数允许并列,同分时按成员字节排序,不按到达顺序,也不是本地化文字排序;REV 同时反转同分成员的先后。排行榜可能接受这个规则,时间索引则可能需要独立事件标识和明确的并列处理方式。分数使用浮点数,因此还要选择能够保留所需数值精度的单位,不能把排序正确等同于任意精度都能表达。 位置和分数选择的是不同内容 列表不只支持两端访问。LINDEX 可以读取一个位置,LRANGE 返回包含边界的位置区间。到达内部位置需要遍历;两端访问便宜,不代表返回大量元素也无需付出相应成本。对只读取头尾、长度受控的短列表来说,保留一个小窗口完全可能是适合的方案。 ZRANGE 默认解释为从零开始的排名位置,结束位置也包含在内。BYSCORE 才把两个边界解释为数值分数,在分数前加左圆括号则排除该边界。官方给出的范围读取复杂度是 O(log N + M),其中 N 为成员数,M 为返回数量。因为有索引,就认为大结果集或很深的偏移扫描没有成本,是不成立的。 持续变化的两种结构都不会让按排名分页变成快照。插入或分数更新可能移动后续页的位置,分开的请求就可能重复或遗漏。选择容器之前应先决定读取契约。内存开销也没有普遍相近的保证:底层表示、成员数量、载荷长度和配置都会改变结果。容量是决策因素时,应测量具有代表性的数据,而不是根据类型名字推断。 坏掉的样子 时间戳分数不是成员的过期截止时间。ZREMRANGEBYSCORE 显式删除数值区间,LTRIM 显式保留按位置选择的窗口;两者都不会因为时间过去或有人读取而自动清理。追加与清理如果分成两个命令,就是两个操作,保留逻辑仍然需要自己的触发与并发策略。 有序集合也不是完整的延迟任务调度器。ZPOPMIN 会立即移除最低分成员,即使分数表达的是未来时刻。先查到期成员、再用另一条命令删除,可能让多个工作进程选到同一任务。原子认领、处理确认以及遗弃任务恢复都是额外要求,不会因为分数放了时间戳就自然具备。 列表弹出同样发生在业务处理完成之前。移除后消费者失败,若没有合适的处理协议,任务就可能丢失。需要序列操作时选列表,需要按分数索引唯一成员时选有序集合;不能从任何一种排序方式推导出送达保证。数据结构选择与业务处理协议应分别判断,避免用容器替代完整语义。 列表位置保留每次出现,有序集合分数排列唯一成员。未来分数示例说明立即移除,不代表完整调度器。来源夹具未重跑。 快速验证 原文报告的环境是 macOS、Python 3.14.4、Redis 8.10.1 和 redis-py 8.1.0;本轮查阅的证据中未找到匹配的执行日志,也没有重新运行。需要 POSIX、C 编译器、make、curl、tar 和 Python 3.10 或更新版本。必须启用断言,不能使用 -O 或 PYTHONOPTIMIZE,否则检查及其中必需的调用都会被跳过。 ...

一月 4, 2028 · 3 分钟 · XB Field Notes
时钟与满容量容器表示两种不同未命中原因;正 TTL 不能保护符合候选条件的键免于容量逐出。

Redis 的过期是截止时间,逐出是容量策略

过期与逐出都可能让一次成功查找变成未命中,但它们回答的是不同问题。过期服从键的截止时间,逐出则在内存压力下移除符合条件的键。延长 TTL 不能阻止容量驱动的逐出,扩大内存也不能纠正应用写错的截止时间。必须先确定实际发生了哪种变化,才能判断应该检查写入协议、调整容量,还是重新设计缓存用途。 时间与容量是两个独立维度 带有过期设置的键在 Redis 中称为 volatile。Redis 会在访问和主动清理过程中检查过期键,但这不承诺键一定存活到截止时间。显式删除、替换以及适用的逐出策略,都可能提前改变结果。因此,不管最初设定的期限还有多久,缓存客户端都必须能处理未命中,不能把较长 TTL 当作某条数据不会丢失的保证。 maxmemory 是 Redis 进行内存管理决策时使用的统计上限,不是操作系统 RSS 的硬上限。复制与追加日志缓冲区可能包含不计入逐出判断的内存,进程开销、分配器行为和持久化任务也需要容量余量。大型命令还可能短暂超过配置值。规划时既要考虑缓存预算,也要考虑运行整个进程的环境,不能把两者设成同一个数字就认为足够。 先确定候选范围,再讨论排序 allkeys 类策略不管键有没有 TTL,都会把它纳入候选;volatile 类策略则只考虑带过期设置的键。在各自候选范围内,LRU 偏向最久未访问的键,LFU 使用访问频率信息,random 类策略随机选择。Redis 的 LRU、LFU 是近似算法,不保证全局精确的受害键顺序。volatile-ttl 偏向剩余寿命较短的条目,也不是只清除已经到期的数据。 noeviction 在无法满足内存限制时拒绝增长内存的命令,而不是为了腾空间丢键;已有数据仍可能正常读取。但它不是持久性保证,普通过期、显式删除、进程故障和持久化配置仍然分别起作用。应检查当前实例实际配置,不能根据某种部署形式或集群名称,推断所有环境都使用相同默认策略。 volatile 策略如果找不到带 TTL 的候选键,在这种场景下就表现得像 noeviction。选了 volatile-lru,不会自动把持久键变成可丢弃数据。反过来,把会话或权威记录与可重建缓存混放在 allkeys 策略之下,它们也会成为候选。键名前缀只是命名约定,不会给逐出算法建立独立的数据保护边界。 坏掉的样子 未命中突然增加,可能来自过期、逐出、主动失效或键构造变化。需要把 expired_keys、evicted_keys 的增量,与同期请求负载和写命令结果一起看。这些是累计计数,应比较有限时间区间,并考虑重启或重置;仅看到逐出数上升,还不能知道哪个应用功能失去了哪些数据。 ...

十二月 26, 2027 · 2 分钟 · XB Field Notes