07

事务

数据库的原子操作

阅读量:5 · 预计 12 分钟读完

ACID隔离级别快照隔离
阅读进度4%

第七章:事务

导读

事务是数据库系统中保证数据一致性的关键机制。它将一组操作打包成一个原子单元,要么全部成功,要么全部失败。事务的存在,使得开发者可以专注于业务逻辑,而不必担心并发访问和系统故障带来的数据不一致问题。

本章将深入探讨事务的ACID特性,分析各种隔离级别和并发控制机制。我们将学习如何处理幻读、写 skew等并发问题,理解不同隔离级别的权衡。

通过本章的学习,你将理解:

  • ACID特性的含义和实现
  • 各种隔离级别的区别
  • 并发控制机制的原理
  • 分布式事务的挑战
  • 事务的性能权衡

核心概念详解

7.1 ACID特性

ACID是事务的四个核心特性的缩写:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。

7.1.1 原子性(Atomicity)

原子性保证事务中的所有操作要么全部成功,要么全部失败。如果事务中的某个操作失败,整个事务会被回滚,数据库状态恢复到事务开始之前。

示例

sql
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

如果第二个UPDATE失败,第一个UPDATE也会被回滚,账户1的余额不会减少。

实现方式

  • Undo Log:记录数据修改前的状态,用于回滚
  • 补偿操作:对于无法回滚的操作(如发送邮件),使用补偿操作撤销

7.1.2 一致性(Consistency)

一致性保证事务执行后,数据库从一个一致状态转换到另一个一致状态。一致性是由应用程序和数据库共同保证的。

示例

  • 账户余额不能为负数
  • 订单总金额必须等于商品总价
  • 外键约束必须满足

实现方式

  • 约束(Constraints):如主键约束、外键约束、唯一性约束
  • 触发器(Triggers):在数据修改时自动执行检查
  • 应用层检查:在应用层进行业务逻辑检查

7.1.3 隔离性(Isolation)

隔离性保证并发执行的事务互不干扰。不同的隔离级别提供不同程度的隔离保证。

并发问题

  • 脏读(Dirty Read):读取到其他事务未提交的数据
  • 不可重复读(Non-Repeatable Read):同一事务中两次读取同一数据,结果不同
  • 幻读(Phantom Read):同一事务中两次执行相同查询,第二次查询返回了第一次查询没有的行

隔离级别

  • 读未提交(Read Uncommitted)
  • 读已提交(Read Committed)
  • 可重复读(Repeatable Read)
  • 串行化(Serializable)

7.1.4 持久性(Durability)

持久性保证一旦事务提交,其结果就会永久保存,即使系统故障也不会丢失。

实现方式

  • Write-Ahead Log(WAL):在修改数据之前,先将修改记录写入日志
  • 数据刷盘:将数据从内存写入磁盘
  • 复制:将数据复制到其他节点

权衡

  • 持久性越强,性能越低
  • 可以通过配置选择不同的持久性级别
  • 例如:PostgreSQL的synchronous_commit参数

7.2 隔离级别

7.2.1 读未提交(Read Uncommitted)

特点

  • 事务可以读取其他事务未提交的数据(脏读)
  • 性能最高,隔离性最低

适用场景

  • 对数据一致性要求不高的场景
  • 统计分析、报表生成

7.2.2 读已提交(Read Committed)

特点

  • 事务只能读取其他事务已提交的数据
  • 避免脏读,但可能出现不可重复读和幻读
  • 大多数数据库的默认隔离级别

实现方式

  • 读取时获取数据的最新已提交版本
  • 使用MVCC(多版本并发控制)

适用场景

  • 大多数OLTP应用
  • 对一致性要求中等的场景

7.2.3 可重复读(Repeatable Read)

特点

  • 事务中多次读取同一数据,结果相同
  • 避免脏读和不可重复读,但可能出现幻读
  • MySQL InnoDB的默认隔离级别

实现方式

  • 事务开始时获取数据的一个快照
  • 使用MVCC,事务始终读取快照版本

适用场景

  • 需要保证读取一致性的场景
  • 大多数OLTP应用

7.2.4 串行化(Serializable)

特点

  • 事务执行效果与串行执行相同
  • 避免所有并发问题(脏读、不可重复读、幻读、写 skew)
  • 性能最低,隔离性最高

实现方式

  • 两阶段锁(2PL):使用读写锁,保证事务串行执行
  • 可串行化快照隔离(SSI):使用MVCC,检测并 abort 冲突事务

适用场景

  • 对一致性要求极高的场景
  • 金融系统、库存系统

7.3 并发控制机制

7.3.1 锁(Locking)

锁是最经典的并发控制机制。

锁类型

  • 共享锁(Shared Lock):多个事务可以同时持有,用于读操作
  • 排他锁(Exclusive Lock):只有一个事务可以持有,用于写操作

两阶段锁(2PL)

  • 增长阶段:事务只能获取锁,不能释放锁
  • 收缩阶段:事务只能释放锁,不能获取锁
  • 保证可串行化

死锁

  • 两个或多个事务互相等待对方释放锁
  • 解决方法:超时、死锁检测、等待图

优势

  • 实现简单
  • 保证可串行化

局限

  • 性能开销大
  • 可能出现死锁
  • 锁粒度难以选择

7.3.2 多版本并发控制(MVCC)

MVCC通过维护数据的多个版本,实现并发控制。

工作原理

  • 每次修改数据时,创建一个新的版本
  • 每个事务读取数据时,选择一个合适的版本
  • 不同事务可以读取不同版本的数据

版本选择

  • 读已提交:每次读取时,选择最新的已提交版本
  • 可重复读:事务开始时,选择一个快照版本,始终读取该版本

优势

  • 读写不冲突,性能好
  • 不会出现死锁
  • 支持读己之所写

局限

  • 存储开销大,需要维护多个版本
  • 可能出现写 skew 和只读事务异常

7.3.3 可串行化快照隔离(SSI)

SSI是MVCC的一种扩展,提供可串行化隔离级别。

工作原理

  • 使用MVCC,事务读取快照版本
  • 检测事务之间的冲突
  • 如果检测到冲突,abort 其中一个事务

冲突检测

  • 读-写冲突:事务A读取的数据被事务B修改
  • 写-写冲突:事务A和事务B修改同一数据

优势

  • 读写不冲突,性能好
  • 保证可串行化
  • 不会出现死锁

局限

  • 可能出现事务重试
  • 实现复杂

7.4 并发异常

7.4.1 脏读(Dirty Read)

定义:事务读取到其他事务未提交的数据。

示例

事务A: BEGIN;
事务A: UPDATE accounts SET balance = 200 WHERE id = 1;
事务B: SELECT balance FROM accounts WHERE id = 1;  -- 读取到200
事务A: ROLLBACK;
事务B: -- 读取到的200是无效的

解决方法:使用读已提交或更高的隔离级别。

7.4.2 不可重复读(Non-Repeatable Read)

定义:事务中两次读取同一数据,结果不同。

示例

事务A: BEGIN;
事务A: SELECT balance FROM accounts WHERE id = 1;  -- 读取到100
事务B: UPDATE accounts SET balance = 200 WHERE id = 1;
事务B: COMMIT;
事务A: SELECT balance FROM accounts WHERE id = 1;  -- 读取到200

解决方法:使用可重复读或更高的隔离级别。

7.4.3 幻读(Phantom Read)

定义:事务中两次执行相同查询,第二次查询返回了第一次查询没有的行。

示例

事务A: BEGIN;
事务A: SELECT * FROM accounts WHERE balance > 100;  -- 返回3行
事务B: INSERT INTO accounts (id, balance) VALUES (4, 150);
事务B: COMMIT;
事务A: SELECT * FROM accounts WHERE balance > 100;  -- 返回4行

解决方法:使用可串行化隔离级别,或使用范围锁。

7.4.4 写 skew(Write Skew)

定义:两个事务同时读取同一数据集,各自修改不同的部分,导致数据不一致。

示例

事务A: BEGIN;
事务B: BEGIN;
事务A: SELECT * FROM doctors WHERE on_call = true;  -- 返回医生1和医生2
事务B: SELECT * FROM doctors WHERE on_call = true;  -- 返回医生1和医生2
事务A: UPDATE doctors SET on_call = false WHERE id = 1;  -- 医生1下班
事务B: UPDATE doctors SET on_call = false WHERE id = 2;  -- 医生2下班
事务A: COMMIT;
事务B: COMMIT;
-- 结果:没有医生值班

解决方法:使用可串行化隔离级别,或使用显式锁。

7.4.5 丢失更新(Lost Update)

定义:两个事务同时读取同一数据,各自修改后写回,导致其中一个修改被覆盖。

示例

事务A: BEGIN;
事务B: BEGIN;
事务A: SELECT balance FROM accounts WHERE id = 1;  -- 读取到100
事务B: SELECT balance FROM accounts WHERE id = 1;  -- 读取到100
事务A: UPDATE accounts SET balance = 100 + 50 WHERE id = 1;  -- 写入150
事务B: UPDATE accounts SET balance = 100 + 30 WHERE id = 1;  -- 写入130,覆盖了事务A的修改
事务A: COMMIT;
事务B: COMMIT;
-- 结果:余额为130,而不是180

解决方法

  • 使用可串行化隔离级别
  • 使用显式锁(SELECT ... FOR UPDATE)
  • 使用乐观锁(版本号或时间戳)

7.5 分布式事务

分布式事务涉及多个节点或数据库,保证跨节点的数据一致性。

7.5.1 两阶段提交(2PC)

工作原理

  • 准备阶段:协调者向所有参与者发送准备请求,参与者执行事务但不提交,返回准备结果
  • 提交阶段:如果所有参与者都准备成功,协调者发送提交请求;否则发送回滚请求

优势

  • 保证跨节点的一致性
  • 实现相对简单

局限

  • 阻塞:准备阶段完成后,参与者需要等待协调者的指令
  • 单点故障:协调者是单点故障
  • 性能开销大:需要多次网络通信

7.5.2 三阶段提交(3PC)

工作原理

  • 在2PC的基础上增加一个预提交阶段
  • 减少阻塞时间

局限

  • 仍然存在单点故障
  • 实现更复杂
  • 实际应用中很少使用

7.5.3 Saga模式

工作原理

  • 将分布式事务拆分为多个本地事务
  • 每个本地事务有对应的补偿事务
  • 如果某个本地事务失败,执行之前事务的补偿事务

示例

T1: 创建订单
T2: 扣减库存
T3: 扣款

如果T3失败:
C2: 恢复库存
C1: 取消订单

优势

  • 非阻塞,性能好
  • 没有单点故障

局限

  • 实现复杂,需要定义补偿事务
  • 不保证隔离性

7.5.4 最终一致性

工作原理

  • 不保证强一致性
  • 保证最终一致性
  • 通过异步消息、事件驱动等方式实现

优势

  • 性能好
  • 可用性高

局限

  • 不保证强一致性
  • 需要处理数据不一致的情况

重要知识点

知识点1:ACID特性是事务的基石

原子性保证操作的原子性,一致性保证数据的正确性,隔离性保证并发安全,持久性保证数据的持久性。四个特性缺一不可。

知识点2:隔离级别是性能和一致性的权衡

隔离级别越高,一致性越好,但性能越低。应该根据业务需求,选择合适的隔离级别。

知识点3:并发控制机制各有优劣

锁实现简单但性能低,MVCC性能好但存储开销大,SSI保证可串行化但可能重试。应该根据场景选择合适的机制。

知识点4:分布式事务是挑战

分布式事务涉及多个节点,保证一致性更加困难。2PC保证强一致性但性能低,Saga性能好但实现复杂。应该根据业务需求选择合适的方案。

知识点5:理解并发异常很重要

脏读、不可重复读、幻读、写 skew、丢失更新等并发异常会影响数据一致性。理解这些异常的产生原因和解决方法,对于设计正确的并发控制至关重要。

常见误区

误区1:认为可串行化隔离级别总是最好的

可串行化隔离级别虽然保证最强的一致性,但性能最低。对于很多应用,读已提交或可重复读已经足够,不需要使用可串行化。

误区2:认为MVCC可以解决所有并发问题

MVCC可以避免读写冲突,但无法解决写 skew 和丢失更新等问题。对于这些场景,需要使用显式锁或可串行化隔离级别。

误区3:认为分布式事务总是使用2PC

2PC虽然保证强一致性,但性能低、阻塞、单点故障。对于很多场景,Saga或最终一致性是更好的选择。

误区4:忽视事务的性能影响

事务的隔离级别、锁机制、日志记录等都会影响性能。应该根据业务需求,选择合适的配置,避免过度设计。

误区5:认为事务可以解决所有数据一致性问题

事务只能保证单个数据库内的一致性。对于跨数据库、跨服务的一致性问题,需要使用分布式事务或其他方案。

实践应用

案例1:电商系统的事务设计

一个电商系统需要处理订单、库存、支付:

订单创建:使用本地事务,保证订单和订单详情的一致性。

库存扣减:使用乐观锁或显式锁,防止超卖。

支付处理:使用Saga模式,协调订单、库存、支付等多个服务。

关键经验

  • 根据业务需求选择合适的隔离级别
  • 使用乐观锁防止丢失更新
  • 分布式事务使用Saga模式

案例2:银行系统的事务设计

一个银行系统需要处理转账、账户查询:

转账:使用可串行化隔离级别,保证强一致性。

账户查询:使用读已提交隔离级别,保证读取到最新数据。

跨行转账:使用2PC或最终一致性,保证跨行数据一致。

关键经验

  • 金融系统对一致性要求高
  • 关键操作使用可串行化
  • 跨行转账需要分布式事务

案例3:社交网络的事务设计

一个社交网络需要处理动态、点赞、关注:

动态发布:使用读已提交隔离级别,保证动态内容一致。

点赞:使用乐观锁,防止丢失更新。

关注:使用最终一致性,异步更新粉丝数和关注数。

关键经验

  • 社交网络对一致性要求中等
  • 使用乐观锁防止并发问题
  • 非关键操作使用最终一致性

本章小结

本章深入探讨了事务的核心概念。

ACID特性:原子性保证操作的原子性,一致性保证数据的正确性,隔离性保证并发安全,持久性保证数据的持久性。

隔离级别:读未提交、读已提交、可重复读、可串行化,隔离性依次增强,性能依次降低。

并发控制机制:锁实现简单但性能低,MVCC性能好但存储开销大,SSI保证可串行化但可能重试。

并发异常:脏读、不可重复读、幻读、写 skew、丢失更新,不同隔离级别可以解决不同的异常。

分布式事务:2PC保证强一致性但性能低,Saga性能好但实现复杂,最终一致性性能好但不保证强一致性。

选择事务策略时,应该根据业务需求、性能要求、一致性要求等因素综合考虑。没有完美的方案,只有适合业务需求的方案。

下一章,我们将探讨分布式系统的麻烦,这是分布式系统设计中必须面对的挑战。网络延迟、分区故障、时钟不同步等问题,都会影响分布式系统的正确性和性能。