【MySQL】事务与锁
一、事务:什么是事务及其核心特性
1.1 事务的定义
事务是一组SQL语句的集合,要么全部执行成功(提交),要么全部执行失败(回滚),是数据库并发控制的基本单位。最经典的场景就是转账:
-- 转账示例:张三给李四转100元
START TRANSACTION;
-- 张三余额减少100
UPDATE account SET balance = balance - 100 WHERE name = '张三';
-- 李四余额增加100
UPDATE account SET balance = balance + 100 WHERE name = '李四';
-- 提交事务(或ROLLBACK回滚)
COMMIT;
1.2 核心特性:ACID模型
事务的可靠性由ACID四大特性保障,这也是面试高频考点:
- 原子性(Atomicity):事务是不可分割的最小单位,要么全成,要么全败;
- 一致性(Consistency):事务执行前后,数据库完整性约束不被破坏(如转账总额不变);
- 隔离性(Isolation):多个并发事务互不干扰,避免脏读、不可重复读、幻读;
- 持久性(Durability):事务提交后,数据修改永久保存,即使系统崩溃也不丢失。
初学者疑问:ACID中哪个是最终目标?
一致性是最终目标,原子性、隔离性、持久性是实现一致性的三大基石。只要保证这三点,数据自然会满足一致性。
1.3 事务的基本使用
InnoDB是MySQL唯一支持事务的存储引擎,核心操作如下:
-- 1. 开启事务(二选一)
START TRANSACTION;
BEGIN;
-- 2. 执行SQL操作(INSERT/UPDATE/DELETE)
UPDATE account SET balance = balance - 100 WHERE name = '张三';
UPDATE account SET balance = balance + 100 WHERE name = '李四';
-- 3. 提交事务(持久化修改)
COMMIT;
-- 4. 回滚事务(撤销修改)
ROLLBACK;
-- 5. 设置自动提交模式(默认ON)
SET AUTOCOMMIT = OFF; -- 关闭自动提交,需手动COMMIT/ROLLBACK
SET AUTOCOMMIT = ON; -- 开启自动提交,每条SQL单独构成事务
实战验证:自动提交与手动提交的区别
-- 查看当前自动提交模式
SHOW VARIABLES LIKE 'autocommit'; -- 默认ON
-- 关闭自动提交
SET AUTOCOMMIT = OFF;
-- 执行修改
UPDATE account SET balance = balance - 100 WHERE name = '张三';
-- 未提交前,其他会话看不到修改
-- 回滚后修改失效
ROLLBACK;
-- 重新执行修改并提交
UPDATE account SET balance = balance - 100 WHERE name = '张三';
COMMIT; -- 提交后修改持久化,其他会话可见
二、ACID实现原理:UndoLog、RedoLog与锁的协同
2.1 原子性(Atomicity):基于UndoLog的回滚机制
原子性的核心是“失败回滚”,而回滚的依据是UndoLog(撤销日志)。
实现逻辑
- 事务执行每个DML操作前,InnoDB会记录数据的原始版本到UndoLog;
- 若事务执行失败(如报错、ROLLBACK),InnoDB通过UndoLog反向执行操作,恢复数据到事务开始前的状态;
- UndoLog按操作类型分为
Insert Undo(记录INSERT操作,提交后可直接删除)和Update Undo(记录UPDATE/DELETE操作,需支持MVCC,提交后加入历史链表)。
初学者疑问:UndoLog是如何关联数据版本的?
每个数据行包含隐藏字段DB_ROLL_PTR(回滚指针),指向对应的UndoLog记录,多条UndoLog通过roll_pointer串联成版本链,回滚时按版本链逆向恢复。
2.2 持久性(Durability):基于RedoLog的崩溃恢复
持久性的核心是“提交后数据不丢失”,依赖RedoLog(重做日志) 和双写缓冲区。
实现逻辑
- 事务执行DML操作时,InnoDB先修改内存中的数据页(缓冲池),同时记录操作到RedoLog;
- 事务提交时,RedoLog先写入磁盘(WAL原则:Write-Ahead Logging),再异步刷写数据页到磁盘;
- 若系统崩溃,重启后InnoDB通过RedoLog恢复未刷盘的已提交事务,保证数据不丢失。
初学者疑问:RedoLog和UndoLog的区别是什么?
- RedoLog记录“数据修改后的状态”,用于崩溃恢复已提交事务;
- UndoLog记录“数据修改前的状态”,用于事务回滚;
- 两者协同:RedoLog保障提交后不丢失,UndoLog保障失败后可回滚。
2.3 一致性(Consistency):原子性+持久性+隔离性的自然结果
一致性是ACID的最终目标,无需单独实现:
- 原子性保证事务要么全成要么全败,避免数据部分修改;
- 持久性保证提交后数据不丢失;
- 隔离性保证并发事务互不干扰;
- 三者结合,自然满足数据的完整性约束(如转账总额不变、外键约束等)。
2.4 隔离性(Isolation):基于锁与MVCC的并发控制
隔离性是最复杂的特性,核心是解决并发事务的相互干扰。InnoDB通过“锁机制”和“MVCC”协同实现,既保证数据安全,又提升并发性能。
三、锁机制:InnoDB的并发控制核心
锁是实现隔离性的基础,InnoDB支持多种锁类型,按粒度可分为表级锁和行级锁,按模式可分为共享锁、排他锁等,不同锁的协同实现了不同的隔离级别。
3.1 锁的核心分类
3.1.1 按粒度划分:表级锁 vs 行级锁
| 锁类型 | 锁定范围 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 表级锁 | 整个表 | 开销小、加锁快 | 并发度低,易阻塞 | 全表扫描、批量更新 |
| 行级锁 | 单个数据行 | 并发度高,冲突少 | 开销大、加锁慢 | 单行/少量行修改、高并发 |
初学者疑问:InnoDB默认是行级锁,为什么有时会升级为表级锁?
当SQL未命中索引(如全表扫描),InnoDB无法精准锁定行,会自动升级为表级锁,导致并发性能下降。因此,索引是行级锁的前提。
3.1.2 按模式划分:共享锁与排他锁(核心锁)
InnoDB实现了标准的行级锁:
- 共享锁(S锁/读锁):允许事务读取数据,多个事务可同时持有S锁,但禁止写操作;
- 排他锁(X锁/写锁):允许事务修改/删除数据,持有X锁时,其他事务无法持有任何锁。
锁兼容性矩阵:
| 共享锁(S) | 排他锁(X) | |
|---|---|---|
| 共享锁(S) | 兼容 | 冲突 |
| 排他锁(X) | 冲突 | 冲突 |
实战示例:手动加锁
-- 加共享锁(读锁),其他事务可读不可写
SELECT * FROM account WHERE id = 1 FOR SHARE; -- MySQL 8.0+
SELECT * FROM account WHERE id = 1 LOCK IN SHARE MODE; -- 兼容旧版本
-- 加排他锁(写锁),其他事务不可读不可写
SELECT * FROM account WHERE id = 1 FOR UPDATE;
3.1.3 意向锁:表级锁与行级锁的桥梁
意向锁是表级锁,用于标识事务“打算”对表中的行加锁,分为:
- 意向共享锁(IS锁):事务打算对行加S锁,加S锁前先加IS锁;
- 意向排他锁(IX锁):事务打算对行加X锁,加X锁前先加IX锁。
初学者疑问:意向锁为什么不需要阻止行锁请求?
意向锁的核心作用是“快速判断表中是否有行锁”,避免加表锁时遍历所有行。例如,当事务要加表级X锁时,只需检查是否有IS/IX锁,无需逐行检查行锁,提升加锁效率。
意向锁兼容性矩阵:
| X锁 | IX锁 | S锁 | IS锁 | |
|---|---|---|---|---|
| X锁 | 冲突 | 冲突 | 冲突 | 冲突 |
| IX锁 | 冲突 | 兼容 | 冲突 | 兼容 |
| S锁 | 冲突 | 冲突 | 兼容 | 兼容 |
| IS锁 | 冲突 | 兼容 | 兼容 | 兼容 |
3.1.4 行级锁的细分:记录锁、间隙锁、临键锁
InnoDB的行级锁基于索引实现,细分三种锁以解决幻读问题:
- 记录锁(Record Lock):锁定具体的索引记录(如
id=10的行),仅锁住行本身; - 间隙锁(Gap Lock):锁定索引记录之间的间隙(如
id=10和id=20之间),防止插入新数据; - 临键锁(Next-Key Lock):记录锁+间隙锁的组合(如
(10,20]),默认隔离级别(REPEATABLE READ)下自动启用,解决幻读。
实战示例:间隙锁与临键锁
-- 表结构(id为主键)
CREATE TABLE account (id INT PRIMARY KEY, name VARCHAR(20), balance DECIMAL(10,2));
INSERT INTO account VALUES (10, '张三', 1000), (20, '李四', 1000);
-- 事务A执行:锁定id在10-20之间的行,触发临键锁 (10,20]
START TRANSACTION;
SELECT * FROM account WHERE id BETWEEN 10 AND 20 FOR UPDATE;
-- 事务B执行:插入id=15的行,会被间隙锁阻塞
INSERT INTO account VALUES (15, '王五', 1000); -- 阻塞,直到事务A提交/回滚
初学者疑问:什么时候间隙锁会失效?
当隔离级别设置为READ COMMITTED时,间隙锁会被禁用;或使用唯一索引查询唯一行时(如id=10),仅触发记录锁,不触发间隙锁。
3.1.5 其他特殊锁
- 插入意向锁(Insert Intention Lock):插入数据时的间隙锁,多个事务向同一间隙插入不同数据时不会阻塞;
- AUTO-INC锁:表级锁,用于自增列,保证自增值唯一,事务回滚后自增值不回退(可能出现不连续);
- 元数据锁(MDL锁):表级锁,防止DDL与DML并发冲突(如修改表结构时禁止插入数据)。
3.2 死锁:并发事务的“陷阱”
3.2.1 死锁的定义与产生条件
死锁是指两个或多个事务互相持有对方需要的锁,导致事务无法继续执行的状态。例如:
- 事务A持有记录1的S锁,等待记录2的X锁;
- 事务B持有记录2的S锁,等待记录1的X锁;
- 两者互相等待,形成死锁。
死锁产生的四大必要条件(缺一不可):
- 互斥访问:锁只能被一个事务持有;
- 不可抢占:锁只能由持有事务主动释放;
- 保持与请求:事务持有一个锁后,再请求其他锁;
- 循环等待:事务之间形成锁的循环依赖。
3.2.2 死锁模拟与排查
步骤1:模拟死锁
-- 会话A(事务A)
SET GLOBAL innodb_print_all_deadlocks = ON; -- 开启死锁日志
START TRANSACTION;
SELECT * FROM account WHERE name = '张三' FOR SHARE; -- 持有张三的S锁
-- 会话B(事务B)
START TRANSACTION;
SELECT * FROM account WHERE name = '李四' FOR SHARE; -- 持有李四的S锁
-- 会话B更新张三的记录(请求X锁,阻塞)
UPDATE account SET balance = 1100 WHERE name = '张三';
-- 会话A更新李四的记录(请求X锁,触发死锁)
UPDATE account SET balance = 900 WHERE name = '李四';
步骤2:排查死锁
-- 查看死锁次数
SELECT `count` FROM INFORMATION_SCHEMA.INNODB_METRICS WHERE NAME = "lock_deadlocks";
-- 查看最近一次死锁详情
SHOW ENGINE INNODB STATUS\G -- 查看LATEST DETECTED DEADLOCK部分
-- 查看锁等待信息
SELECT * FROM performance_schema.data_lock_waits;
3.2.3 死锁的避免策略
打破死锁的核心是破坏“循环等待”条件,常用方法:
- 保持事务短小:减少事务持有锁的时间和范围;
- 统一锁申请顺序:事务修改多个表/行时,按固定顺序操作(如先修改张三再修改李四);
- 避免长时间未提交的事务:及时COMMIT/ROLLBACK;
- 合理使用索引:避免表锁升级,减少锁冲突;
- 禁用死锁检测(高并发场景):设置
innodb_deadlock_detect = OFF,依赖innodb_lock_wait_timeout(默认50秒)超时释放; - 降低隔离级别:如使用
READ COMMITTED,减少间隙锁的使用。
四、事务隔离级别:并发性能与数据安全的权衡
隔离级别定义了并发事务之间的隔离程度,InnoDB支持四种隔离级别,从低到高依次为:
4.1 四种隔离级别详解
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 并发性能 | 实现方式 |
|---|---|---|---|---|---|
| READ UNCOMMITTED | 允许 | 允许 | 允许 | 最高 | 读不加锁,写加S锁 |
| READ COMMITTED | 禁止 | 允许 | 允许 | 较高 | 快照读(MVCC),写加X锁 |
| REPEATABLE READ | 禁止 | 禁止 | 部分 | 中等 | 快照读(MVCC)+ Next-Key锁 |
| SERIALIZABLE | 禁止 | 禁止 | 禁止 | 最低 | 读加S锁,写加X锁,完全串行执行 |
注:InnoDB默认隔离级别为REPEATABLE READ,通过Next-Key锁解决了大部分幻读场景。
4.1.1 关键概念解释
- 脏读:读取到未提交事务的修改(如事务A修改数据未提交,事务B读取到该修改);
- 不可重复读:同一事务内,多次查询同一数据得到不同结果(如事务A查询后,事务B修改并提交,事务A再次查询得到新值);
- 幻读:同一事务内,多次查询同一范围得到不同数量的结果(如事务A查询
id<10的行,事务B插入id=5的行并提交,事务A再次查询多了一行)。
4.2 隔离级别的设置与验证
4.2.1 查看与设置隔离级别
-- 查看隔离级别(全局/会话)
SELECT @@GLOBAL.transaction_isolation; -- 全局
SELECT @@SESSION.transaction_isolation; -- 会话(默认REPEATABLE READ)
-- 设置隔离级别(全局/会话)
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 选项文件配置(永久生效)
[mysqld]
transaction-isolation = REPEATABLE-READ
4.2.2 不同隔离级别的问题验证
示例1:READ UNCOMMITTED的脏读问题
-- 会话A设置隔离级别为READ UNCOMMITTED
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE name = '张三'; -- 未提交
-- 会话B查询,读取到未提交的修改(脏读)
SELECT balance FROM account WHERE name = '张三'; -- 结果为900(原1000)
-- 会话A回滚,会话B再次查询,结果恢复为1000
ROLLBACK;
示例2:REPEATABLE READ的可重复读特性
-- 会话A设置隔离级别为REPEATABLE READ
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT balance FROM account WHERE name = '张三'; -- 结果1000
-- 会话B修改并提交
START TRANSACTION;
UPDATE account SET balance = 900 WHERE name = '张三';
COMMIT;
-- 会话A再次查询,结果仍为1000(可重复读)
SELECT balance FROM account WHERE name = '张三';
初学者疑问:REPEATABLE READ为什么能实现可重复读?
因为该隔离级别下,事务首次查询时生成ReadView(读视图),后续查询复用该ReadView,只能看到事务开始前已提交的数据版本,看不到后续提交的修改。
五、MVCC:多版本并发控制的底层逻辑
MVCC(Multi-Versioned Concurrency Control)是InnoDB实现高并发的核心,通过数据版本链和ReadView实现“读不加锁、写不加锁”,提升并发性能。
5.1 MVCC的核心组成
5.1.1 数据版本链
每个数据行包含三个隐藏字段:
DB_TRX_ID:修改该行的事务ID;DB_ROLL_PTR:回滚指针,指向该行的上一个版本(存储在UndoLog中);DB_ROW_ID:无主键/唯一键时自动生成的行ID。
事务修改数据时,会生成新的数据版本,通过DB_ROLL_PTR串联成版本链,例如:
当前版本(id=1,balance=900,trx_id=200,roll_ptr→版本1)
版本1(id=1,balance=1000,trx_id=100,roll_ptr→版本0)
版本0(id=1,balance=1000,trx_id=80,roll_ptr=NULL)
5.1.2 ReadView(读视图)
ReadView是事务查询时生成的内存结构,记录当前活跃事务的信息,用于判断数据版本的可见性,包含四个核心属性:
m_ids:当前活跃事务ID集合;m_low_limit_id:活跃事务中最小的ID;m_up_limit_id:下一个将要分配的事务ID;m_creator_trx_id:创建该ReadView的事务ID。
5.1.3 版本可见性判断规则
查询时,InnoDB遍历数据版本链,按以下规则判断版本是否可见:
- 若版本的
trx_id==m_creator_trx_id:可见(读取自己修改的数据); - 若版本的
trx_id<m_low_limit_id:可见(事务已提交); - 若版本的
trx_id>=m_up_limit_id:不可见(事务在ReadView生成后创建); - 若版本的
trx_id在m_low_limit_id和m_up_limit_id之间:- 若
trx_id不在m_ids中:可见(事务已提交); - 否则:不可见(事务仍活跃)。
- 若
5.2 MVCC与隔离级别的关联
MVCC的ReadView生成时机决定了隔离级别:
- READ COMMITTED:每次查询都生成新的
ReadView,因此能看到查询前已提交的修改(可能出现不可重复读); - REPEATABLE READ:事务首次查询生成
ReadView,后续查询复用,因此同一事务内查询结果一致(解决不可重复读)。
初学者疑问:MVCC为什么能提升并发性能?
因为MVCC实现了“快照读”(不加锁的读),读事务与写事务互不阻塞,写事务之间通过锁互斥,既保证了隔离性,又提升了并发度。
六、实战:事务与锁的最佳实践
6.1 核心优化建议
- 优先使用行级锁:通过索引精准锁定数据,避免表锁升级;
- 保持事务短小:减少锁持有时间,降低死锁风险;
- 合理设置隔离级别:
- 高并发读场景:使用
READ COMMITTED(减少锁冲突); - 数据一致性要求高:使用
REPEATABLE READ(默认); - 严格一致性要求:使用
SERIALIZABLE(慎用,并发低);
- 高并发读场景:使用
- 避免长事务:及时提交/回滚,防止锁占用过久;
- 监控锁状态:定期查看锁等待和死锁日志,优化SQL。
6.2 常用监控SQL
-- 查看当前锁信息
SELECT
ENGINE_TRANSACTION_ID AS trx_id,
OBJECT_NAME AS table_name,
LOCK_TYPE AS lock_type,
LOCK_MODE AS lock_mode,
LOCK_STATUS AS lock_status,
LOCK_DATA AS lock_data
FROM performance_schema.data_locks;
-- 查看锁等待信息
SELECT
REQUESTING_ENGINE_TRANSACTION_ID AS req_trx_id,
BLOCKING_ENGINE_TRANSACTION_ID AS blk_trx_id,
OBJECT_NAME AS table_name
FROM performance_schema.data_lock_waits;
-- 查看事务状态
SHOW ENGINE INNODB STATUS\G
七、总结与进阶方向
核心知识点梳理
- 原子性依赖UndoLog,持久性依赖RedoLog,隔离性依赖锁+MVCC;
- 锁按粒度分为表级锁/行级锁,按模式分为S/X锁、意向锁、间隙锁等;
- 四种隔离级别权衡并发性能与数据安全,InnoDB默认REPEATABLE READ;
- MVCC通过版本链和ReadView实现无锁读,是高并发的关键;
- 死锁的核心是循环等待,通过统一锁顺序、缩短事务等方式避免。
进阶学习方向
- RedoLog与UndoLog的刷盘机制;
- 双写缓冲区与崩溃恢复的详细流程;
- 索引与锁的关系(如非主键索引的锁机制);
- 分布式事务(XA事务、2PC/3PC);
- 锁优化工具(如pt-table-checksum、pt-deadlock-logger)。
1万+

被折叠的 条评论
为什么被折叠?



