第 12 / 23 篇

第11章 删除与可见性:防止旧对象重新出现

删除比更新更容易被忽视

插入和更新都有 after,删除的 after 却为空。一个只把非空 after 写入查询库的消费者,看起来可以正常同步大部分业务,却永远不删除旧对象。用户常常先在搜索结果里发现下架内容,团队才意识到同步契约缺了一个操作。删除必须从消息模型、存储状态与查询过滤三处共同设计。

本实验消费 d 包络后保存 deleted=true,查询投影时固定排除删除记录。快照期间删除 id 二,停机期间删除 id 十,两个阶段最终都完整对账。对账列表来自源表实际仍存在的行,因此任何未被隐藏的残留商品都会使比较失败。我们没有只统计删除事件次数,而是检查其业务可见结果。

为什么保留删除水位

假设商品先创建、再更新、最后删除。如果恢复程序从较早位置回放创建事件,而查询库已经物理删除对应记录,简单 upsert 可能把它重新插入。保留删除记录及最后 offset,可以在重放时拒绝旧事件。这是查询状态的防倒退策略,不是把删除变成永远保存业务内容的理由。

生产若要清理删除标记,需要确认旧事件不会再合法回放到当前世代,或者使用独立持久水位阻止复活。可以在重建后整体回收旧世代,让新世代从当前源事实出发;不能在缺少恢复边界时单独删除所有 tombstone,再继续使用任意旧 offset 重放。清理与回放必须有共同协议。

软删除与物理删除是不同事件

active=0 仍然是一行数据。本书投影保留它,并让业务查询决定是否展示。若把 active=0 当成删除,需要在快照、更新和重建三条路径采用相同规则;否则初始快照可能过滤掉它,之后一次普通更新又把它写回来。源库字段语义必须先于消费者条件判断确定。

物理删除则意味着源表不再有该 id。连接器 d 事件负责把变化传到投影,后续新快照不会再包含它。若上游执行表截断或批量替换,事件类型和连接器支持可能不同,不能直接套用逐行 DELETE 的实验结论。本书没有测试 TRUNCATE,生产接入时应明确禁止或单独验证这类操作。

Kafka tombstone 不会清理所有副本

记录空值可以影响压缩 topic 的旧键保留,但 broker 清理是异步策略,其他存储和备份也不会因此自动删除。删除包络、查询隐藏、日志清理、缓存失效和备份保留属于不同层级。若业务要求可验证的数据清除,需要建立覆盖各层的生命周期协议,并记录完成证据。

实验使用合成商品数据,只验证查询投影中的删除传播和旧事件防倒退。它没有处理用户个人数据删除、日志脱敏或备份擦除,不应从一次查询结果为空推出合规结论。读者可以借助同样的事件身份和世代概念建立追踪,但需要另加真实删除范围与验收方法。

权限撤回不能依赖异步投影的乐观假设

企业检索中,权限变化往往比普通内容修改更敏感。若撤回授权后仍允许旧投影命中,就可能产生短暂越权。设计上可以在读取时查询权威权限,或使用短时缓存与版本栅栏,让旧授权无法单独决定访问。权限数据和内容数据若经不同 topic 传播,还要考虑到达顺序。

这些是设计讨论,本书没有搭建身份系统,也没有权限撤回传播实验。因此我们不声称已验证“撤权立即生效”。商品 active 字段不是访问控制列表,隐藏商品也不是授权协议。若把本书链路用于企业知识库,必须增加真实权限服务、撤回事件、读取拒绝和延迟边界的独立测试。

删除故障怎样定位

先确认源库是否真实删除,再确认连接器是否产生 d 包络,随后检查同键 tombstone 与投影最后 offset,最后检查查询是否固定排除 deleted。不要直接在查询库手工删行作为唯一修复,因为下一次重放可能再次出现。若逻辑错误已经影响大量对象,修正消费者后新建世代重建通常比逐行补丁更容易验证。

本章练习

设计一个删除标记清理策略,明确允许回放的最早位置和旧世代保留时间。然后解释为什么“Kafka 已开启 compaction”不能作为投影不会复活旧对象的充分证据。