前置知识: MySQL

事务与锁机制

12 min中级

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,记录最后修改该行的事务 ID
  • DB_ROLL_PTR:回滚指针,指向 Undo Log 中的历史版本
  • DB_ROW_ID:行 ID,自增,用于聚簇索引

3.2.2 Undo Log

  • 作用:保存数据的历史版本,用于事务回滚和 MVCC
  • 类型:
  • INSERT Undo Log:记录插入操作,事务提交后可删除
  • UPDATE Undo Log:记录更新操作,事务提交后需要保留,用于 MVCC
  • DELETE Undo Log:记录删除操作,事务提交后需要保留,用于 MVCC

3.2.3 ReadView

  • 作用:判断数据版本的可见性
  • 组成:
  • m_ids:当前活跃事务的 ID 集合
  • min_trx_id:活跃事务中最小的 ID
  • max_trx_id:下一个将要分配的事务 ID
  • creator_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:死锁排查

问题:应用程序出现死锁错误 分析:

  1. 查看死锁日志:SHOW ENGINE INNODB STATUS;
  2. 发现两个事务相互等待对方的锁
  3. 分析 SQL 语句,发现访问表的顺序不同 解决方案:
  4. 统一访问表的顺序
  5. 减小事务粒度
  6. 使用索引优化查询

8.2 案例 2:事务性能优化

问题:事务执行时间过长,导致并发性能下降 分析:

  1. 查看慢查询日志
  2. 发现事务中包含大量计算和网络操作
  3. 事务持有锁的时间过长 解决方案:
  4. 将计算和网络操作移到事务外
  5. 拆分大事务为小事务
  6. 优化 SQL 查询

8.3 案例 3:隔离级别选择

问题:应用程序出现幻读 分析:

  1. 检查隔离级别:SELECT @@transaction_isolation;
  2. 发现使用的是读已提交隔离级别
  3. 业务需求需要可重复读 解决方案:
  4. 将隔离级别设置为可重复读:SET SESSION transaction_isolation = 'REPEATABLE-READ';
  5. 优化查询,使用索引

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;