查看原文
其他

警示:软删除引发泼天大祸!

BB 仔 Bytebase 2024-07-09

原文 The Day Soft Deletes Caused Chaos
地址 https://blog.bemi.io/soft-deleting-chaos/
在我作为软件工程师的生涯中,最大的失误莫过于 5 年前合并了一个表面看起来毫无问题的 Pull Request。
简而言之:在生产级系统中,我们不应该使用软删除 —— —这是一个我通过一次严重的失误痛苦学到的教训。那次失误导致同一场音乐会的座位能被无限次地售卖给不同的买家。

所谓软删除,是一种在不真正执行删除操作的情况下保留数据的简便方法,仅通过标记一个「DELETE」标志来实现。

你在表格中增加了一个新的列,这样在进行「删除」操作时,实际上是更新了这条数据的状态,并在进行数据查询时自动排除那些标记为已删除的数据。

尽管这种做法初看起来简单易行,但它隐藏着潜在的风险 —— 如果不小心让一些本不应该被展示的数据泄露出去,后果可能非常严重。

重大事件警示

我在一家活动票务公司工作时,创建了一个与此类似的拉取请求:

在座位预订流程中,顾客可以在结账过程中暂时锁定座位 5 分钟。这个过程是通过一个后台任务实现的,它会在时间到后删除锁定记录,使得座位重新可被预订。我当时的任务是将这种基于「软删除」的锁定机制迁移到一个新的数据库集合中,这个集合专门用来存放被删除的记录。

问题就出在了一行代码上:

这一操作移除了模型上负责处理软删除逻辑的 Paranoia 库 (https://github.com/rubysherpas/paranoia?ref=blog.bemi.io),即通过设置一个 deleted_at 字段为当前时间来标记记录为已删除。我当时没有意识到的是,这个库还自动地从 ORM 查询中过滤掉了所有标记为软删除的记录。
由于自动排除软删除记录的机制失效,且数据迁移尚未完成,后台任务开始错误地处理那些已经被标记为「已删除」的座位锁定记录 —— 这导致一些已经成功售出的座位被错误释放,再次对外开放预订!
我永远也忘不了,当我意识到发生了什么事时,那种心沉如石、惶恐不安的感觉。
这导致了在如 Shawn Mendes 音乐会这样的事件中,同一座位被多次出售的情况。放大到多个座位、多个活动,影响简直糟透了。
诚然,软删除机制并非唯一的问题所在。对于这次改动,我本应采取更多的预防措施,比如分步骤进行。CI/CD 流程中的自动化测试本应该能够发现这个错误,但它却还是漏了。万幸的是,这个领域的监控做得很好,问题几乎立刻就被发现并解决了。但是,由此造成的影响和后果还是非常严重的,包括数百个需要退款的重复预订、取消的订单、发送给受影响顾客的道歉邮件,还有一份深夜编写的事故总结报告。

不要使用软删除!

即便在像 GDPR 这样的法规监管下,我们想要保留被删除数据的倾向是可以理解的。开发者可能出于合规、报告、分析的需要,或者仅仅是希望有一个后备方案 —— 以防万一误删数据或需要排查已删除记录的错误。想象这样的场景:一个顾客不慎删除了一张至关重要的发票,或者一个社交媒体用户删去了一条违反规则的评论。在某个宽限期内保留这些被删除的数据看似有其价值。然而,软删除策略实际上带来的问题要比它解决的更多。

复杂性增加

软删除像病毒一样扩散,让数据查询变得异常复杂。虽然应用程序的 ORM 层通常会自动排除被标记为「已删除」的记录,这种看似方便的做法却可能在手动编写复杂 SQL 查询时造成重大疏漏。正如我所经历的那样,你可能会得到不精确的结果,甚至可能暴露敏感信息,或基于片面信息做出错误判断。诚然,创建数据库视图似乎是一种更安全的做法,但这仍然增加了不必要的复杂性和额外的负担。

墨菲定律:任何可能出错的事情都会出错

索引、唯一约束和外键关系也都需要考虑「删除」状态,这使得它们的创建和维护更加复杂。

为 active users 的 email 字段创建唯一索引

即便引入了部分索引,软删除还是可能引起表的膨胀,不利地影响表的大小和性能。在高流量的环境下,这个问题可能更加突出,可能需要进行性能调整或数据分区来保持效率。

数据完整性

通过软删除来处理应用层中的删除操作会失去数据库的一个优势,即数据库会尽力为您保持数据的有效性。
ERROR: delete on table "users" violates foreign key constraint "orders_user_id_fkey" on table "orders"
DETAIL: Key (id)=(456) is still referenced from table "orders".
数据库外键违规错误
自行执行参照完整性可能容易出错,并会增加大量的开发和维护开销。

软删除的替代方案

软删除的另一种替代方法是将删除的数据存档到历史表中。这样做仍然很简单,而且可以消除软删除带来的长期责任和维护负担。在删除之前,可以将删除的记录插入到一个单独的表中。

删除前归档数据的事务
如果不想在整个代码库中手动归档数据,最好的办法就是在数据库层建立审计跟踪。《PostgreSQL 数据更改跟踪终极指南》(https://blog.bemi.io/the-ultimate-guide-to-postgresql-data-change-tracking/) 概述了 PostgreSQL 的不同策略。我还推荐大家查看我参与的一个名为 Bemi 的开源项目 (https://github.com/BemiHQ/bemi?ref=blog.bemi.io),该项目旨在通过插入数据库和应用程序(支持大量不同的 ORM,如 Bemi-rails )来简化这一过程,从而自动提供上下文数据变更记录。

底线

远离软删除。虽然它们看上去似乎是处理已删除数据的便捷方法,但实际上,它们就像一个即将爆炸的定时炸弹。这是我几年前吃过的苦头,绝对是你不愿意重蹈覆辙的教训。相比之下,使用历史记录或审计表是一个更为明智的选择。这种做法不仅更整洁、更安全,而且长远来看能够避免无数的麻烦。

HN 热帖|难以想象,20 年前代码版本管理是如何做的

2024 最热门开源 GitOps 工具盘点
Bytebase 签约 PropertyGuru,助力东南亚最大地产科技平台跨国多地数据库变更自动化
我们使用 Postgres 构建多租户 SaaS 服务时踩的坑

继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存