300块钱从Alice转到Bob,数据库崩了。重启后查账,700和800原封不动。这不是运气,是Durability(持久性)在兜底——ACID四个字母里,它最像那个沉默的保险丝,平时看不见,熔断时救命。
转账实验:commit之后,崩溃也追不回
实验 setup 很朴素。两张表,Alice 1000块,Bob 500块。BEGIN 开启事务,Alice 扣300,Bob 加300,COMMIT 提交。这时候拔电源、杀进程、把服务器从三楼扔下去——重启后查账,结果还是 700 和 800。
持久性的核心承诺就一句:commit 之后的写入,必须比硬件故障更长寿。
原文作者做了对照实验。崩溃发生在 commit 之前?改动全丢,事务回滚,数据库假装什么都没发生。崩溃发生在 commit 之后?日志已经落盘,重启时根据重做日志(redo log)把内存里的脏页重新刷进去,数据原样恢复。
这解释了为什么金融系统敢在 commit 瞬间告诉用户"转账成功"。后台可能正在着火,但承诺已经硬化成磁盘上的比特。
预写日志:数据库的"黑匣子"逻辑
实现持久性的技术细节,本质是"先写日志,后写磁盘"的套路。PostgreSQL 叫 WAL(Write-Ahead Log),MySQL InnoDB 叫 redo log,名字不同,结构类似:顺序追加、页号+偏移量+新旧值、LSN(日志序列号)单调递增。
commit 时,数据库保证日志已经 fsync 到磁盘——注意是日志,不是数据页。数据页可以慢慢刷,反正崩了能从日志重建。这种设计用顺序写替代随机写,把机械盘的 IOPS 从几百提升到几万。
原文没提具体数据库类型,但实验现象符合 WAL 机制。作者观察到"after commit → data stays safe",正是日志先行策略的外在表现。如果数据库傻乎乎地直接写数据文件,commit 和落盘之间那几毫秒的窗口期,足够让 300 块钱蒸发。
持久性不是"数据在磁盘上",而是"commit 和数据在磁盘上之间存在因果律"。
CAP 语境下的隐藏代价
分布式系统里,持久性会和其他属性打架。单节点数据库靠本地磁盘 + fsync 就能满足 Durability,但主从架构下,commit 要不要等从库落盘?等,延迟爆炸;不等,主库挂了丢数据。
MySQL 的半同步复制(semi-sync)默认等一个从库 ack,PostgreSQL 的 synchronous_commit 可以配成 remote_apply。这些选项都是在持久性和可用性之间找平衡点。原文的实验是单节点场景,避开了这些麻烦,但也提醒我们:分布式语境下,"commit 成功"四个字背后可能有不同的硬度等级。
云厂商的 RDS 常把"跨可用区同步"当卖点卖,本质上就是在卖更高等级的持久性承诺。用户多付的钱,买的是 commit 之后、AZ 级灾难也不丢数据的保险。
开发者容易踩的坑:隐式 commit 和连接池
持久性保证有个前提:你真的调用了 commit。很多 ORM 框架的默认行为是自动提交(autocommit),每条语句隐式包裹事务。这看似方便,实则把长事务拆成无数短事务,锁持有时间变长,并发性能下降。
更隐蔽的是连接池问题。应用代码里开了事务,忘记 commit 就归还连接到池子。下一个拿到连接的线程,可能发现上个事务还在进行中,或者干脆被连接池检测到异常而回滚。这种情况下,数据没丢,但业务逻辑已经乱了——持久性保住了,一致性却被破坏。
原文作者的实验流程很规范:显式 BEGIN、显式 COMMIT、崩溃后手动重连验证。这种"不信任任何中间层"的验证习惯,是排查数据丢失嫌疑时的最佳实践。
持久性防的是硬件故障,防不住逻辑错误。删库跑路再 commit,数据一样持久——只是持久地错了。
最后留个思考题:如果你的系统要求"commit 后数据必须存在于三个物理隔离的机房",而网络分区导致只有两个机房可达,这时候该拒绝写入、还是降级接受风险?这不是技术问题,是产品决策——你愿意为 99.999% 的持久性,牺牲多少可用性?
热门跟贴