Course Builder个人学习工作台 · 本地模式
DDIA 第二版:从数据模型到分布式系统设计

24 · 数据责任:删除、解释与反馈循环

目标:沿数据流规划用户数据的删除与用途边界,并检验重放会不会恢复已删除信息。

数据系统最终影响谁

活动平台记录了报名、地点、兴趣和同行关系。这些字段对排期有帮助,也可能暴露个人行为。可靠地保存数据并不等于应该无限期保存;权限、保留期、用途和删除要像索引一样进入设计。

推荐系统如果只根据历史报名推送热门活动,冷门组织者得到更少展示,再产生更少报名,最终“历史不受欢迎”的判断被系统自己强化。这是反馈循环。总点击率提高也不能证明每个群体都受益,应观察曝光机会、错误案例和用户纠正渠道。

隐藏姓名不等于匿名。城市、时间、活动组合可能重新识别一个人,哈希后的稳定 ID 也可能继续关联行为。不同意个性化推荐的用户仍应有清晰的基础使用路径。此处讨论工程和产品设计,不把某个示例保留期当成法律要求。

删除不能只改当前页面

第 23 课依靠历史重建视图。如果只从当前用户列表删掉一人,旧事件还在,下一次重建就可能把数据带回来。删除流程需要覆盖事实、派生视图、索引、缓存、导出和备份恢复后的再清理。

保存 data_responsibility.py。使用最小的删除标记集合演示防复活规则;它不证明实际备份或训练模型已完成删除。先预测重建时 u1 是否重新出现。

events = [
    {"id": "r1", "user": "u1", "activity": "a1"},
    {"id": "r2", "user": "u2", "activity": "a1"},
]
deleted_users = {"u1"}

def rebuild(rows, deleted):
    return {row["id"]: dict(row) for row in rows if row["user"] not in deleted}

old_view = rebuild(events, set())
old_view.pop("r1")
naive_rebuild = rebuild(events, set())
safe_rebuild = rebuild(events, deleted_users)
assert "r1" in naive_rebuild
assert "r1" not in safe_rebuild
assert set(safe_rebuild) == {"r2"}
print("naive_restores_deleted:", "r1" in naive_rebuild)
print("safe_users:", sorted(row["user"] for row in safe_rebuild.values()))
uv run --python 3.12 --no-project data_responsibility.py

预期输出:

naive_restores_deleted: True
safe_users: ['u2']

删除标记本身也与用户有关,应只保留防复活必要的信息、控制访问并定义生命周期。不能既彻底丢弃防复活信息、保留可恢复旧备份,又无条件保证以后不会恢复该用户。某些数据可通过去标识聚合继续保留,但小样本和可连接字段仍要评估。

引导练习:① 将 u2 也加入删除集合;② 更新结果断言为空视图;③ 重建任意次数,仍应为空。独立练习:备份恢复时只有旧事件,没有最新删除标记,写出恢复服务前的顺序及失败时的行为。

参考恢复方案

先隔离恢复出的实例,不向用户开放查询;从可信的较新删除记录恢复删除边界,清理事实与派生表示,再重建并核对,最后恢复服务。无法取得需要的删除记录时,应保持相关数据不可访问并进入人工处理,不把“恢复了备份”当作可以立即公开。删除传播需要记录各系统完成状态,不能只记录请求已接收。

把决定留给可负责的人

若系统依据行为给用户打“高风险”标签,至少要知道标签来自哪份数据、用于什么决定、错误如何纠正、谁负责处理申诉。概率性预测不能当成某个个体的确定事实,也不能用“算法算出来的”替代责任归属。工程师可以通过减少采集、缩短保留和限制跨用途关联,减少问题发生的机会。

实际删除需逐个检查副本、备份、导出和训练产物。加密密钥销毁只有在密钥范围、所有副本和访问路径都设计正确时才有相应效果,不能替代数据目录与验证。本课不要求实现这些设施,但要求把未知依赖列成可解决的设计问题。

如果删除后列表为空但搜索仍能命中,检查下游是否收到删除;若重建后复活,检查删除边界是否进入重建输入。在 design.md 完成字段用途、保留、删除传播与推荐反馈检查四项内容。

参考资料