mysql脏读是怎么产生的_mysql事务问题说明

来源:这里教程网 时间:2026-02-28 20:37:01 作者:

脏读只发生在 READ UNCOMMITTED 隔离级别下

MySQL 默认隔离级别是

REPEATABLE READ
,这个级别下不会发生脏读。只有显式把事务设为
READ UNCOMMITTED
,才可能读到其他事务尚未提交的修改。

常见错误现象:应用中执行了

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
,又没意识到后续所有
SELECT
都可能读到未提交数据;或者 ORM(如 Django)配置了全局低隔离级别,导致业务查询意外读取到回滚前的中间状态。

使用场景极少:一般只用于对一致性无要求的统计类查询(如实时在线人数估算),且必须接受“可能读到根本不存在的数据”
READ UNCOMMITTED
下,
SELECT
不加锁,也不检查行版本,直接读最新写入的记录
一旦另一个事务在你
SELECT
后立刻
ROLLBACK
,你就拿到了逻辑上从未成功存在的值

如何复现一次典型的脏读

需要两个并发会话,手动控制事务节奏。关键点在于:Session B 在 Session A 提交前就读取了其修改。

-- Session A
START TRANSACTION;
UPDATE accounts SET balance = 1000 WHERE id = 1;
-- 不执行 COMMIT 或 ROLLBACK,保持事务开启
<p>-- Session B(已设为 READ UNCOMMITTED)
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1; -- 返回 1000,即脏读
-- 此时若 Session A 执行 ROLLBACK,则该 1000 从未真正生效

为什么 REPEATABLE READ 能避免脏读但仍有幻读

REPEATABLE READ
使用多版本并发控制(MVCC),每个事务启动时拍一个快照,后续所有
SELECT
都基于这个快照——它天然过滤掉未提交事务的变更,所以不可能读到脏数据。

但它不阻止其他事务插入新行并提交,因此同一范围的

SELECT
可能两次返回不同数量的行(幻读)。这不是脏读,因为插入的行是已提交的合法数据。

脏读本质是“读到了不该存在的数据”,幻读本质是“读到了新出现的、合法的数据” 想彻底避免幻读,需升级到
SERIALIZABLE
(加范围锁)或用
SELECT ... FOR UPDATE
显式锁定区间
注意:
innodb_locks_unsafe_for_binlog=ON
(已弃用)曾让 RR 行为退化,现代 MySQL 无需担心

ORM 和连接池常悄悄改变隔离级别

Django 的

ATOMIC_REQUESTS
、Spring 的
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
、或是某些数据库连接池(如 HikariCP)预设的
connection-init-sql
,都可能覆盖会话默认隔离级别。

排查时不要只看代码里的 SQL,要确认实际执行前的会话状态:

SELECT @@transaction_isolation, @@session.transaction_isolation;
连接池可能复用连接,而上一个使用者改过隔离级别,导致当前请求“继承”了异常设置 Go 的
database/sql
包中,
db.Exec("SET SESSION...")
只影响单次调用,但若用
tx, _ := db.Begin()
,则需在
tx
上显式
Exec
PHP PDO 中,
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, ...)
不影响隔离级别,但
$pdo->exec("SET SESSION ...")
会影响后续所有语句

实际线上环境几乎从不主动启用

READ UNCOMMITTED
,真正容易被忽略的是:隔离级别被某处配置静默修改后,脏读成为偶发、难复现的数据异常根源。

相关推荐