数据库事务
约 1673 字大约 6 分钟
2025-04-10
事务的四大特性
事务是必须满足4个条件(ACID):[A] 原子性 、[C] 一致性 、[I] 隔离性 、[D] 持久性 。
原子性:一个事务(
transaction
)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback
)到事务开始前的状态,就像这个事务从来没有执行过一样。一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(
Read uncommitted
)、读提交(read committed
)、可重复读(repeatable read
)和串行化(Serializable
)。持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
事务控制语句
使用 BEGIN
或 START TRANSACTION
显式地开启一个事务
COMMIT
: 用于提交事务,将所有的修改永久保存到数据库ROLLBACK
: 用于回滚事务,撤销自上次提交以来所做的所有更改SAVEPOINT
: 用于在事务中设置保存点,以便稍后能够回滚到该点
SAVEPOINT savepoint_name;
ROLLBACK TO SAVEPOINT
: 把事务回滚到标记点
ROLLBACK TO SAVEPOINT savepoint_name;
MySQL 事务例子:
-- 开始事务
START TRANSACTION;
-- 执行一些SQL语句
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;
-- 判断是否要提交还是回滚
IF (条件) THEN
COMMIT; -- 提交事务
ELSE
ROLLBACK; -- 回滚事务
END IF;
事务隔离级别
隔离级别 | 脏读(Dirty Read) | 不可重复读 (Non Repeatable Read) | 幻读(Phantom Read) |
---|---|---|---|
读未提交 | ✔️ | ✔️ | ✔️ |
读提交 | ❌ | ✔️ | ✔️ |
可重复读 | ❌ | ❌ | ✔️ |
串行化 | ❌ | ❌ | ❌ |
提示
在MySQL中,如果使用InnoDB,默认的隔离级别是Repeatable Read
➡️ Read Uncommitted
Read Uncommitted
是隔离级别最低的一种事务级别。在这种隔离级别下,一个事务会读到另一个事务更新后但未提交的数据,如果另一个事务回滚,那么当前事务读到的数据就是脏数据,这就是脏读(Dirty Read
)
时刻 | 事务A | 事务B |
---|---|---|
1 | SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; | SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; |
2 | BEGIN; | BEGIN; |
3 | UPDATE students SET name = 'Bob' WHERE id = 1; | |
4 | SELECT * FROM students WHERE id = 1; | |
5 | ROLLBACK; | |
6 | SELECT * FROM students WHERE id = 1; | |
7 | COMMIT; |
当事务A执行完第3步时,它更新了id=1的记录,但并未提交,而事务B在第4步读取到的数据就是未提交的数据。
随后,事务A在第5步进行了回滚,事务B再次读取id=1的记录,发现和上一次读取到的数据不一致,这就是脏读。
➡️ Read Committed
在Read Committed
隔离级别下,一个事务不会读到另一个事务还没有提交的数据,但可能会遇到不可重复读(Non Repeatable Read
)的问题。
不可重复读是指,在一个事务内,多次读同一数据,在这个事务还没有结束时,如果另一个事务恰好修改了这个数据,那么,在第一个事务中,两次读取的数据就可能不一致。
时刻 | 事务A | 事务B |
---|---|---|
1 | SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; | SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; |
2 | BEGIN; | BEGIN; |
3 | SELECT * FROM students WHERE id = 1; -- Alice | |
4 | UPDATE students SET name = 'Bob' WHERE id = 1; | |
5 | COMMIT; | |
6 | SELECT * FROM students WHERE id = 1;-- Bob | |
7 | COMMIT; |
当事务B第一次执行第3步的查询时,得到的结果是Alice,随后,由于事务A在第4步更新了这条记录并提交,
所以,事务B在第6步再次执行同样的查询时,得到的结果就变成了Bob,因此,在Read Committed
隔离级别下,事务不可重复读同一条记录,因为很可能读到的结果不一致。
➡️ Repeatable Read
在Repeatable Read
隔离级别下,一个事务可能会遇到幻读(Phantom Read
)的问题。
幻读是指,在一个事务中,第一次查询某条记录,发现没有,但是,当试图更新这条不存在的记录时,竟然能成功,并且,再次读取同一条记录,它就神奇地出现了
时刻 | 事务A | 事务B |
---|---|---|
1 | SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; | SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; |
2 | BEGIN; | BEGIN; |
3 | SELECT * FROM students WHERE id = 99; -- empty | |
4 | INSERT INTO students (id, name) VALUES (99, 'Bob'); | |
5 | COMMIT; | |
6 | SELECT * FROM students WHERE id = 99; -- empty | |
7 | UPDATE students SET name = 'Alice' WHERE id = 99; -- 1 row affected | |
8 | SELECT * FROM students WHERE id = 99; -- Alice | |
9 | COMMIT; |
看到这里纳闷了,怎么左边提交了 右边还是查不到
幻读的重点在于数据是否存在。原本不存在的数据却真实的存在了,这便是幻读。引起幻读的原因在于另一个事务进行了INSERT操作。
在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。因此,幻读在 当前读 下才会出现。
什么是快照读,什么是当前读?
快照读读取的是快照数据。不加锁的简单的 SELECT都属于快照读,比如:
SELECT * FROM player WHERE ...
当前读就是读取最新数据,而不是历史版本的数据。加锁的 SELECT,或者对数据进行增删改都会进行当前读。这有点像是 Java 中的 volatile 关键字,被 volatile 修饰的变量,进行修改时,JVM 会强制将其写回内存,而不是放在 CPU 缓存中,进行读取时,JVM 会强制从内存读取,而不是放在 CPU 缓存中。这样就能保证其可见行,保证每次读取到的都是最新的值。如果没有用 volatile 关键字修饰,变量的值可能会被放在 CPU 缓存中,这就导致读取到的值可能是某次修改的值,不能保证是最新的值。
如下的操作都会进行 当前读:
SELECT * FROM player LOCK IN SHARE MODE;
SELECT * FROM player FOR UPDATE;
INSERT INTO player values ...
DELETE FROM player WHERE ...
UPDATE player SET ...