事务与锁机制
ACID 特性、隔离级别、MVCC 与锁类型。
前置知识
建议先阅读以下内容再进入本文:
1. 事务特性 (ACID)
1.1 原子性 (Atomicity)
- 定义:事务是一个不可分割的工作单位,事务中的操作要么全部成功,要么全部失败
- 实现原理:通过 Undo Log 实现,当事务失败时,回滚到事务开始前的状态
- 示例:银行转账操作,扣款和存款要么同时成功,要么同时失败
1.2 一致性 (Consistency)
- 定义:事务执行前后,数据库从一个一致性状态转换到另一个一致性状态
- 实现原理:由应用程序和数据库共同保证,数据库确保数据的完整性约束
- 示例:转账前后,两个账户的总金额保持不变
1.3 隔离性 (Isolation)
- 定义:多个事务并发执行时,一个事务的执行不应影响其他事务的执行
- 实现原理:通过锁机制和 MVCC 实现
- 示例:两个事务同时操作同一数据时,不会互相干扰
1.4 持久性 (Durability)
- 定义:事务提交后,其结果永久保存到数据库,即使系统崩溃也不会丢失
- 实现原理:通过 Redo Log 实现,事务提交时将修改记录到 Redo Log
- 示例:事务提交后,即使数据库重启,修改仍然存在
2. 事务隔离级别 (Isolation Levels)
2.1 隔离级别的类型
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 并发性能 |
|---|---|---|---|---|
| 读未提交 (Read Uncommitted) | 可能 | 可能 | 可能 | 最高 |
| 读已提交 (Read Committed) | 不可能 | 可能 | 可能 | 高 |
| 可重复读 (Repeatable Read) | 不可能 | 不可能 | 不可能 (InnoDB) | 中 |
| 串行化 (Serializable) | 不可能 | 不可能 | 不可能 | 最低 |
2.2 各隔离级别的特点
2.2.1 读未提交 (Read Uncommitted)
- 特点:事务可以读取其他事务未提交的数据
- 问题:可能出现脏读
- 适用场景:对数据一致性要求不高的场景
2.2.2 读已提交 (Read Committed)
- 特点:事务只能读取其他事务已提交的数据
- 问题:可能出现不可重复读
- 适用场景:大多数应用场景
2.2.3 可重复读 (Repeatable Read)
- 特点:MySQL InnoDB 默认隔离级别,保证同一事务中多次读取同一数据的结果一致
- 问题:在其他数据库中可能出现幻读,但 InnoDB 通过 Next-Key Lock 解决了这个问题
- 适用场景:对数据一致性要求较高的场景
2.2.4 串行化 (Serializable)
- 特点:事务串行执行,完全隔离
- 问题:并发性能最差
- 适用场景:对数据一致性要求极高的场景
2.3 设置隔离级别
SELECT @@transaction_isolation;
SET GLOBAL transaction_isolation = 'READ-COMMITTED';
SET SESSION transaction_isolation = 'REPEATABLE-READ';
3. MVCC (多版本并发控制)
3.1 MVCC 概述
MVCC (Multi-Version Concurrency Control) 是 InnoDB 实现隔离级别的核心技术,它通过保存数据的多个版本,实现了读写并发,提高了数据库的并发性能。
3.2 MVCC 的工作原理
3.2.1 数据版本管理
- 行记录中的隐藏列:
DB_TRX_ID:事务 ID,记录最后修改该行的事务 IDDB_ROLL_PTR:回滚指针,指向 Undo Log 中的历史版本DB_ROW_ID:行 ID,自增,用于聚簇索引
3.2.2 Undo Log
- 作用:保存数据的历史版本,用于事务回滚和 MVCC
- 类型:
INSERT Undo Log:记录插入操作,事务提交后可删除UPDATE Undo Log:记录更新操作,事务提交后需要保留,用于 MVCCDELETE Undo Log:记录删除操作,事务提交后需要保留,用于 MVCC
3.2.3 ReadView
- 作用:判断数据版本的可见性
- 组成:
m_ids:当前活跃事务的 ID 集合min_trx_id:活跃事务中最小的 IDmax_trx_id:下一个将要分配的事务 IDcreator_trx_id:创建 ReadView 的事务 ID
3.2.4 可见性判断规则
- 如果数据的
DB_TRX_ID等于creator_trx_id,则可见 - 如果数据的
DB_TRX_ID小于min_trx_id,则可见 - 如果数据的
DB_TRX_ID大于等于max_trx_id,则不可见 - 如果数据的
DB_TRX_ID在m_ids中,则不可见;否则可见
3.3 MVCC 在不同隔离级别下的表现
- 读未提交:不使用 MVCC,直接读取最新数据
- 读已提交:每次查询都会创建新的 ReadView
- 可重复读:事务开始时创建 ReadView,之后不再创建
- 串行化:不使用 MVCC,使用锁机制
4. 锁机制 (Locks)
4.1 锁的分类
4.1.1 按粒度分类
- 行锁:锁定单行数据,粒度最小,并发性能最高
- 页锁:锁定数据页,粒度中等
- 表锁:锁定整个表,粒度最大,并发性能最低
4.1.2 按类型分类
- 共享锁 (S Lock):读锁,允许并发读,阻塞写
- 排他锁 (X Lock):写锁,阻塞读写
- 意向共享锁 (IS Lock):表级锁,表示事务准备对表中的某些行加共享锁
- 意向排他锁 (IX Lock):表级锁,表示事务准备对表中的某些行加排他锁
4.1.3 按算法分类
- 记录锁 (Record Lock):锁定单行记录
- 间隙锁 (Gap Lock):锁定索引间隙,防止插入
- 临键锁 (Next-Key Lock):记录锁 + 间隙锁,解决幻读
- 插入意向锁 (Insert Intention Lock):插入操作时的间隙锁
4.2 锁的使用场景
4.2.1 共享锁
-- 共享锁:8.0+ 推荐 FOR SHARE(旧写法 LOCK IN SHARE MODE 已废弃)
SELECT * FROM users WHERE id = 1 FOR SHARE;
4.2.2 排他锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;
INSERT INTO users (name) VALUES ('John');
UPDATE users SET name = 'John' WHERE id = 1;
delete FROM users WHERE id = 1;
4.2.3 间隙锁和临键锁
- 间隙锁:在可重复读隔离级别下,使用范围查询时会自动添加间隙锁
- 临键锁:InnoDB 默认使用的锁算法,解决幻读问题
4.3 锁的兼容性
| 共享锁 (S) | 排他锁 (X) | 意向共享锁 (IS) | 意向排他锁 (IX) | |
|---|---|---|---|---|
| 共享锁 (S) | 兼容 | 冲突 | 兼容 | 冲突 |
| 排他锁 (X) | 冲突 | 冲突 | 冲突 | 冲突 |
| 意向共享锁 (IS) | 兼容 | 冲突 | 兼容 | 兼容 |
| 意向排他锁 (IX) | 冲突 | 冲突 | 兼容 | 兼容 |
5. 死锁 (Deadlocks)
5.1 死锁的定义
死锁是指两个或多个事务相互等待对方释放锁的状态,导致所有事务都无法继续执行。
5.2 死锁的产生条件
- 互斥条件:资源不能被共享,一次只能被一个事务使用
- 请求与保持条件:事务已经保持了至少一个资源,又提出了新的资源请求
- 不剥夺条件:事务获得的资源在未使用完之前,不能被强行剥夺
- 循环等待条件:若干事务之间形成头尾相接的循环等待资源关系
5.3 死锁的检测与处理
5.3.1 死锁检测
SHOW ENGINE INNODB STATUS;
SET GLOBAL innodb_deadlock_detect = ON;
5.3.2 死锁处理
- 自动检测:InnoDB 会自动检测死锁,并回滚其中一个事务
- 手动处理:当死锁检测关闭时,需要手动处理
5.4 死锁的预防
- 按固定顺序访问表:避免循环等待
- 减小事务粒度:减少事务持有锁的时间
- 使用索引:避免全表扫描,减少锁的范围
- 避免长时间事务:尽快提交或回滚事务
- 使用较低的隔离级别:减少锁的竞争
- 设置合理的锁超时:
SET SESSION innodb_lock_wait_timeout = 30;
6. 事务的实现原理
6.1 日志系统
6.1.1 Redo Log
- 作用:保证事务的持久性
- 工作原理:事务提交时,将修改记录到 Redo Log,即使系统崩溃,重启后也可以通过 Redo Log 恢复数据
- 特点:顺序写入,性能高
6.1.2 Undo Log
- 作用:保证事务的原子性和 MVCC
- 工作原理:记录数据的历史版本,用于事务回滚和 MVCC 查询
- 特点:逆序写入,支持多版本
6.2 两阶段提交
- 准备阶段:事务将修改写入 Undo Log 和 Redo Log,但不提交
- 提交阶段:事务提交,释放锁
6.3 事务的状态
- 活跃 (Active):事务正在执行
- 部分提交 (Partially Committed):事务执行完成,但修改还未写入磁盘
- 提交 (Committed):事务已提交
- 失败 (Failed):事务执行失败
- 中止 (Aborted):事务已回滚
7. 事务的最佳实践
7.1 事务设计最佳实践
- 保持事务简短:减少事务持有锁的时间
- 避免在事务中进行网络操作:网络操作可能导致事务长时间持有锁
- 避免在事务中进行大量计算:计算操作可能导致事务长时间持有锁
- 合理设置隔离级别:根据业务需求选择合适的隔离级别
- 使用批量操作:减少事务数量
7.2 锁的使用最佳实践
- 使用索引:避免全表扫描,减少锁的范围
- 选择合适的锁粒度:根据业务需求选择合适的锁粒度
- 避免死锁:按固定顺序访问表,减小事务粒度
- 使用乐观锁:对于并发冲突较少的场景,使用乐观锁
- 监控锁等待:定期检查锁等待情况
7.3 性能优化
- 使用连接池:减少连接创建和销毁的开销
- 批量提交:减少事务提交的次数
- 合理使用索引:提高查询效率,减少锁的竞争
- 监控事务性能:定期分析慢事务
- 优化 SQL:减少事务中的复杂查询
8. 实际案例分析
8.1 案例 1:死锁排查
问题:应用程序出现死锁错误 分析:
- 查看死锁日志:
SHOW ENGINE INNODB STATUS; - 发现两个事务相互等待对方的锁
- 分析 SQL 语句,发现访问表的顺序不同 解决方案:
- 统一访问表的顺序
- 减小事务粒度
- 使用索引优化查询
8.2 案例 2:事务性能优化
问题:事务执行时间过长,导致并发性能下降 分析:
- 查看慢查询日志
- 发现事务中包含大量计算和网络操作
- 事务持有锁的时间过长 解决方案:
- 将计算和网络操作移到事务外
- 拆分大事务为小事务
- 优化 SQL 查询
8.3 案例 3:隔离级别选择
问题:应用程序出现幻读 分析:
- 检查隔离级别:
SELECT @@transaction_isolation; - 发现使用的是读已提交隔离级别
- 业务需求需要可重复读 解决方案:
- 将隔离级别设置为可重复读:
SET SESSION transaction_isolation = 'REPEATABLE-READ'; - 优化查询,使用索引
9. 常见问题与解决方案
9.1 事务超时
问题:事务执行时间过长,导致超时 解决方案:
- 减小事务粒度
- 优化 SQL 查询
- 增加超时时间:
SET SESSION innodb_lock_wait_timeout = 60;
9.2 死锁
问题:应用程序出现死锁错误 解决方案:
- 按固定顺序访问表
- 减小事务粒度
- 使用索引优化查询
- 监控死锁日志
9.3 并发性能低
问题:并发访问时性能下降 解决方案:
- 使用合理的隔离级别
- 优化锁的使用
- 提高索引效率
- 使用连接池
9.4 数据一致性问题
问题:事务执行后数据不一致 解决方案:
- 确保事务的 ACID 特性
- 使用合适的隔离级别
- 检查应用程序逻辑
- 定期备份数据
10. 总结
事务和锁机制是 MySQL 数据库并发控制的核心,通过理解 ACID 特性、隔离级别、MVCC 原理和锁机制,可以有效地设计和优化数据库应用,提高并发性能,保证数据一致性。
核心要点
- 事务特性:ACID(原子性、一致性、隔离性、持久性)
- 隔离级别:读未提交、读已提交、可重复读、串行化
- MVCC:多版本并发控制,通过 Undo Log 和 ReadView 实现
- 锁机制:行锁、表锁、共享锁、排他锁、间隙锁等
- 死锁:预防和处理死锁的方法
- 最佳实践:事务设计、锁的使用、性能优化
学习建议
- 实践:通过实际操作熟悉事务和锁的使用
- 分析:使用
SHOW ENGINE INNODB STATUS;分析死锁 - 监控:监控事务性能和锁等待情况
- 优化:根据实际情况调整事务和锁的使用
- 持续学习:关注 MySQL 的新特性和优化技巧
事务控制
单行写法:开启事务
START TRANSACTION / BEGIN
-- 开启事务
START TRANSACTION;
换行写法:提交事务
COMMIT
-- 提交事务并持久化变更
START TRANSACTION;
INSERT INTO users (username, email) VALUES ('张三', 'zhangsan@example.com');
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;
COMMIT;
单行写法:回滚事务
ROLLBACK
-- 回滚事务撤销变更
ROLLBACK;
换行写法:使用保存点
SAVEPOINT <保存点名> / ROLLBACK TO <保存点名>
-- 使用保存点部分回滚
START TRANSACTION;
INSERT INTO users (username) VALUES ('张三');
SAVEPOINT sp1;
INSERT INTO users (username) VALUES ('李四');
ROLLBACK TO sp1;
COMMIT;
单行写法:释放保存点
RELEASE SAVEPOINT <保存点名>
-- 释放指定保存点
RELEASE SAVEPOINT sp1;
隔离级别
单行写法:查看隔离级别
SELECT @@transaction_isolation
-- 查看当前事务隔离级别
SELECT @@transaction_isolation;
单行写法:查看旧变量名隔离级别(仅 5.7 及更早,8.0 已移除该变量)
SELECT @@tx_isolation(5.7)
-- 5.7 及更早版本使用旧变量名;8.0 起请改用 @@transaction_isolation
SELECT @@tx_isolation;
单行写法:设置会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL <级别>
-- 设置会话隔离级别为读已提交
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
单行写法:设置全局隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL <级别>
-- 设置全局隔离级别为可序列化
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
单行写法:通过变量设置全局隔离级别
SET GLOBAL transaction_isolation = '<级别>'
-- 通过变量设置全局隔离级别
SET GLOBAL transaction_isolation = 'READ-COMMITTED';
单行写法:通过变量设置会话隔离级别
SET SESSION transaction_isolation = '<级别>'
-- 通过变量设置会话隔离级别
SET SESSION transaction_isolation = 'REPEATABLE-READ';
锁机制
单行写法:加共享锁
SELECT ... FOR SHARE(8.0+ 推荐;旧写法 LOCK IN SHARE MODE 已废弃)
-- 查询时加共享锁
SELECT * FROM users WHERE id = 1 FOR SHARE;
单行写法:加排他锁
SELECT ... FOR UPDATE
-- 查询时加排他锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;
单行写法:INSERT 自动加排他锁
INSERT INTO <表名> (<列名>) VALUES (<值>)
-- 插入操作自动加排他锁
INSERT INTO users (name) VALUES ('John');
单行写法:UPDATE 自动加排他锁
UPDATE <表名> SET <列名> = <值> WHERE <条件>
-- 更新操作自动加排他锁
UPDATE users SET name = 'John' WHERE id = 1;
单行写法:DELETE 自动加排他锁
DELETE FROM <表名> WHERE <条件>
-- 删除操作自动加排他锁
DELETE FROM users WHERE id = 1;
锁等待与超时
单行写法:查看锁等待超时
SELECT @@innodb_lock_wait_timeout
-- 查看锁等待超时时间
SELECT @@innodb_lock_wait_timeout;
单行写法:设置锁等待超时
SET SESSION innodb_lock_wait_timeout = <秒数>
-- 设置锁等待超时为 30 秒
SET SESSION innodb_lock_wait_timeout = 30;
死锁检测
单行写法:查看 InnoDB 状态
SHOW ENGINE INNODB STATUS
-- 查看死锁日志
SHOW ENGINE INNODB STATUS;
单行写法:开启死锁检测
SET GLOBAL innodb_deadlock_detect = ON
-- 开启死锁检测
SET GLOBAL innodb_deadlock_detect = ON;
事务实战
换行写法:转账事务
START TRANSACTION; <DML>; COMMIT;
-- 转账事务保证原子性
START TRANSACTION;
UPDATE accounts SET balance = balance - 1000 WHERE user_id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE user_id = 2;
COMMIT;
换行写法:条件提交
IF <条件> THEN COMMIT; ELSE ROLLBACK; END IF
-- 检查余额后决定提交或回滚
START TRANSACTION;
UPDATE accounts SET balance = balance - 1000 WHERE user_id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE user_id = 2;
IF (SELECT balance FROM accounts WHERE user_id = 1) < 0 THEN
ROLLBACK;
ELSE
COMMIT;
END IF;
换行写法:订单创建事务
START TRANSACTION; <DML>; SET @变量; <DML>; COMMIT;
-- 订单创建事务包含订单和订单项
START TRANSACTION;
INSERT INTO orders (user_id, total_amount) VALUES (1, 500);
SET @order_id = LAST_INSERT_ID();
INSERT INTO order_items (order_id, product_id, quantity, price) VALUES
(@order_id, 101, 2, 200),
(@order_id, 102, 1, 100);
UPDATE products SET stock = stock - 3 WHERE id IN (101, 102);
COMMIT;
换行写法:悲观锁查询
SELECT ... FOR UPDATE
-- 先锁定再更新
SELECT * FROM users WHERE id = 1 FOR UPDATE;
UPDATE users SET status = 0 WHERE last_login_time < '2023-01-01';
换行写法:批量删除事务
START TRANSACTION; <DML>; COMMIT;
-- 批量更新避免长事务
START TRANSACTION;
UPDATE users SET status = 0 WHERE last_login_time < '2023-01-01';
UPDATE stats SET inactive_users = inactive_users + 1;
COMMIT;
单行写法:分批删除
DELETE FROM <表名> WHERE <条件> LIMIT <N>
-- 分批删除避免锁表
DELETE FROM logs WHERE created_at < '2023-01-01' LIMIT 1000;