跳至内容
数据工程

数据库中的原子性:可靠事务的支柱

了解为什么原子性对数据库可靠性至关重要。学习其工作原理、在各系统中的实现方式,并查看转账等真实案例。
Marie Fayard's photo
Marie Fayard
已更新 · 10分钟 阅读

使用 AI 探索

ChatGPTClaudePerplexity

创建您的免费账户

实践并学习路径你的进步。

使用 Google 继续
继续操作即表示您接受我们的《使用条款》和《隐私政策》,并同意您的数据存储在美国。

原子性是数据库系统中的核心概念。它意味着一次事务要么完全成功,要么根本不执行。如果中途有任何一步失败,整个事务都会被回滚,仿佛从未发生过。

为什么这如此重要?想象一下从一个银行账户向另一个账户转账。您点击“发送”,应用加载了一秒钟,然后出错了。钱从您的账户扣掉了,却没有到达对方账户。现在它就……不见了。原子性正是为防止这种情况而设计的。

您可能听说过 ACID。它是一组用于确保数据库不出问题的四个关键特性。原子性位列其首,理由很充分:没有它,其余(一致性、隔离性和持久性)就意义不大。

在本文中,我们将探讨原子性的含义、工作方式,以及为什么它在银行、电商等日常系统中至关重要。我们还将看看不同数据库如何实现原子性,以及如何设计在出错时也能良好应对的原子化系统。

什么是原子性?

原子性意味着数据库中的一次事务是不可分割的。要么完整执行,每一步都按计划进行;要么完全不执行。不存在中间状态。没有半途而废的更新,没有部分数据被保存,也没有那种有些成功有些失败的怪异“灰色地带”。

Atomicity

DBMS 中的原子性。来源: Arpit Bhayani

如果事务的某一部分失败,整个操作就会被回滚,仿佛什么都没发生过。这使原子性成为强有力的安全保障。没有它,您就会不断冒着数据损坏或不一致的风险,尤其是在需要按顺序完成多步操作的系统中。

下面来看几个原子性决定系统成败的真实场景:

示例 1:银行转账

假设您要从活期账户转出 100 美元到储蓄账户。

该事务包含两个步骤:

  1. 从活期账户扣除 100 美元
  2. 向储蓄账户增加 100 美元

没有原子性,就可能出现第一步完成而第二步失败的情况,原因可能是网络抖动或服务器崩溃。恭喜,您的钱就这样消失在虚空中。原子性确保两个步骤要么都成功,要么都不执行。如果出错,100 美元会原地不动。

示例 2:库存与订单处理

您正在网上购买最后一张限量版黑胶唱片。系统需要:

  1. 将库存减少一件
  2. 确认您的订单

若没有原子性,系统可能先减少库存,但在确认订单前崩溃。现在商店以为没货了,但没人拿到唱片。

我网购时遇到这种情况的次数之多令人惊讶。我通常会直接关掉标签页,祝他们的工程师好运,但我大概率不会再尝试在那个网站购买。连核心购买功能的可靠性都无法保证,又怎能让我放心他们处理我的个人信息?

示例 3:机票预订系统

订机票通常不只是选个座位。系统需要:

  1. 分配座位
  2. 更新座位可用性
  3. 向您的卡收费

如果第二步失败而第三步成功,您就为一个不存在的座位付了钱。这显然不理想。

示例 4:同步数据库与搜索索引

假设您的应用在主数据库中更新了某个商品的描述。与此同时,它还应该更新搜索索引,以便用户能用新描述找到该商品。

如果数据库更新成功而索引更新失败,用户会看到过期的结果,甚至完全找不到该商品。

原子性为何重要

那么,为什么原子性如此重要?

事务被中断的原因有很多。可能会遇到停电、服务器崩溃,或在半途中网络超时。若不强制原子性,事务可能只执行了一部分。此时数据库就会处在尴尬状态:有些变更生效了,有些没有,您也没有简单的方法来恢复干净。

这正是数据问题最狡猾的地方。用户起初可能没发现异常,但时间一长,问题就开始显现:分析数据对不上号、订单消失、账目不平衡,或者客服工单激增,因为人们找不到刚保存的内容。

原子性通过以下方式避免这些问题:

  • 事务要么完全发生,要么完全不发生
  • 即使中途失败,系统也能干净地恢复
  • 即使在系统崩溃或并发访问期间,数据仍保持一致

它也与其他 ACID 特性紧密相关。没有原子性,一致性就无从谈起,因为当一半数据缺失时您无法强制规则;隔离性也会破坏,因为其他用户可能会看到不完整事务的部分数据。

以下场景尤为重要:

  • 多步骤事务,其中一个动作依赖另一个动作。
  • 多行或多表更新,需要保持相关数据同步。
  • 分布式系统,事务的不同部分发生在不同机器或数据库上。

原子性在实践中的工作方式

“要么全有,要么全无”的简单背后,是出人意料的复杂机制。数据库执行事务时,会主动跟踪、准备并保护每一个步骤。

以下是其高层次的工作原理:

事务生命周期

  1. 开始事务:系统标记事务开始,将即将发生的更改与数据库的其余部分隔离。
  2. 执行操作:包括所有插入、更新、删除,或您正在进行的各种 SQL 操作。
  3. 提交或回滚:如果每个操作都成功,事务被提交,更改永久生效;若有任何失败,整个事务回滚。

日志与预写

为确保原子性,数据库会在真正写入之前,将即将发生的更改写入日志。这些日志单独存储,充当备份计划。

如果系统在事务中途崩溃,它可以参考日志,回滚任何未完成的更改,安全地重放已完成的部分,将数据库恢复到干净一致的状态。

这种方法有时称为预写日志(WAL)或日志式文件系统,是崩溃恢复的关键组成部分。

回滚与恢复

当事务失败时,数据库不会“自暴自弃”,而是会小心地撤销更改。被修改的行会恢复到原始状态,事务持有的任何锁会被释放,并利用日志逆转部分更新。

在崩溃的情况下,恢复流程会在重启时启动。它们会重放日志、处理未完成的事务,确保数据库在没有“半成品”状态的情况下,从中断处继续运行。

性能 vs. 原子性

不出意外,这一切都是有代价的。原子性会带来:

  • 加锁:防止他人在事务中途修改相同数据
  • 开销:来自日志与回滚机制
  • 死锁:多个事务相互等待对方释放资源

这就是为什么需要谨慎设计大事务。如果一次性做太多,可能会拖慢整个系统,或遇到并发问题。

主流数据库系统中的原子性

原子性是所有主流数据库的核心概念,但其实现方式可能会因底层存储引擎和设计理念略有差异。下面看看 PostgreSQL 和 MySQL(InnoDB)如何确保事务正确工作。

PostgreSQL

在任何数据被永久写入之前,PostgreSQL 会创建预写日志(WAL),记录每一次预期的更改(正如我们之前提到的)。该日志被安全存储,如果事务过程中出现问题,就作为恢复计划使用。

当事务提交时,PostgreSQL 会检查 WAL,确认所有更改完整且有效,然后最终应用更新。如果中途崩溃,数据库会使用该日志回滚未完成的工作。

好处在于,这一切都在幕后自动进行。对开发者而言非常无缝:您编写事务,PostgreSQL 就会保证原子性,而无需您处理任何复杂性。

在事务完全提交之前,其他用户看不到任何更改。这保持了一致性,并避免了多用户环境中的“半保存”怪象。

PostgreSQL WAL

PostgreSQL WAL。来源: AlgoMaster

如果您想亲自动手创建数据库来测试事务,请查看我们的 PostgreSQL 课程。

MySQL(InnoDB)

MySQL 的 InnoDB 存储引擎同样支持原子事务,其方法兼顾可靠性与性能。

与 PostgreSQL 类似,InnoDB 使用事务日志在提交前跟踪更改。如果出现问题,日志会用于将一切回滚到事务前的状态,确保不会有部分更新“漏网”。

InnoDB 做得特别好的一点是将写入分组,这有助于减少磁盘 I/O 并提升性能,同时仍然保护原子性。它还使用 undo 日志在回滚时撤销更改,并使用 redo 日志进行崩溃恢复。

就像在 PostgreSQL 中一样,您可以依赖数据库为您处理这些机制。无需编写任何自定义回滚代码。

工程师与数据架构师的关键要点

在自己的项目中使用原子事务时,有几件事需要牢记。总体思路是将原子性视为默认选项,而非奢侈品。如果您在构建同时修改多个地方数据的系统(几乎所有系统都如此),以原子性为前提进行设计,将为您节省时间、心力,甚至可能避免凌晨三点的告警。

为任务选择合适的数据库

并非所有数据库以相同方式处理原子性,存储引擎也参差不齐。如果原子性对您的场景至关重要(如金融、物流、用户状态管理),请确保数据库支持完整的 ACID 事务。如果您使用的是 MySQL,这点尤为重要,因为存储引擎的选择很关键。

为失败而设计

假设事情一定会出错——因为它们确实会。将多步骤操作包裹在事务中,并妥善处理错误。明确使用 BEGIN、COMMIT 和 ROLLBACK 语句,或使用在底层安全处理这些操作的框架。

大多数现代 ORM 和数据访问层都提供内置的事务支持。例如:

  • 在 Node.js 中,Prisma 和 TypeORM 等库允许您将逻辑包裹在 transaction() 调用中,在幕后处理提交/回滚。

  • 在 Python 中,SQLAlchemy 提供上下文管理器(with session.begin():),确保安全提交和回滚。

  • 在 Java 中,Spring 等框架允许用 @Transactional 注解方法,使数据库以原子方式处理一切,甚至跨多个函数调用。

这些工具让“默认安全”的构建变得更容易。它们降低了忘记回滚或错误管理提交逻辑的风险,尤其是在应用变得更复杂时。

在性能与可靠性之间取得平衡

原子性会带来一些开销,尤其是在大事务中。尽可能拆分庞大操作,避免锁住整张表,并保持事务简短而聚焦。长时间运行的事务会增加争用、死锁和用户不满的风险。

理解依赖与隔离

正如我们之前所见,原子性并非孤立存在,而是与一致性和隔离性共同发挥作用。确保相关更改一并发生,并考虑并发事务可能的相互影响。使用适合您负载的隔离级别,避免过度复杂化。

DBMS 中的 ACID 特性。来源:Datacamp

用贴近现实的失败场景进行测试

不要只测“快乐路径”。模拟应用在事务中途崩溃,或查询超时的情况。您可以使用 failpoint 或模拟数据库故障,看看系统在压力下的表现。

结论

如果您构建的系统需要多件事情同时发生,那么原子性不是可选项,而是必需品。无论是账户间转账、更新库存,还是系统间同步,原子性都能确保数据不会陷入破碎或半成品状态。

如果您想更深入学习,不妨参加我们的 Database Design 课程。您将学习如何用 SQL 设计数据库,更高效地处理、存储和组织数据。

如果您对更多实用的数据库设计文章和教程感兴趣,请查看我们的数据库范式化系列:


Marie Fayard's photo
Author
Marie Fayard

我是一名具有产品思维的技术负责人,专注于帮助早期初创公司从首个原型成长到实现产品与市场匹配,并持续向前发展。我对人们如何使用技术充满好奇,热爱与创始人及跨职能团队紧密合作,将大胆的想法变为现实。不在打造产品的时候,我会在世界的新角落寻找灵感,或在瑜伽馆放松减压。

FAQs

原子性是在硬件层面还是由数据库强制实现的?

原子性是由数据库引擎在软件层面实现的保证。然而,数据库确实依赖某些硬件行为(如按顺序刷新磁盘写入)来支持持久性与崩溃恢复。

原子性能否应用在数据库之外,比如应用逻辑或 API?

当然可以!虽然原子性是数据库中的形式化特性,但您可以在代码中应用同样的原则:把多步骤流程当作不可分割的单元。例如,您可以在 ORM 中使用事务、使用状态机或支持重试且安全的工作流,以确保跨服务边界的一致性。在处理支付、调度或第三方 API 等场景时尤为重要。

原子性与幂等性有何区别?

原子性关注确保一次事务要么完全发生,要么完全不发生。幂等性关注确保同一操作可以安全地重复执行而不改变结果。两者都很重要,尤其在 API 与分布式系统中。原子性保护多步骤操作的完整性,而幂等性有助于重试和容错。

主题
数据工程

在 DataCamp 学习数据工程

课程

Databricks 中的数据管理

3 小时
6.9K
在 Databricks 中学习数据管理,涵盖 Delta Lake、ACID 事务、模式强制和安全性。
查看详情开始课程

创建您的免费账户

实践并学习路径你的进步。

使用 Google 继续
继续操作即表示您接受我们的《使用条款》和《隐私政策》,并同意您的数据存储在美国。
查看更多