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 会使已确认数据失去发布契约要求的同步界限。
已验证假设¶
- 假设:日期桶最大序号可作为全局标量 trim。
- 证据:旧
promote_expired_mailbox_buckets.lua在桶过期时以max_seq更新user_trim_seq,随后删除该桶的 max/expiry 证明。 - 结果:否定。日期与序号乱序时不存在由单桶 max 导出的连续前缀;删除证明后也 无法在后续游标请求中重新判定缺口。
- 假设:现有元数据可区分旧格式和缺失证明。
- 证据:
retention_format_version=1不含“证明永久保留、不可提升标量”的语义; 缺少成对bucket_max_seq/bucket_expires_at同样无法判断物理缺口。 - 结果:否定,必须 fail closed。
- 假设:
aof_enabled=1足以说明 AOF 持久性满足发布要求。 - 证据:Redis 官方
redis.conf将appendfsync独立配置,支持always、everysec与no;后者不执行 fsync。 - 结果:否定,启动期还须读取
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.luacrates/qim-store/lua/append_batch.luacrates/qim-store/src/redis/store.rscrates/qim-store/tests/redis_store.rscrates/qim-common/src/redis_runtime.rscrates/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或未知值同样不再可启动。