到目前为止,我们已经介绍了复制和分区技术,复制技术(包介后面绍的共识算法)提升了系统的容错性,而分区技术提升了系统的扩展性,这两项技术解决的是数据的*“物理问题”。除此以外,分布式系统中的数据访问还经常面临着“逻辑问题”,此时就需要本章将要介绍的事务*技术来解决:
- 复制 (Replication):主要目标是高可用性和数据冗余。通过在不同的节点上存储相同的数据副本,当某个节点发生故障时,系统可以从其他副本继续提供服务。它回答的是:“我的数据会不会因为一台机器挂掉而丢失或无法访问?"。
- 分区 (Partitioning / Sharding):主要目标是可扩展性。当单台机器的存储或计算能力无法承载全部数据和请求时,我们将数据水平切分到多个节点上。它回答的是:“我的系统如何处理不断增长的数据量和访问压力?”
- 事务 (Transaction):主要目标是数据操作的正确性。它将一系列操作打包成一个不可分割的逻辑单元,保证这些操作要么全部成功,要么全部失败,并且在并发执行时互不干扰。它回答的是:“我如何确保一系列相关的操作在任何情况下(并发、故障)都能保持数据的正确状态?”
为什么只有复制和分区是不够的?我们来看以下几个典型场景:
场景一:转账操作(原子性缺失的灾难)
用户A要向用户B转账100元。这个操作至少包含两个步骤:
- 从用户A的账户中扣除100元。
- 给用户B的账户增加100元。
在一个缺少事务技术的分布式系统中,可能会出现以下的问题:
- 发生故障:系统在执行完第1步后突然崩溃(比如数据库节点宕机)。此时,A的钱被扣了,但B没收到钱。钱"凭空消失"了。即使数据有多个副本(复制技术),但所有副本记录的都是这个"中间状态"的错误数据。
- 跨分区操作:假设用户A的数据在分区1,用户B的数据在分区2。这个转账操作需要一个协调者来分别通知两个分区执行操作。如果分区1成功扣款,但分区2因为网络问题或自身故障未能成功收款,同样会导致数据不一致。
但是如果使用事务技术,事务的原子性 (Atomicity) 保证了这一系列操作是一个"全有或全无"的原子单元。系统会确保"扣款"和"收款"这两个步骤要么都完成,要么如果中间有任何差错,所有已经完成的步骤都会被回滚(Rollback),系统状态恢复到操作开始之前。这样就绝不会出现钱平白消失或多出来的情况。
场景二:并发抢购(隔离性缺失的混乱)
一个电商网站上,某件商品只剩下最后1件库存。此时,两个用户(用户C和用户D)同时点击了"购买"按钮。
在一个缺少事务技术的分布式系统中,可能会出现以下的问题:
- 用户C的请求到达,系统读取库存为1。
- 在用户C的请求完成"减库存"操作之前,用户D的请求也到达了,系统读取库存仍然为1。
- 用户C的请求执行"减库存”,库存变为0。
- 用户D的请求也执行"减库存",库存变为-1(超卖)。
- 最终结果是:两个用户都以为自己买到了商品,而系统库存出现了负数。这造成了严重的业务逻辑错误。
但是如果使用事务技术,事务的隔离性 (Isolation) 保证了并发执行的多个事务之间互不干扰,就像它们是串行执行的一样。当用户C的事务开始处理库存时,它会锁定该数据。用户D的事务在用户C的事务完成(提交或回滚)之前,无法修改库存数据,它要么等待,要么读取到一个旧的值然后操作失败。这样就保证了最终只有一个人能成功买到商品。
场景三:系统崩溃后的数据完整性(持久性缺失的风险)
一个订单系统刚刚完成了一笔重要订单的创建,所有数据已经写入。在数据从内存刷到磁盘的瞬间,服务器断电了。
在一个缺少事务技术的分布式系统中,如果系统依赖于操作系统的缓存写入,那么这笔订单数据可能就永久丢失了。尽管你可能收到了"操作成功"的响应,但数据并未真正持久化。
事务的持久性 (Durability) 保证了一旦事务被提交,其结果就是永久性的。即使系统崩溃,数据也能够被恢复。这通常通过预写日志(Write-Ahead Logging, WAL)等机制来实现:在修改数据本身之前,先将操作记录到持久化的日志中。
我们可以把分布式系统想象成一个大型的、跨部门的公司项目:
- 复制技术保证了每个部门都有核心成员的备份,或者有完整的项目文档副本。如果一个核心成员请假或离职,备份人员可以顶上,保证部门工作不中断(高可用性)。
- 分区技术把项目拆分给不同的部门(前端部、后端部、数据库部),这提高了整个公司的处理能力(可扩展性)。
- 事务技术是项目管理中的一个"工作流"或"流程规定"。比如,“产品上线"这个工作流必须包含:1. 代码部署成功, 2. 数据库迁移成功, 3. CDN缓存刷新成功。这个流程规定了:这三件事必须全部搞定,这个"产品上线"才算真正成功。 如果任何一步失败,整个上线流程就要回滚到初始状态(比如代码回滚,数据库恢复),绝不能停在一个"部署了一半"的尴尬状态。
除此以外,事务还为应用开发者提供简化的编程模型。试想一下,如果没有事务的各种保证,应用开发者需要:
- 手动处理各种失败场景下的回滚逻辑。
- 手动加锁来避免并发冲突。
- 编写复杂的补偿代码来修复部分失败导致的数据不一致。
这会使业务代码变得异常复杂、容易出错且难以维护。事务将所有这些复杂性封装起来,向开发者提供了一个简洁的抽象:“你可以把一系列操作当作一个不可分割的单元来执行,系统保证其ACID属性。” 这极大地提升了开发效率和应用可靠性。
在本章中,我们先从单机数据库事务开始展开对事务ACID特性的讨论,进而延展到涉及多个服务的分布式事务。在分布式系统中,由于可能跨多个服务(例如一次电商的购买行为中中涉及订单服务、库存服务、支付服务),面临着更大的挑战:
- 网络不可靠,可能出现延迟、分区、丢包等情况。
- 节点会失败,服务器可能会宕机、进程可能会崩溃。
- 数据不一致的风险,一部分操作成功,一部分失败(例如,订单创建成功,但扣库存失败)。
7.1 深入理解ACID #
在单机数据库中,可能出现各种故障:
- 数据库正在写入数据时系统崩溃,在重启数据库之后,如何从故障数据中恢复。
- 多个客户端同时写入多个数据,例如本章开头的电商购买例子中,
为了保障应用开发者不被这些故障困扰,事务技术一直据库系统的首选机制。事务技术向应用开发者提供了ACID的安全保证,我们首先来了解这些特性。ACID最早在论文中被提出,做为精确描述数据库的容错机制而定义,是以下四个单词的缩写:
- A(Atomicity):原子性,事务保证了对多个数据的修改,要么同时成功,要么同时失败。
- C(Consistency):一致性:一个事务在执行前后,数据库都必须处于正确的状态,满足完整性约束。
- I(Isolation):隔离性,多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
- D(Durability):持久性,事务处理完成后,对数据的修改就是永久的,即便系统故障也不会丢失。
总体而言,ACID属性提供了一种机制,使每个事务都作为一个单元,完成一组操作,产生一致结果,事务之间彼此隔离,更新永久生效”,从而来确保数据库的正确性和一致性。