数据库 事务隔离级别 深入理解数据库事务隔离级别:脏读、不可重复读、幻读与串行化
概述
数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。
隔离级别一共有四种:
-
读未提交:READ UNCOMMITTED
允许Transaction01读取Transaction02未提交的修改。
-
读已提交:READ COMMITTED、
要求Transaction01只能读取Transaction02已提交的修改。
-
可重复读:REPEATABLE READ
确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务对这个字段进行更新。
-
串行化:SERIALIZABLE
确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。
各个隔离级别解决并发问题的能力见下表:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ UNCOMMITTED | 有 | 有 | 有 |
READ COMMITTED | 无 | 有 | 有 |
REPEATABLE READ | 无 | 无 | 有 |
SERIALIZABLE | 无 | 无 | 无 |
各种数据库产品对事务隔离级别的支持程度:
隔离级别 | Oracle | MySQL |
---|---|---|
READ UNCOMMITTED | × | √ |
READ COMMITTED | √(默认) | √ |
REPEATABLE READ | × | √(默认) |
SERIALIZABLE | √ | √ |
各种级别的详细解释
脏读 (Dirty Read)
定义:脏读发生在一个事务读取了另一个未提交事务修改的数据时。
发生场景:隔离级别为读未提交(READ UNCOMMITTED)时。
示例说明:
-- 事务A
BEGIN;
UPDATE accounts SET balance = 200 WHERE id = 1; -- 未提交-- 事务B
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- 可能读到200(脏数据)-- 事务A
ROLLBACK; -- 回滚后余额恢复原值,但事务B已读到不存在的数据
危害:基于可能回滚的临时数据做出决策,导致业务逻辑错误。
不可重复读 (Non-Repeatable Read)
定义:在同一事务中,两次读取同一数据得到不同结果。
发生场景:隔离级别为读已提交(READ COMMITTED)或更低时。
示例说明:
-- 事务A
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- 第一次读取,返回1000-- 事务B
BEGIN;
UPDATE accounts SET balance = 1500 WHERE id = 1;
COMMIT;-- 事务A
SELECT balance FROM accounts WHERE id = 1; -- 第二次读取,返回1500
COMMIT;
危害:破坏了事务内数据一致性视图,导致校验结果不可靠。
幻读 (Phantom Read)
定义:在同一事务中,两次相同查询返回的结果集行数不同。
发生场景:隔离级别为可重复读(REPEATABLE READ)或更低时。
示例说明:
-- 事务A
BEGIN;
SELECT * FROM accounts WHERE balance > 1000; -- 返回2条记录-- 事务B
BEGIN;
INSERT INTO accounts VALUES (3, '王五', 2000);
COMMIT;-- 事务A
SELECT * FROM accounts WHERE balance > 1000; -- 返回3条记录
COMMIT;
危害:基于过时的结果集前提进行操作,导致数据不一致。
串行化 (Serializable)
定义:最高隔离级别,强制事务串行执行。
工作机制:通过强锁机制(通常是表级锁)确保事务完全隔离。
示例说明:
-- 事务A
BEGIN;
SELECT * FROM accounts FOR UPDATE; -- 加锁-- 事务B
BEGIN;
SELECT * FROM accounts; -- 可能被阻塞或超时
优缺点:
优点:完全保证数据一致性
缺点:性能最低,并发性差
关于读已提交和可重复读
这是一个非常核心且容易混淆的概念。我们用一个非常详细的例子来把 读已提交 (Read Committed) 和 可重复读 (Repeatable Read) 彻底讲清楚。
想象一个简单的银行账户表 accounts:
id | name | balance |
---|---|---|
1 | 张三 | 1000 |
2 | 李四 | 2000 |
现在有两个并发的事务:事务A 和 事务B。
场景一:读已提交 (Read Committed)
核心思想: 一个事务只能读到其他事务已经提交的修改。它保证了你不会读到“脏数据”,但不保证在同一次事务中多次读取同一数据会得到相同的结果。
隔离级别设定:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
事件时序:
时间 | 事务A | 事务B | 说明 |
---|---|---|---|
T1 | BEGIN; | 事务A开始 | |
T2 | BEGIN; | 事务B开始 | |
T3 | SELECT balance FROM accounts WHERE id = 1; → 1000 | 事务A第一次查询张三的余额,结果是1000。 | |
T4 | UPDATE accounts SET balance = 1500 WHERE id = 1; | COMMIT; 事务B将张三的余额增加500并提交了! 现在数据库里的真实数据已经是1500了。 | |
T5 | SELECT balance FROM accounts WHERE id = 1; → 1500 | 关键点来了! 事务A第二次查询。因为它处于“读已提交”级别,它会读取最新已提交的数据,所以看到1500。 | |
T6 | COMMIT; | 事务A结束。 |
事务A的感受:
“我在这个事务里,第一次查余额是1000元,过了一会儿再查,莫名其妙变成了1500元!这钱是谁给我打的?我的查询结果不可重复!”
这就是“不可重复读”。 读已提交级别允许这种现象发生。
可重复读 (Repeatable Read)
核心思想: 在同一个事务中,多次读取同一数据的结果是一致的。就像在事务开始的那一刻,给整个数据库拍了一张快照(Snapshot),之后在这个事务里所有的读操作都基于这个快照,看不到其他事务提交的新数据。
隔离级别设定:
SET TRANSACTION ISISOLATION LEVEL REPEATABLE READ;
时间 | 事务A | 事务B | 说明 |
---|---|---|---|
T1 | BEGIN; | 事务A开始。就在这一刻,数据库为事务A创建了一个“一致性视图”或“快照”。 | |
T2 | BEGIN; | 事务B开始。 | |
T3 | SELECT balance FROM accounts WHERE id = 1; → 1000 | 事务A第一次查询,从快照中读取数据,结果是1000。 | |
T4 | UPDATE accounts SET balance = 1500 WHERE id = 1; | COMMIT; 事务B再次将张三的余额增加500并提交了! 数据库真实数据变为1500。 | |
T5 | SELECT balance FROM accounts WHERE id = 1; → 1000 | 最关键的差别! 事务A第二次查询。它不会去读最新的数据,而是依然从它开始时的那个快照里读取数据。所以它看到的还是1000元! | |
T6 | COMMIT; | 事务A结束。当事务A提交后,这个快照就被释放了。 |
事务A的感受:
“太神奇了!不管外面世界如何变化,在我这个事务的生命周期内,我每次查张三的余额都是1000元,结果始终如一,可以重复读。”
这就是“可重复读”。 它解决了不可重复读的问题。
核心机制:MVCC (多版本并发控制)
这两种级别在现代数据库(如MySQL InnoDB, PostgreSQL)中,通常通过MVCC来实现。
-
读已提交:在每条SELECT语句执行的瞬间,创建一个快照。所以每次读都能看到最新已提交的数据。
-
可重复读:在事务开始(BEGIN)后的第一条SELECT语句执行时创建快照(有些数据库在BEGIN时就创建)。这个快照会贯穿整个事务的生命周期。
总结对比表格
特性 | 读已提交 (Read Committed) | 可重复读 (Repeatable Read) |
---|---|---|
核心保证 | 只读已提交的数据 | 同一事务内读取可重复 |
解决什么问题 | 脏读 (Dirty Read) | 脏读 + 不可重复读 (Non-Repeatable Read) |
快照创建时机 | 每条SELECT语句开始时 | 事务开始时(或第一条SELECT时) |
看到其他事务的提交 | 是,立即看到 | 否,整个事务期间都看不到 |
性能 高 | 略低于读已提交 | (需要维护更久的快照) |
常用场景 | 绝大多数业务场景,如网上论坛、新闻站 | 对数据一致性要求更高的场景,如银行账户查询、对账业务 |
简单记法:
读已提交:你提交了,我就能看见。(结果可能变)
可重复读:不管你有没有提交,我自始至终只看我开始时的样子。(结果不变)