当前位置: 首页 > news >正文

数据库在并发访问时,不同隔离级别下脏读幻读问题

数据库隔离级别并非安装后就固定,绝大多数主流数据库(如MySQL、PostgreSQL、SQL Server)都支持动态调整和运行中自定义,具体调整范围可分为全局、会话和语句三个层级。

  1. 全局级别调整:修改数据库配置文件(如MySQL的my.cnf)并重启服务,会影响所有新创建的会话,属于长期生效的配置。
  2. 会话级别调整:在当前数据库连接中执行特定SQL命令(如MySQL的SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED),仅对当前会话生效,关闭连接后失效,适合临时切换隔离级别。
  3. 语句级别调整:部分数据库支持为单个事务语句指定隔离级别(如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:修改用户余额但不提交)
1BEGIN;(开启事务)
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(事务未提交)
3SELECT balance FROM user_balance WHERE user_id=1001;
– 结果:800.00(读取到事务B未提交的修改,脏读发生!)
-
4-ROLLBACK;(事务B回滚,修改作废)
5SELECT balance FROM user_balance WHERE user_id=1001;
– 结果:1000.00(数据恢复,验证步骤3读的是脏数据)
-
6COMMIT;(关闭事务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:修改并提交用户余额)
1BEGIN;(开启事务)
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;(提交事务,修改生效)
3SELECT balance FROM user_balance WHERE user_id=1001;
– 结果:800.00(与步骤1结果不一致,不可重复读发生!)
-
4COMMIT;(关闭事务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:插入新用户并提交)
1BEGIN;(开启事务)
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;(提交事务,新用户插入生效)
3SELECT COUNT(*) FROM user_balance WHERE user_id > 1000;
– 结果:2(新增了user_id=1002,行数不一致,幻读发生!)
-
4COMMIT;(关闭事务A)-
2. 解决幻读:使用「REPEATABLE READ」或「SERIALIZABLE」隔离级别
-- 两个会话均设置隔离级别为可重复读(MySQL默认)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;-- 重复上述步骤2,会发现:步骤3中会话1查询的COUNT(*)仍为1(事务B插入的新数据对事务A不可见),幻读被禁止。-- 若用SERIALIZABLE级别:会话2插入数据时会被阻塞,直到会话1提交事务,彻底避免幻读(但性能损耗大)。

六、关键总结

  1. 问题本质:并发事务对数据的「修改/插入」与「读取」的时序冲突,隔离级别通过控制数据可见性解决冲突。
  2. MySQL默认隔离级别:REPEATABLE READ,已能禁止脏读、不可重复读、幻读(InnoDB的MVCC机制实现),兼顾性能与一致性。
  3. 语法记忆
    • 查看当前会话隔离级别: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. 配置方式(永久生效)

  1. 找到 MySQL 5.7 的配置文件(路径因系统而异):
    • Linux:通常为 /etc/my.cnf/etc/mysql/my.cnf
    • Windows:通常为 MySQL安装目录/my.ini
  2. [mysqld] 模块下添加/修改参数:
    [mysqld]
    # 设置默认事务隔离级别为可重复读(MySQL 5.7 默认值,可根据需求修改)
    transaction_isolation = REPEATABLE-READ
    
  3. 重启 MySQL 服务使配置生效:
    • Linux:systemctl restart mysqld
    • Windows:在“服务”中重启“MySQL”服务

3. 临时生效方式(当前会话/全局)

若无需永久修改,可通过 SQL 语句临时设置(重启服务后失效):

  • 当前会话生效
    SET SESSION transaction_isolation = 'READ-COMMITTED';
    
  • 全局生效(对新会话生效,已存在会话不影响)
    SET GLOBAL transaction_isolation = 'SERIALIZABLE';
    

4. 验证隔离级别

通过以下 SQL 查看当前生效的隔离级别:

-- 查看当前会话的隔离级别
SELECT @@session.transaction_isolation;-- 查看全局的隔离级别
SELECT @@global.transaction_isolation;

文章转载自:

http://6UQ1ARGS.wdrxh.cn
http://ZyqjNVFk.wdrxh.cn
http://tIeib9eL.wdrxh.cn
http://XcHL4Ukk.wdrxh.cn
http://NESVEUva.wdrxh.cn
http://6rhT5ctY.wdrxh.cn
http://dJaLfIbD.wdrxh.cn
http://VpnVnMLu.wdrxh.cn
http://Wnt5AILx.wdrxh.cn
http://Zea7QxPR.wdrxh.cn
http://4bDLojyN.wdrxh.cn
http://VmugizIi.wdrxh.cn
http://c3TJQYg0.wdrxh.cn
http://gyPsgfJE.wdrxh.cn
http://YFpro9v6.wdrxh.cn
http://v8ydUtd7.wdrxh.cn
http://SiCmb8Vg.wdrxh.cn
http://phamobjo.wdrxh.cn
http://pnDxqMyx.wdrxh.cn
http://gPGC4mi0.wdrxh.cn
http://625Bv44c.wdrxh.cn
http://ZD324U0C.wdrxh.cn
http://3WFN5QKm.wdrxh.cn
http://yPT7CfJl.wdrxh.cn
http://zRdpdVyI.wdrxh.cn
http://XYe7v0pY.wdrxh.cn
http://YocWsJ6d.wdrxh.cn
http://eFKqHS29.wdrxh.cn
http://ty1XHBJO.wdrxh.cn
http://5uhNb0mF.wdrxh.cn
http://www.dtcms.com/a/382662.html

相关文章:

  • Python核心技术开发指南(065)——with语句
  • Python核心技术开发指南(064)——析构方法
  • 20250913-01: Langchain概念:Runnable可运行接口
  • 记一次谷歌语法获取路径 针对空白页面
  • Java GC:从GC Roots到分代设计的哲学
  • 一款4000℃高温材料设计方案及性能预测
  • 【leetcode】64. 最小路径和
  • 2.10组件间的通信
  • MinerU学习
  • 网络安全学习
  • 如何用 Rust 重写 SQLite 数据库(一):项目探索
  • Qwen3-80B-A3B混合注意力机制
  • OBS使用教程:OBS多路推流插件如何下载?如何安装使用?
  • 禁用 vscode 的终端的粘滞滚动
  • 人工智能通识与实践 - 人工智能概述
  • Symantec卸载
  • 第34章 AI在文娱与内容创作领域的应用
  • 学生信息管理系统(面向对象初步接触)
  • LangChain 中 Output Parsers 是什么?
  • Wolfspeed重组计划已确认
  • 【C++】继承机制深度解析:多继承与菱形继承
  • 如何用Maxscript在选择样条线顶点放置球体?
  • (LeetCode 面试经典 150 题) 190. 颠倒二进制位(位运算)
  • P1043题解
  • 如何用 Rust 重写 SQLite 数据库(二):项目探索
  • SQLI-labs[Part 2]
  • 如何安装 Prometheus 2.20.0 for Windows(amd64 版本详细步骤)​
  • 1004:字符三角形
  • Python 生成乘法练习题:一位数乘以两位数(乘积小于100)
  • 打工人日报#20250913