MySQL 事务隔离级别与并发问题深度解析及实战演示
事务的核心特性(ACID)
关系型数据库中的事务(Transaction)代表了一个逻辑上的最小工作单元。它要求内部包含的所有数据库操作必须作为一个整体被执行,即要么全部生效,要么全部撤销。InnoDB 存储引擎通过以下四大特性(ACID)来保障数据的可靠性:
- 原子性(Atomicity):事务是不可分割的整体。其底层依赖 Undo Log(回滚日志)来记录数据修改前的状态,一旦事务执行失败或主动回滚,引擎会利用 Undo Log 将数据恢复到初始状态。
- 一致性(Consistency):事务执行前后,数据库必须从一个完整性状态过渡到另一个完整性状态。这不仅依赖数据库自身的约束(如外键、唯一性),还需要应用程序的业务逻辑来共同保障。
- 隔离性(Isolation):在并发环境下,多个事务的执行应当互不干扰。InnoDB 主要通过锁机制(如行锁、表锁)和多版本并发控制(MVCC)来实现不同级别的隔离。
- 持久性(Durability):事务一旦成功提交,其对数据的修改就是永久性的。即使数据库进程崩溃或服务器宕机,Redo Log(重做日志)也能确保已提交的数据在恢复时不会丢失。
并发事务引发的数据异常
当多个事务同时操作相同的数据集时,如果隔离措施不当,可能会引发以下三种典型的并发问题:
- 脏读(Dirty Read):当前事务读取到了其他事务尚未提交的中间状态数据。若该事务随后发生回滚,当前事务基于这些"脏数据"所做的业务决策将是错误的。
- 不可重复读(Non-repeatable Read):在同一个事务的生命周期内,多次读取同一行数据却得到了不同的结果。这通常是因为在两次读取之间,该行数据被其他事务修改并提交了。
- 幻读(Phantom Read):在同一个事务内,多次执行相同条件的范围查询,返回的结果集行数不一致。这通常是因为其他事务在期间插入或删除了符合查询条件的新记录。
事务隔离级别与配置
为了平衡数据一致性与系统并发性能,SQL 标准定义了四种隔离级别。MySQL 默认采用可重复读(Repeatable Read)级别。
可以通过以下 SQL 语句查看和动态调整当前会话的隔离级别:
-- 获取当前会话的事务隔离级别
SELECT @@transaction_isolation AS current_isolation_level;
-- 切换会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
隔离级别实战演示
以下演示基于 employee_salary 表(包含 emp_id 和 salary 字段),通过模拟客户端 A 和客户端 B 的并发交互来验证各隔离级别的行为。
1. 读未提交(Read Uncommitted)
在此级别下,事务可以读取其他事务未提交的修改,无法解决任何并发问题。
-- 客户端 A
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
SELECT salary FROM employee_salary WHERE emp_id = 101;
-- 初始结果: 5000
-- 客户端 B
BEGIN;
UPDATE employee_salary SET salary = 8000 WHERE emp_id = 101;
-- 此时 B 尚未提交事务
-- 客户端 A
SELECT salary FROM employee_salary WHERE emp_id = 101;
-- 结果: 8000 (发生脏读,读取了 B 未提交的数据)
-- 客户端 B
ROLLBACK; -- B 撤销了修改,A 之前读取的 8000 成为无效的脏数据
2. 读已提交(Read Committed)
此级别解决了脏读,但无法避免不可重复读。其底层原因在于 ReadView 的生成时机:InnoDB 会在每次执行快照读(普通 SELECT)时重新生成一个 ReadView。
-- 客户端 A
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT salary FROM employee_salary WHERE emp_id = 101;
-- 初始结果: 5000
-- 客户端 B
BEGIN;
UPDATE employee_salary SET salary = 8000 WHERE emp_id = 101;
COMMIT; -- B 提交了修改
-- 客户端 A
SELECT salary FROM employee_salary WHERE emp_id = 101;
-- 结果: 8000 (发生不可重复读,因为 A 的第二次查询生成了新的 ReadView,看到了 B 的提交)
3. 可重复读(Repeatable Read)
MySQL 的默认级别。它解决了不可重复读,因为在事务首次执行快照读时生成 ReadView 后,后续的所有快照读都会复用该 ReadView。但在特定场景下仍会引发幻读现象。
-- 客户端 A
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT * FROM employee_salary WHERE emp_id = 105;
-- 结果: Empty set (记录不存在)
-- 客户端 B
BEGIN;
INSERT INTO employee_salary (emp_id, salary) VALUES (105, 6000);
COMMIT; -- B 插入并提交了新记录
-- 客户端 A
SELECT * FROM employee_salary WHERE emp_id = 105;
-- 结果: Empty set (快照读复用 ReadView,依然看不到 B 插入的记录)
INSERT INTO employee_salary (emp_id, salary) VALUES (105, 7000);
-- 报错: Duplicate entry '105' for key 'PRIMARY'
幻读现象解析:在 RR 级别下,普通的 SELECT 采用快照读,无法看到其他事务新插入的记录。然而,当执行 INSERT、UPDATE 或 DELETE 时,InnoDB 会进行当前读(Current Read)。这就导致了一个看似矛盾的现象:快照读显示记录不存在,但执行插入操作时却因检测到 B 已提交的记录而抛出主键冲突异常。
4. 串行化(Serializable)
最高隔离级别,强制事务串行执行,解决了所有并发问题,但极大地牺牲了并发性能。
-- 客户端 A
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
SELECT * FROM employee_salary WHERE emp_id = 108;
-- 自动加上共享锁(S锁),转化为当前读
-- 客户端 B
BEGIN;
INSERT INTO employee_salary (emp_id, salary) VALUES (108, 9000);
-- 尝试获取排他锁(X锁),因与 A 的 S 锁冲突而被阻塞
-- 最终报错: Lock wait timeout exceeded; try restarting transaction
-- 客户端 A
COMMIT; -- A 提交后释放 S 锁,B 的插入操作才得以继续执行