跳转至

Redis 日期桶裁剪与 AOF fsync 门禁诊断

日期:2026-08-23 状态:已修复,定向回归已通过

缺陷摘要

Redis 邮箱按 Entry.created_at 分日期桶,但 mailbox_seq 来自分发日志,二者没有 单调关系。旧实现把任一已到期日桶的 bucket_max_seq 写入全局 user_trim_seq,例如旧桶含 seq=100、新存活桶含 seq=50 时,会错误声称 seq < 100 已被裁剪。与此同时,运行时只检查 AOF 是否开启,没有验证 AOF 的 appendfsync 策略;no 会使已确认数据失去发布契约要求的同步界限。

已验证假设

  1. 假设:日期桶最大序号可作为全局标量 trim。
  2. 证据:旧 promote_expired_mailbox_buckets.lua 在桶过期时以 max_seq 更新 user_trim_seq,随后删除该桶的 max/expiry 证明。
  3. 结果:否定。日期与序号乱序时不存在由单桶 max 导出的连续前缀;删除证明后也 无法在后续游标请求中重新判定缺口。
  4. 假设:现有元数据可区分旧格式和缺失证明。
  5. 证据:retention_format_version=1 不含“证明永久保留、不可提升标量”的语义; 缺少成对 bucket_max_seq/bucket_expires_at 同样无法判断物理缺口。
  6. 结果:否定,必须 fail closed。
  7. 假设:aof_enabled=1 足以说明 AOF 持久性满足发布要求。
  8. 证据:Redis 官方 redis.conf 将 appendfsync 独立配置,支持 always、everysec 与 no;后者不执行 fsync。
  9. 结果:否定,启动期还须读取 CONFIG GET appendfsync。

根因

  • 直接原因:把按日期的局部最大值误当成按 mailbox_seq 的连续裁剪前缀。
  • 根本原因:保留证明没有表达其适用范围,读路径也未在物理 TTL 已删 key 时保留 “无法证明空洞天然存在”的失败语义。
  • 运行时直接原因:Redis 安全探测只读取 INFO persistence,遗漏独立的 AOF fsync 配置及其 ACL 失败路径。

最小修复

  • 元数据版本升级为 2。每个日期桶的 bucket_max_seq 与 bucket_expires_at 成对永久保留为缺口证明;读前脚本不再写入 TTL 驱动的 user_trim_seq,也不删除证明。
  • 当任一已过期桶的最大序号仍大于当前 after_seq 时,脚本返回该边界; RangeScan 返回 StoreError::CursorExpired。该策略刻意保守:即使请求上界较低, 只要无法证明该游标未跨过物理缺口,就绝不返回空成功。
  • 显式 TruncateBefore 的 user_trim_seq 保持原语义,仍独立参与游标校验。
  • 启动期在 AOF 已开启后精确要求 CONFIG GET appendfsync 返回唯一的 appendfsync=everysec 或 appendfsync=always;no、未知值、缺字段和 ACL/ 命令错误全部拒绝启动。QIM_REDIS_ALLOW_UNSAFE_DEV=1 只跳过 AOF、fsync 与 noeviction 检查,Cluster 检查仍先执行。
  • slot 计算/分组改为仅测试私有模块,移除公开 re-export,避免将其误解为 Cluster 写入拆分能力。

影响文件

  • crates/qim-store/lua/promote_expired_mailbox_buckets.lua
  • crates/qim-store/lua/append_batch.lua
  • crates/qim-store/src/redis/store.rs
  • crates/qim-store/tests/redis_store.rs
  • crates/qim-common/src/redis_runtime.rs
  • crates/qim-store/src/redis/{mod.rs,slot.rs}

回归风险与迁移

  • 已有 retention_format_version=1 或任何缺少成对桶证明的 mbmeta 不可安全原地 升级:新的 append/scan 将拒绝,需迁移或由分发日志重放。这是有意的发布行为变化, 不能用补写一个版本号掩盖已经删除的证明。
  • 新格式不再把 TTL 自动删除折叠进持久标量 trim;过期桶涉及的旧游标会更保守地收到 CURSOR_EXPIRED 并走既有 REBUILD,而不会把存活低序号桶误标为已裁剪或空成功。
  • Redis 生产 ACL 必须允许服务启动期执行 CONFIG GET appendfsync;缺权限现改为 readiness 失败。生产配置 appendfsync no 或未知值同样不再可启动。