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

原文 The Day Soft Deletes Caused Chaos.

在我作为软件工程师的生涯中,最大的失误莫过于 5 年前合并了一个表面看起来毫无问题的 Pull Request。

简而言之:在生产级系统中,我们不应该使用软删除 ------ ---这是一个我通过一次严重的失误痛苦学到的教训。那次失误导致同一场音乐会的座位能被无限次地售卖给不同的买家。

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

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

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

重大事件警示

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

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

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

这一操作移除了模型上负责处理软删除逻辑的 Paranoia 库,即通过设置一个 deleted_at 字段为当前时间来标记记录为已删除。我当时没有意识到的是,这个库还自动地从 ORM 查询中过滤掉了所有标记为软删除的记录。

由于自动排除软删除记录的机制失效,且数据迁移尚未完成,后台任务开始错误地处理那些已经被标记为「已删除」的座位锁定记录 ------ 这导致一些已经成功售出的座位被错误释放,再次对外开放预订!

我永远也忘不了,当我意识到发生了什么事时,那种心沉如石、惶恐不安的感觉。

这导致了在如 Shawn Mendes 音乐会这样的事件中,同一座位被多次出售的情况。放大到多个座位、多个活动,影响简直糟透了。

诚然,软删除机制并非唯一的问题所在。对于这次改动,我本应采取更多的预防措施,比如分步骤进行。CI/CD 流程中的自动化测试本应该能够发现这个错误,但它却还是漏了。万幸的是,这个领域的监控做得很好,问题几乎立刻就被发现并解决了。但是,由此造成的影响和后果还是非常严重的,包括数百个需要退款的重复预订、取消的订单、发送给受影响顾客的道歉邮件,还有一份深夜编写的事故总结报告。

不要使用软删除!

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

复杂性增加

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

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

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

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

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

数据完整性

通过软删除来处理应用层中的删除操作会失去数据库的一个优势,即数据库会尽力为您保持数据的有效性。

vbnet 复制代码
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 数据更改跟踪终极指南》概述了 PostgreSQL 的不同策略。我还推荐大家查看我参与的一个名为 Bemi 的开源项目,该项目旨在通过插入数据库和应用程序(支持大量不同的 ORM,如 Bemi-rails )来简化这一过程,从而自动提供上下文数据变更记录。

底线

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


💡 更多资讯,请关注 Bytebase 公号:Bytebase

相关推荐
南城花随雪。11 分钟前
硬盘(HDD)与固态硬盘(SSD)详细解读
数据库
儿时可乖了12 分钟前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite
懒是一种态度14 分钟前
Golang 调用 mongodb 的函数
数据库·mongodb·golang
天海华兮16 分钟前
mysql 去重 补全 取出重复 变量 函数 和存储过程
数据库·mysql
gma9991 小时前
Etcd 框架
数据库·etcd
爱吃青椒不爱吃西红柿‍️1 小时前
华为ASP与CSP是什么?
服务器·前端·数据库
Yz98762 小时前
hive的存储格式
大数据·数据库·数据仓库·hive·hadoop·数据库开发
苏-言2 小时前
Spring IOC实战指南:从零到一的构建过程
java·数据库·spring
Ljw...2 小时前
索引(MySQL)
数据库·mysql·索引
菠萝咕噜肉i2 小时前
超详细:Redis分布式锁
数据库·redis·分布式·缓存·分布式锁