第七章:事务
导读
事务是数据库系统中保证数据一致性的关键机制。它将一组操作打包成一个原子单元,要么全部成功,要么全部失败。事务的存在,使得开发者可以专注于业务逻辑,而不必担心并发访问和系统故障带来的数据不一致问题。
本章将深入探讨事务的ACID特性,分析各种隔离级别和并发控制机制。我们将学习如何处理幻读、写 skew等并发问题,理解不同隔离级别的权衡。
通过本章的学习,你将理解:
- ACID特性的含义和实现
- 各种隔离级别的区别
- 并发控制机制的原理
- 分布式事务的挑战
- 事务的性能权衡
核心概念详解
7.1 ACID特性
ACID是事务的四个核心特性的缩写:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。
7.1.1 原子性(Atomicity)
原子性保证事务中的所有操作要么全部成功,要么全部失败。如果事务中的某个操作失败,整个事务会被回滚,数据库状态恢复到事务开始之前。
示例:
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性能好但实现复杂,最终一致性性能好但不保证强一致性。
选择事务策略时,应该根据业务需求、性能要求、一致性要求等因素综合考虑。没有完美的方案,只有适合业务需求的方案。
下一章,我们将探讨分布式系统的麻烦,这是分布式系统设计中必须面对的挑战。网络延迟、分区故障、时钟不同步等问题,都会影响分布式系统的正确性和性能。