数据库在并发访问时,不同隔离级别下脏读幻读问题
数据库隔离级别并非安装后就固定,绝大多数主流数据库(如MySQL、PostgreSQL、SQL Server)都支持动态调整和运行中自定义,具体调整范围可分为全局、会话和语句三个层级。
- 全局级别调整:修改数据库配置文件(如MySQL的my.cnf)并重启服务,会影响所有新创建的会话,属于长期生效的配置。
- 会话级别调整:在当前数据库连接中执行特定SQL命令(如MySQL的
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED
),仅对当前会话生效,关闭连接后失效,适合临时切换隔离级别。 - 语句级别调整:部分数据库支持为单个事务语句指定隔离级别(如SQL Server的
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN TRANSACTION; ... COMMIT;
),仅对该次事务生效,灵活性最高。
不同数据库的具体调整语法略有差异,但核心逻辑均支持动态修改,无需重新安装数据库。
要模拟MySQL 5.7中事务并发的脏读、不可重复读、幻读,需先创建测试表和基础数据,再通过「两个会话模拟并发事务」,结合不同隔离级别验证问题及解决办法。以下是完整步骤:
一、基础准备:创建表与初始化数据
1. 创建测试表(用户余额表)
-- 建表:id(主键)、user_id(用户ID)、balance(余额)
CREATE TABLE `user_balance` (`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '主键',`user_id` INT(11) NOT NULL COMMENT '用户ID',`balance` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '账户余额',PRIMARY KEY (`id`),UNIQUE KEY `idx_user_id` (`user_id`) -- 唯一索引,确保用户ID不重复
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户余额表';
2. 插入初始化数据
-- 插入1条测试数据:用户ID=1001,初始余额1000元
INSERT INTO user_balance (user_id, balance) VALUES (1001, 1000.00);-- 验证数据
SELECT * FROM user_balance WHERE user_id = 1001;
二、核心概念:MySQL隔离级别与并发问题
MySQL 5.7默认隔离级别是 REPEATABLE READ(可重复读),不同隔离级别对并发问题的抑制能力不同:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ UNCOMMITTED(读未提交) | 允许 | 允许 | 允许 |
READ COMMITTED(读已提交) | 禁止 | 允许 | 允许 |
REPEATABLE READ(可重复读) | 禁止 | 禁止 | 禁止(InnoDB通过MVCC实现) |
SERIALIZABLE(串行化) | 禁止 | 禁止 | 禁止 |
模拟规则:需打开「两个MySQL会话」(如Navicat的两个查询窗口、CMD的两个mysql连接),分别执行「事务A」和「事务B」,按步骤操作。
三、场景1:脏读(Dirty Read)
什么是脏读?
事务A读取了事务B未提交的修改数据,若事务B后续回滚,事务A读取的就是“无效脏数据”。
1. 模拟脏读(需先设置隔离级别为「READ UNCOMMITTED」)
步骤1:两个会话均设置隔离级别
-- 会话1、会话2均执行:设置当前会话隔离级别为读未提交
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
步骤2:开启事务并执行操作(按顺序执行)
步骤 | 会话1(事务A:查询用户余额) | 会话2(事务B:修改用户余额但不提交) |
---|---|---|
1 | BEGIN; (开启事务)SELECT balance FROM user_balance WHERE user_id=1001; – 结果:1000.00 | - |
2 | - | BEGIN; (开启事务)UPDATE user_balance SET balance=balance-200 WHERE user_id=1001; – 不执行COMMIT(事务未提交) |
3 | SELECT balance FROM user_balance WHERE user_id=1001; – 结果:800.00(读取到事务B未提交的修改,脏读发生!) | - |
4 | - | ROLLBACK; (事务B回滚,修改作废) |
5 | SELECT balance FROM user_balance WHERE user_id=1001; – 结果:1000.00(数据恢复,验证步骤3读的是脏数据) | - |
6 | COMMIT; (关闭事务A) | - |
2. 解决脏读:提升隔离级别至「READ COMMITTED」及以上
-- 两个会话均设置隔离级别为读已提交
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;-- 重复上述步骤2,会发现:步骤3中会话1读取的余额仍为1000.00(事务B未提交的修改不可见),脏读被禁止。
四、场景2:不可重复读(Non-Repeatable Read)
什么是不可重复读?
事务A在同一事务内多次读取同一数据,若事务B在两次读取间「提交了修改」,则事务A两次读取的结果不一致。
1. 模拟不可重复读(需设置隔离级别为「READ COMMITTED」)
步骤1:两个会话均设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
步骤2:开启事务并执行操作(按顺序执行)
步骤 | 会话1(事务A:多次查询同一用户余额) | 会话2(事务B:修改并提交用户余额) |
---|---|---|
1 | BEGIN; (开启事务)SELECT balance FROM user_balance WHERE user_id=1001; – 结果:1000.00 | - |
2 | - | BEGIN; (开启事务)UPDATE user_balance SET balance=balance-200 WHERE user_id=1001; COMMIT; (提交事务,修改生效) |
3 | SELECT balance FROM user_balance WHERE user_id=1001; – 结果:800.00(与步骤1结果不一致,不可重复读发生!) | - |
4 | COMMIT; (关闭事务A) | - |
2. 解决不可重复读:提升隔离级别至「REPEATABLE READ」及以上
-- 两个会话均设置隔离级别为可重复读(MySQL默认级别)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;-- 重复上述步骤2,会发现:步骤3中会话1读取的余额仍为1000.00(事务B提交的修改对事务A不可见),不可重复读被禁止。
五、场景3:幻读(Phantom Read)
什么是幻读?
事务A在同一事务内按同一条件多次查询,若事务B在两次查询间「提交了新数据插入/删除」,则事务A两次查询的「结果行数不一致」(像出现了“幻觉”)。
1. 模拟幻读(需设置隔离级别为「READ COMMITTED」,MySQL默认的REPEATABLE READ已禁止幻读)
步骤1:两个会话均设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
步骤2:开启事务并执行操作(按顺序执行)
步骤 | 会话1(事务A:按条件多次查询用户) | 会话2(事务B:插入新用户并提交) |
---|---|---|
1 | BEGIN; (开启事务)SELECT COUNT(*) FROM user_balance WHERE user_id > 1000; – 结果:1(仅user_id=1001) | - |
2 | - | BEGIN; (开启事务)INSERT INTO user_balance (user_id, balance) VALUES (1002, 1500.00); COMMIT; (提交事务,新用户插入生效) |
3 | SELECT COUNT(*) FROM user_balance WHERE user_id > 1000; – 结果:2(新增了user_id=1002,行数不一致,幻读发生!) | - |
4 | COMMIT; (关闭事务A) | - |
2. 解决幻读:使用「REPEATABLE READ」或「SERIALIZABLE」隔离级别
-- 两个会话均设置隔离级别为可重复读(MySQL默认)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;-- 重复上述步骤2,会发现:步骤3中会话1查询的COUNT(*)仍为1(事务B插入的新数据对事务A不可见),幻读被禁止。-- 若用SERIALIZABLE级别:会话2插入数据时会被阻塞,直到会话1提交事务,彻底避免幻读(但性能损耗大)。
六、关键总结
- 问题本质:并发事务对数据的「修改/插入」与「读取」的时序冲突,隔离级别通过控制数据可见性解决冲突。
- MySQL默认隔离级别:REPEATABLE READ,已能禁止脏读、不可重复读、幻读(InnoDB的MVCC机制实现),兼顾性能与一致性。
- 语法记忆:
- 查看当前会话隔离级别:
SELECT @@tx_isolation;
(MySQL 5.7)/SELECT @@transaction_isolation;
(MySQL 8.0+) - 设置会话隔离级别:
SET SESSION TRANSACTION ISOLATION LEVEL 级别名称;
- 开启/提交/回滚事务:
BEGIN;
/COMMIT;
/ROLLBACK;
- 查看当前会话隔离级别:
在 MySQL 5.7 配置文件中,用于设置事务默认隔离级别的参数是 transaction_isolation
(或旧版兼容参数 tx_isolation
,两者功能一致,推荐使用 transaction_isolation
)。
1. 参数说明
- 核心作用:定义 MySQL 实例启动后,所有新创建会话的默认事务隔离级别,无需在每个会话中手动设置。
- 参数值(对应 4 种隔离级别):
READ-UNCOMMITTED
:读未提交(可能出现脏读、不可重复读、幻读)READ-COMMITTED
:读已提交(避免脏读,可能出现不可重复读、幻读)REPEATABLE-READ
:可重复读(MySQL 5.7 默认级别,避免脏读、不可重复读,通过 MVCC 减少幻读)SERIALIZABLE
:串行化(完全避免三种问题,性能最低)
2. 配置方式(永久生效)
- 找到 MySQL 5.7 的配置文件(路径因系统而异):
- Linux:通常为
/etc/my.cnf
或/etc/mysql/my.cnf
- Windows:通常为
MySQL安装目录/my.ini
- Linux:通常为
- 在
[mysqld]
模块下添加/修改参数:[mysqld] # 设置默认事务隔离级别为可重复读(MySQL 5.7 默认值,可根据需求修改) transaction_isolation = REPEATABLE-READ
- 重启 MySQL 服务使配置生效:
- Linux:
systemctl restart mysqld
- Windows:在“服务”中重启“MySQL”服务
- Linux:
3. 临时生效方式(当前会话/全局)
若无需永久修改,可通过 SQL 语句临时设置(重启服务后失效):
- 当前会话生效:
SET SESSION transaction_isolation = 'READ-COMMITTED';
- 全局生效(对新会话生效,已存在会话不影响):
SET GLOBAL transaction_isolation = 'SERIALIZABLE';
4. 验证隔离级别
通过以下 SQL 查看当前生效的隔离级别:
-- 查看当前会话的隔离级别
SELECT @@session.transaction_isolation;-- 查看全局的隔离级别
SELECT @@global.transaction_isolation;