搞懂数据库事务性
数据库是现代应用的关键支柱,而“事务”更是数据库赖以保证一致性与正确性的核心机制。很多读者在使用MySQL、PostgreSQL等关系型数据库时,仅停留在“BEGIN、COMMIT、ROLLBACK”的表面操作,对事务背后的原理与实现细节了解不多。本文将从ACID特性、隔离级别、多版本并发控制(MVCC)到常见陷阱、优化策略等方面,深度剖析数据库的事务性,帮助大家在实际开发和运维中做到心中有数、下
数据库是现代应用的关键支柱,而“事务”更是数据库赖以保证一致性与正确性的核心机制。很多读者在使用MySQL、PostgreSQL等关系型数据库时,仅停留在“BEGIN、COMMIT、ROLLBACK”的表面操作,对事务背后的原理与实现细节了解不多。本文将从ACID特性、隔离级别、多版本并发控制(MVCC)到常见陷阱、优化策略等方面,深度剖析数据库的事务性,帮助大家在实际开发和运维中做到心中有数、下手不慌。
目录
4.2.2 不可重复读(Non-Repeatable Read)
一、为什么要有事务?
在谈论数据库事务之前,先要思考:没有事务,会导致怎样的问题?在一个多用户并发访问、数据随时可能写入或更新的系统中,如果没有事务层的保护,常见问题包括:
-
部分更新导致数据不一致
假设一个业务流程分多步写库操作,如果执行了一半就失败了,已经写入的数据可能无法回滚,最终引发数据失真或脏数据。 -
并发冲突
多个用户同时修改同一行数据,若没有锁机制或并发控制,后写入的数据可能覆盖前写入的数据,导致应用无法追踪到正确状态。 -
数据读取不一致
在多线程或多进程并行读写的环境中,如果没有隔离手段,有些线程可能读到一半被更新、一半没被更新的“半成品数据”。
为了解决上述难题,事务应运而生。事务是一种逻辑单元,它将一系列数据库操作(如增删改)组合在一起,要么全部成功,要么全部失败,从而确保数据库的完整性与一致性。
二、ACID特性:事务的四大支柱
谈到数据库事务,离不开ACID四大特性,它们分别是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
-
原子性
原子性要求事务中的所有操作要么全部成功提交,要么全部取消回滚。“原子”就像物理概念中不可再分割的最小单元,数据库也通过日志系统(Undo/Redo)以及内部的回滚机制来保证这一点。只要事务出现任何异常,数据库会将其已执行的操作全部撤销,回到事务开始前的状态。 -
一致性
一致性强调数据库在事务开始前和结束后,数据都必须满足预先定义的约束或规则。比如在银行转账例子里,总金额应保持不变;如果发生了外键约束或检查约束,不管事务如何执行,最终都要保证这些约束不被破坏。 -
隔离性
多个事务并发执行时,彼此之间不要相互干扰,读者可根据应用需求选择不同的隔离级别(READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ、SERIALIZABLE)。隔离性在保证性能和并发度的同时,也决定了“脏读、不可重复读、幻读”等现象会否出现。 -
持久性
持久性意味着一旦事务成功提交,其所做的更改应永久存储在数据库中,即使系统崩溃或数据库重启,已提交的数据也要能被恢复。现代数据库通常用WAL(Write-Ahead Logging)、Redo Log等机制,先将操作记录写入日志,再将数据刷新到磁盘,保证事务提交后不丢失。
在这四大支柱的支撑下,事务成为了保障数据安全、一致和可依赖的核心手段。下面将更深入剖析数据库内部是如何具体实现这些特性的。
三、事务的内部实现:日志与锁
3.1 日志系统——Undo与Redo
-
Redo Log(重做日志)
数据库执行任何修改操作时,会在更新真正数据页之前,先把“修改的意图”记录到重做日志。这确保了在系统崩溃或异常后,数据库可以通过重做日志来重放事务,恢复到最新的提交状态。 -
Undo Log(回滚日志)
与Redo Log相对,Undo Log在事务开始或写操作前,会记录一份原数据的“快照”。若事务后续需要回滚或者进行版本读取(MVCC场景),则可通过Undo Log恢复行数据到旧版本。
对许多数据库而言,Redo/Undo日志系统是实现原子性、持久性、多版本并发控制的根基。其中,MySQL InnoDB称它们为“Redo Log、Undo Log”,PostgreSQL对应“WAL日志、行版本”,但本质思想相近。
3.2 锁机制与并发
3.2.1 两类主要锁:行锁与表锁
- 行级锁(Row Lock):对特定行记录上锁,可以提升并发性能,但管理成本更高。InnoDB存储引擎就支持行级锁。
- 表级锁(Table Lock):一次性锁住整张表,适用于读多写少或对并发要求不高的场景,MyISAM引擎就是典型表锁模型。
3.2.2 乐观锁与悲观锁
- 悲观锁:假设竞争一定存在,在数据被修改前就先锁住,其他事务必须等待。
- 乐观锁:假设竞争不常发生,通过版本号或CAS(Compare And Swap)机制在提交前校验是否有冲突,一旦冲突则回滚重试。分布式场景中常用乐观锁思路减少锁等待,但程序员需要处理好重试。
3.2.3 两段锁协议
某些数据库(如SQL Server、Oracle)会在事务中采用“两段锁协议”:
- 第一阶段:事务在读取或写入数据时不断申请锁;
- 第二阶段:在第一次释放锁后,不再申请任何新锁。
只有保证严格的两段锁协议,才能避免诸如脏读、不一致更新等问题。
锁机制在事务隔离性上扮演不可或缺的角色,但过多或过重的锁也会降低并发度,因此在实际生产中要结合隔离级别、表结构、访问模式合理设计。
四、事务隔离级别与并发现象
4.1 ANSI SQL定义的四种隔离级别
- READ UNCOMMITTED(读取未提交数据)
允许读取其他事务尚未提交的数据,即脏读。一般很少使用,安全性差。 - READ COMMITTED(读取已提交数据)
只能读取已提交的数据,避免了脏读,但无法避免不可重复读。 - REPEATABLE READ(可重复读)
能避免脏读和不可重复读,在MySQL InnoDB下也可以避免幻读(因为它做了特殊处理),是常用的默认级别。 - SERIALIZABLE(可串行化)
最严格的隔离级别,完全避免脏读、不可重复读、幻读,但往往牺牲了性能,会导致更多锁冲突。
4.2 并发读写常见问题
4.2.1 脏读(Dirty Read)
如果A事务读取到B事务尚未提交的数据,一旦B事务回滚,A读到的数据就是“脏数据”,破坏一致性。在READ UNCOMMITTED级别可能出现。
4.2.2 不可重复读(Non-Repeatable Read)
A事务在两次读取同一行数据时,如果B事务在此间修改并提交了该行数据,A就会前后读取到不一致的值。READ COMMITTED级别会出现该问题。
4.2.3 幻读(Phantom Read)
A事务多次执行同一查询(通常带范围条件)时,如果B事务在此间插入或删除了满足条件的新数据,A就会在下次查询时看到一条“凭空出现/消失”的记录。SERIALIZABLE可避免此问题,MySQL的REPEATABLE READ也通过间隙锁(Gap Lock)或Next-Key Lock来规避幻读。
五、多版本并发控制(MVCC)
5.1 MVCC的概念
多版本并发控制(MVCC)旨在在高并发环境下提高读取性能、减少锁冲突。它通过在每条记录中维护多个版本,让读取操作无需阻塞写入操作,写入操作也无需阻塞读取操作。常见做法是为每行存储“创建版本号”和“过期版本号”,读时根据事务的快照版本进行筛选。
5.2 MVCC在InnoDB中的实现
- 隐藏列:InnoDB为每行记录存储隐藏列,如事务ID、回滚指针等,用于标识该行由哪个事务修改、何时失效等。
- Undo Log:一旦新版本被创建,旧版本会写入Undo Log,用于回滚或事务快照读取。
- 一致性读:当事务以快照读方式(普通SELECT)读取时,只会看到在该事务启动时已经提交的数据版本——这就是可重复读得以实现的基础。
5.3 优势与限制
- 优势:大部分读操作不加锁,减少了读写相互等待,提高了系统吞吐量。
- 限制:需要维护更多版本数据和Undo日志,意味着占用额外存储与内存,且在频繁更新场景下,过期版本的清理也需要耗费资源(如MySQL的purge线程或PostgreSQL的VACUUM)。
MVCC是实现高并发、高性能事务的关键技术之一,但开发者也需理解其原理与限制,做好表结构及索引设计,避免“旧版本堆积”导致性能劣化。
六、数据库中事务的典型流程
下面以MySQL InnoDB为例,梳理一个典型事务的内部执行流程,帮助读者从整体上把握:
-
事务开始(START TRANSACTION)
- InnoDB分配一个唯一的事务ID (Transaction ID)。
- 为可重复读(REPEATABLE READ)或更高隔离级别会建立一致性视图,记录此刻的快照。
-
执行DML操作(INSERT/UPDATE/DELETE)
- 数据库在缓冲池中修改对应数据页,同时记录到Undo Log和Redo Log。
- 行记录的隐藏列存储此事务ID,表示该行最新版本由该事务创建或修改。
-
执行查询(SELECT)
- 如果是普通SELECT,InnoDB按照快照规则只读取在事务开始前已提交的版本,以及在本事务中新建的行数据。
- 如果使用锁定读(SELECT ... FOR UPDATE),则会对涉及的记录加锁或间隙锁。
-
提交(COMMIT)或回滚(ROLLBACK)
- 提交时:InnoDB将Redo Log刷盘以保证持久性,Undo记录后续可异步清理。
- 回滚时:通过Undo Log恢复修改前的数据,释放已加的锁。
-
事务结束
- 数据库释放各种资源,包括行锁、表锁、临时空间。
- 提交的更新对其他新开启的事务可见。
从这个过程中可以看到,事务在整个生命周期中围绕着日志系统(Undo/Redo)与锁机制进行运作,既保证了数据的一致性,又尽量兼顾高并发下的性能需求。
七、常见陷阱与问题
7.1 大事务导致长时间锁定
如果一个事务执行时间过长,比如一个批量更新几百万行的数据,就会在很长时间里持有锁。这会阻塞其他写操作,甚至阻塞读操作(若锁等级较高),严重影响系统吞吐。
- 解决方案:将大事务拆分为多个小事务,避免一次占用过多锁。
7.2 隔离级别与业务逻辑的冲突
在READ COMMITTED下,有些业务要求再读取同一条数据时必须保证一致,这在并发环境下可能造成数据错误。不少用户以为默认MySQL就能避免不可重复读,其实只有REPEATABLE READ才能真正做到这点。因此,隔离级别的选择需要和业务需求匹配,否则就会产生意想不到的结果。
7.3 死锁(Deadlock)
多个事务彼此等待对方持有的锁资源,导致所有事务都无法继续;数据库最终会检测到死锁并中断其中一个事务。
- 应对策略:
- 保持一致的锁顺序,尽量在访问多张表或多行记录时按固定顺序请求锁。
- 拆分大事务,减少锁时间。
- 设置合理重试,在死锁发生后让应用自动重试被回滚的事务。
7.4 分布式事务难题
在微服务或分布式数据库场景下,事务的范围可能跨越多个节点或系统。此时要么使用分布式事务协议(如二阶段提交2PC、三阶段提交3PC)或Paxos/Raft一致性协议,要么干脆采用BASE思想、使用最终一致性方案(比如消息队列+回滚补偿)来避免高耦合。这又引入了更多复杂度和潜在性能瓶颈。

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐
所有评论(0)