在实际开发中,数据库并发控制问题往往是性能瓶颈与数据一致性问题的根源。MySQL 通过其默认的存储引擎 InnoDB,通过事务机制、MVCC、多级锁和日志系统,为开发者提供了强大的并发控制能力。但这些机制的底层实现原理往往被忽略,导致在出现脏读、死锁或性能下降等问题时难以定位。本篇文章将从事务的 ACID 特性出发,系统梳理 InnoDB 如何实现隔离性与持久性,深入解析 MVCC、undo/redo log、锁机制与隔离级别的协作原理。
事务的四大特性(ACID)
原子性(Atomicity):指事务中的所有操作要么全部成功,要么全部失败。
- InnoDB 引擎通过 undo log (回滚日志)保证原子性:事务启动后,对数据的任何修改都先记录原始版本,若事务回滚,可根据 undo log 恢复到修改前状态。
一致性(Consistency):指事务执行前后,数据库都保持一致的状态(满足约束条件、业务逻辑不变)。
例如,在转账场景中,不论如何转账,只要事务正确提交,总资产不变即可。
隔离性(Isolation):并发事务之间互不干扰,各并发事务之间是独立的。
- InnoDB 采用锁机制和多版本并发控制(MVCC)来实现隔离性。InnoDB 会为读写操作加行锁或使用MVCC,使不同事务读取到的数据互不冲突,从而避免未提交的修改互相影响。
持久性(Durability):指事务提交后,对数据的修改是永久性的,即使发生系统崩溃也不会丢失。
InnoDB 通过 redo log(重做日志)保证持久性:事务提交时会将修改记录追加到 redo log 并刷盘,以备故障恢复使用。利用写前日志(WAL)技术,即便数据页未及时写入磁盘,重启后也可通过 redo log 恢复已提交事务的数据,确保改动持久不丢失。
常见并发问题:脏读、不可重复读、幻读
- 脏读:事务A读取事务B未提交的数据 -> 事务B回滚 -> 事务A读到了无效的数据。
- 不可重复读:事务A第一次读取数据 -> 事务B对数据进行修改 -> 事务A第二次读取同一数据(发现数据被修改)。
- 幻读:事务A第一次读取数据 -> 事务B插入或者删除满足条件的数据 -> 事务A第二次读取数据(数据行数发生变化)。
这些问题都是并发事务下读到其他事务提交后变化的数据造成的。脏读可看作读到了不一定最终存在的数据;不可重复读是针对已存在数据的更新;幻读则针对新增或删除整批记录时出现的不一致。数据库通过不同隔离级别来解决这些问题。
事务隔离级别与并发问题
InnoDB 支持四个事务哥隔离级别:读未提交(READ UNCOMMITTED)、读已提交(READ COMMITTED)、可重复读(REPEATABLE READ)和可串行化(SERIALIZABLE),从低到高逐级加强隔离。各级别能够避免的问题如下:
读取未提交:最低的隔离级别,允许读取未提交的事务。会导致脏读、幻读、不可重复读。
读取已提交:允许并发读取已提交的事务。可阻止脏读,但同一事务的两次读如果中间有其他事务提交修改,仍会出现不可重复读(因为第一次读和第二次读看到的数据不一致);幻读依然可能发生。
在读取已提交隔离级别下,下面场景中不会出现脏读,但可能出现不可重复读:
1
2
3
4
5
6
7
8
9
10
11
12-- 事务 T1
BEGIN;
SELECT balance FROM accounts WHERE id=1; -- 假设读到balance=100
-- 事务 T2 并发执行
BEGIN;
UPDATE accounts SET balance=200 WHERE id=1;
COMMIT;
-- 事务T1(继续)
SELECT balance FROM accounts WHERE id=1; -- 此时读到balance=200,与第一次结果不同(不可重复读)
COMMIT;
可重复读:InnoDB的默认隔离级别。同一事务对同一字段的多次读取结果是一致的。
事务开始后建立一个快照(Read View),事务内所有读操作都在该快照上进行,确保多次读同一行得到相同结果,因此可避免脏读和不可重复读。需要注意的是,标准可重复读并不能防止幻读,但 InnoDB 还会对范围查询使用间隙锁(Next-Key Lock)来避免新记录插入,从而实际避免了幻读情况。
可以借助MVCC来解决可重复读的问题,通过维护一个字段作为version,这样可以控制到每次只能更新一个版本。
1
2
3select id from table_xx where id = ? and version = V
update id from table_xx where id = ? and version = V+1
可串行化:最高的事务隔离级别,事务顺序执行,任何并发问题都被消除。但性能代价最高,应用时要谨慎。
MVCC(多版本并发控制)
MVCC是数据库中用来处理并发事务的一种技术。通过维护数据的多个版本来处理数据的读写隔离。
实现原理
- 版本链:InnoDB中的聚簇索引会有两个隐藏列(trx_id 和 roll_pointer),用于实现MVCC
- trx_id:事务id,记录最后一次修改数据行的事务id。
- roll_pointer:回滚指针,指向数据行旧版本在 undo log 中的地址,每次修改都会产生一个新的版本并将指针链指向前一个版本。这些历史版本通过 roll_pointer 串成链表,构成版本链。
- 每个事务启动时会创建一个“读视图”(Read View),记录当前活跃事务的id范围等信息。事务读取数据时,会比较数据行的 trx_id 与其 Read View 中的事务id列表:如果该行是由未提交事务修改(trx_id 在活动id范围内),当前事务就会沿着 roll_pointer 指针去 undo log 找到合适版本。这样即使另一事务对行做了 UPDATE、DELETE,当前事务也可以读取该行修改前的快照版本,而无需等待锁释放。总之,MVCC 为每个事务提供了一个一致性视图,使其看到的数据如同在事务开始时的数据库状态,从而避免了读写冲突。
举例来说,事务T1启动后读取记录A的某个字段,事务T2随后修改并提交该字段。在 MVCC 下,T1 再次读取时不会看到 T2 的更改,而是读取到最初的数据快照版本(由 undo log 提供),实现可重复读而不加锁。
适用范围
- MVCC只适用于RC(读取已提交)和RR(可重复读)。
- 由于读取未提交存在脏读,未能读取到事务提交的行,所以不适用MVCC。(MVCC的版本号在事务提交后才产生)
- MySQL的MVCC从客观上看是乐观锁的一种实现方式,即每行都有版本号,根据版本号来判断是否成功。InnoDB的MVCC使用到的快照存储在undo log(回滚日志)中,通过回滚指针把一个数据行的所有快照连接起来。
Redo Log 与 Undo Log 的作用与区别
- Undo Log:回滚日志,记录事务修改前的数据状态(旧值),用于事务回滚和MVCC版本存储。用于保证事务的原子性。
- 是逻辑层面上的回滚,记录了每次对数据库增、删、改对应的撤销操作。
- 如果是INSERT,则至少会在Undo Log中写入新增记录的主键,以便于回滚时的撤销(删除);如果是DELETE和UPDATE,则至少会在Undo Log中写入删除/更新记录之前的全部数据。
- 可以给MVCC提供版本恢复的操作。
- Redo Log:重做日志,能够恢复事务提交修改的页操作,给MySQL提供了崩溃修复的能力。用于保证事务的持久性。
- 是MySQL存储引擎生成的日志,记录了物理级别上的操作(例如页号XXX、偏移量XXX等)。
- 可以通过参数innodb_flush_log_at_trx_commit设置Redo Log的刷盘策略。
- innodb_flush_log_at_trx_commit = 0:每秒把Redo Log写进磁盘。(宕机会有1秒的数据损失)
- innodb_flush_log_at_trx_commit = 1(默认):当事务提交的时候,会把Redo Log写进磁盘。(因为事务没有提交,所以不会有数据损失)
- innodb_flush_log_at_trx_commit = 2:当事务提交的时候,会把Redo Log写进操作系统的文件系统缓存中(Page Cache)。(MySQL宕机不会有数据损失,若机器宕机会有1秒的数据损失)
乐观锁的实现
乐观锁不是数据库固有的锁机制,而是由应用程序实现的并发控制策略,通常基于版本号或时间戳思想。常见做法是在表中增加一个 version 字段(或类似的版本号列),每次更新前先读取该字段值。更新时使用 SQL 原子操作同时检查并递增版本号:
1 | update id from table_xx where id = ? and version = V+1 |
锁机制:共享锁、排他锁、意向锁等
InnoDB 支持多种锁机制来协调并发访问:
- 共享锁(Shared Lock, S):又称读锁,允许多个事务并发读取同一行数据,但阻止任何事务对该行加排他锁(写锁)。多个事务可同时持有同一行的共享锁。
- 排他锁(Exclusive Lock, X):又称写锁,获得后允许事务修改数据,并阻止其他事务再获取该行的任何锁(既不能读也不能写)。InnoDB 默认在执行 UPDATE/DELETE/INSERT 时自动加行级排他锁,以保证写操作的互斥性。
- 意向锁(Intention Lock):是一种表级别的辅助锁,用于在行锁之上表明事务的意图,避免检查行锁时扫描整个表。包括两种类型:意向共享锁(IS)和意向排他锁(IX)。当事务要给某行加共享锁时,会先在表上加 IS 锁;要加排他锁时,会先加 IX 锁。意向锁之间互相兼容,且与行级锁的兼容性如下:
- IS 与 IS、IX 兼容;IX 与 IS、IX 兼容;
- IS 与共享锁(S)兼容,但与排他锁(X)冲突;
- IX 与共享锁冲突,与排他锁冲突。
- 通过意向锁,数据库管理系统能快速判断:例如,如果某表已有事务加了排他锁(X),任何新的行锁申请都会被拒绝,因其检测到表上存在与之冲突的意向锁;若没有排他锁,多个事务可以自由在不同记录上加锁。
综上所述,MySQL 的 InnoDB 引擎通过事务的 ACID 原则指导事务行为,通过锁和 MVCC 协同实现多事务的隔离和并发。不同隔离级别对并发问题有不同的保障,对应地使用 undo/redo 日志保证原子性和持久性,并可结合乐观锁等应用层方案,构建高效健壮的并发控制体系。