一、脏读、不可重复读、幻读的本质:事务隔离性缺陷的三种表现
1. 脏读(Dirty Read)
定义:一个事务读取到另一个未提交事务修改的数据。
核心问题:数据尚未最终确认,可能回滚导致读取无效值。
模拟场景(以银行转账为例):
// 假设两个事务同时操作账户A(初始余额1000元)
// 事务1:转账操作(未提交)
beginTransaction(); // 开启事务
$sql = "UPDATE accounts SET balance = balance - 500 WHERE id = 1";
execute($sql); // 扣除500元(此时余额变为500,但未提交)
// 事务2:查询操作(脏读发生)
$sql = "SELECT balance FROM accounts WHERE id = 1";
$result = execute($sql); // 读取到临时的500元(实际事务1可能回滚)
底层原理:
- 未提交的数据存放在内存临时空间(未写入磁盘)。
- 事务隔离级别不足(如
READ UNCOMMITTED
)时,允许读取临时数据。 - 风险:若事务1回滚,事务2读取的500元是无效的“脏数据”。
2. 不可重复读(Non-repeatable Read)
定义:一个事务内多次读取同一数据时,结果不一致(因其他事务修改并提交了数据)。
核心问题:数据在事务执行过程中被中途修改。
模拟场景(报表生成场景):
// 事务1:生成报表(需要两次查询余额)
beginTransaction();
// 第一次查询(初始余额1000元)
$sql = "SELECT balance FROM accounts WHERE id = 1";
$balance1 = execute($sql); // 1000元
// 此时事务2修改并提交数据
beginTransaction();
$sql = "UPDATE accounts SET balance = 800 WHERE id = 1";
execute($sql);
commit(); // 事务2提交,余额变为800元
// 事务1第二次查询(结果不一致)
$sql = "SELECT balance FROM accounts WHERE id = 1";
$balance2 = execute($sql); // 800元
echo "第一次查询:{$balance1},第二次查询:{$balance2}"; // 输出 1000 和 800
底层原理:
- 其他事务提交的已确认数据(写入磁盘)会影响当前事务后续查询。
- 本质区别于脏读:脏读是未提交数据,不可重复读是已提交数据。
- 隔离级别影响:
READ COMMITTED
可避免脏读,但无法避免不可重复读(因允许读取已提交数据的最新值)。
3. 幻读(Phantom Read)
定义:一个事务按条件查询数据时,两次查询结果集不同(因其他事务插入/删除了符合条件的新数据)。
核心问题:数据集合的“幻影”变化(新增或消失的行)。
模拟场景(订单统计场景):
// 事务1:统计订单量(条件:状态=未支付)
beginTransaction();
// 第一次查询(假设当前有2条未支付订单)
$sql = "SELECT COUNT(*) FROM orders WHERE status = '未支付'";
$count1 = execute($sql); // 2条
// 此时事务2插入一条新的未支付订单并提交
beginTransaction();
$sql = "INSERT INTO orders (status) VALUES ('未支付')";
execute($sql);
commit(); // 新增1条未支付订单
// 事务1第二次查询(结果集新增数据,产生幻读)
$sql = "SELECT COUNT(*) FROM orders WHERE status = '未支付'";
$count2 = execute($sql); // 3条
echo "第一次统计:{$count1}条,第二次统计:{$count2}条"; // 输出 2 和 3
底层原理:
- 幻读的核心是“行记录的增删”,而不可重复读是“行记录的修改”。
- 普通索引(非唯一索引)无法锁定新增行,导致查询时出现“幻影”。
- 隔离级别影响:
REPEATABLE READ
(可重复读)通过**间隙锁(Gap Lock)**锁定查询条件的范围,阻止其他事务插入符合条件的行,从而避免幻读。
二、三者的对比与本质区别
问题类型 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
数据操作 | 读取未提交的修改 | 读取已提交的修改 | 读取已提交的新增/删除 |
影响范围 | 单条数据 | 单条数据 | 数据集合(多行) |
隔离级别解决 | READ COMMITTED 以上 | REPEATABLE READ 以上 | REPEATABLE READ (需间隙锁) |
底层关键 | 临时数据可见性 | 数据版本一致性 | 索引范围锁(Gap Lock) |
三、为什么需要解决这些问题?——事务隔离级别的核心目标
数据库通过事务隔离级别控制不同事务间的可见性,本质是在性能和数据一致性间做权衡:
-
低隔离级别(如
READ UNCOMMITTED
):- 允许脏读、不可重复读、幻读,性能最高(锁少)。
- 适用场景:非关键业务(如日志统计、临时数据查询)。
-
中等隔离级别(如
READ COMMITTED
):- 避免脏读,但允许不可重复读和幻读。
- 适用场景:大部分业务(如电商订单查询、金融交易查询)。
-
高隔离级别(如
REPEATABLE READ
):- 通过MVCC(多版本并发控制)和间隙锁避免三种问题。
- 适用场景:高一致性需求(如金融转账、库存扣减)。
四、PHP代码中的实战控制(以PDO为例)
// 配置数据库连接(以MySQL为例)
$pdo = new PDO(
'mysql:host=localhost;dbname=test;charset=utf8mb4',
'user',
'pass',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4',
PDO::ATTR_EMULATE_PREPARES => false, // 禁用预处理模拟,使用数据库原生预处理
]
);
// 设置事务隔离级别(示例:可重复读)
$pdo->exec('SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ');
// 开启事务
$pdo->beginTransaction();
try {
// 业务操作:例如查询数据(依赖隔离级别保证一致性)
$stmt = $pdo->query('SELECT * FROM accounts WHERE id = 1');
$data = $stmt->fetch();
// 模拟业务逻辑处理...
$pdo->commit(); // 提交事务,数据永久生效
} catch (Exception $e) {
$pdo->rollBack(); // 回滚事务,利用Undo Log撤销修改
throw $e;
}
代码关键点注释:
-
SET SESSION TRANSACTION ISOLATION LEVEL
:- 配置当前会话的隔离级别,影响后续所有事务。
- 为什么这样写?:通过SQL命令直接告诉数据库如何控制事务间的可见性。
-
beginTransaction()
与commit()/rollBack()
:- 开启事务后,所有操作先记录在Redo Log(用于持久化)和Undo Log(用于回滚)。
- 提交时,Redo Log写入磁盘,数据永久修改;回滚时,Undo Log将数据恢复到事务前状态。
-
ATTR_EMULATE_PREPARES => false
:- 强制使用数据库原生预处理,避免PHP模拟预处理导致的隔离级别失效(如无法正确应用间隙锁)。
五、底层原理总结:隔离级别的实现基石
-
Undo Log的作用:
- 记录数据修改前的版本,用于回滚(如脏读场景中,事务回滚时通过Undo Log恢复原始值)。
- 在
REPEATABLE READ
级别下,配合MVCC提供历史版本数据,避免不可重复读。
-
Redo Log的作用:
- 记录数据修改后的版本,用于事务提交时的持久化(先写日志再写磁盘,提升性能)。
- 崩溃恢复时,通过Redo Log重做未完成的事务操作。
-
锁机制(Lock)与MVCC的配合:
- 低隔离级别:依赖锁(如共享锁、排他锁)控制访问,但锁粒度粗(表锁)会影响性能。
- 高隔离级别:通过MVCC(多版本)避免锁竞争,仅在必要时加间隙锁(如幻读场景)锁定索引范围,阻止插入操作。
六、总结:从现象到本质的思考路径
- 脏读:根源是“未提交数据的可见性”,通过禁止读取未提交数据(
READ COMMITTED
)解决。 - 不可重复读:根源是“事务内数据版本不一致”,通过MVCC提供一致性读视图(读已提交或历史版本)解决。
- 幻读:根源是“数据集合的动态变化”,通过间隙锁锁定索引范围,阻止其他事务插入符合条件的行。
核心认知:三种问题本质是事务并发时的“隔离性缺陷”,数据库通过**日志系统(Redo/Undo Log)和并发控制机制(锁/MVCC)**实现不同隔离级别的平衡,最终保障ACID中的“I(隔离性)”。