数据库的事务及隔离级别

在操作数据库时,经常需要将一系列操作打包成一个原子操作:要么全部执行,要么全部不执行。一个最常被用来举例子的场景是银行转账问题,假设有两个用户 A 和 B,A 要向 B 转账 100 元需要进行以下操作:

这时候若第二部执行成功,但第三部失败了,会导致 A 平白无故损失 100 元,引发不一致的情况。因此需要进行特殊保护。

很多数据库提供了事务的概念。所谓「事务」,就是发送给数据库的一系列操作的集合。这些操作需满足 ACID 特性,即:

以最常见的 MySQL 为例,假设有两张表:

用户表 user
id uid uname
1 1 A
2 2 B
3 3 C
余额表 money
id uid money
1 1 500
2 2 200
3 3 100

则对应的事务语句为:

BEGIN;
select money from money,user where money.uid = user.uid and uname = 'A';
# suppose Amoney greater than 100
update money set money = Amoney-100 where uid = (select uid from user where uname = 'A');
select money from money,user where money.uid = user.uid and uname = 'B';
update money set money = Bmoney+100 where uid = (select uid from user where uname = 'B');
COMMIT;

事务的隔离级别

事务解决了原子性的问题。但如果用户 A 同时给 B 和 C 分别转了 100 元,来看看会有什么问题:

事务1 事务2
BEGIN BEGIN
select A: money==500
select A: money==500
update A: money==400
update A: money==400
select B: money==200
select C: money==100
update B: money==300
update C: money=200
COMMIT
COMMIT

最后的结果是,B 和 C 的账户分别增加了 100 元,但 A 的账户只减少了 100 元,而不是 200!原因在于事务 1 写回 A 的余额之前,事务 2 已经读取了 A 的余额,导致事务 2 的 update 语句覆盖了事务 1 update 的结果。

因此,人们规定了四种隔离级别来在不同层次上防止这些情况:

未提交读(read uncommitted)

未提交读其实就是上面写到的这种情况:事务 2 读到了被事务 1 修改的数据,但事务 1 没有 COMMIT,而是 ROLLBACK,导致「脏读」。这是一种安全性最低的隔离级别,一般很少有这种实现。

已提交读(read committed)

已提交读可以预防「脏读」,其原理就是一个事务无法读到被另一个事务修改但没有提交的数据。如下:

事务1 事务2
BEGIN BEGIN
select A: money==500
update A: money==400
select B: money==200
update B: money==300
select A: money==500
COMMIT
select A: money==400
update A: money==300
select C: money==100
update C: monty==200
COMMIT

已提交读是大多数数据库默认的隔离级别(MySQL InnoDB 引擎除外),它可以解决「脏读」的问题,但也带来了「不可重复读」的问题:即两次对 A 的余额读取的结果不一样。

可重复读(repeatable read)

MySQL 的 InnoDB 引擎默认设置为此级别。其保证了数据在一个事务连续的两次读取中结果相同:

事务1 事务2
BEGIN BEGIN
select A: money==500
update A: money==400
select B: money==200
update B: money==300
delete A: A is nil
select A: money==500
COMMIT
select A: money==500
update A: Error! A is not exists!

事务 2 在事务 1 COMMIT 前后去查询用户 A 的余额,都明确告知存在,且值为 500,但当事务 2 尝试更新用户 A 的余额时,会报错:用户 A 不存在!这就出现了「幻读」。

可串行化(serializable)

可串行化是最高的隔离级别,当然也是性能最低的。但它保证了数据的完全一致性,不会再出现「脏读」、「不可重复读」、「幻读」等现象。总结为下表:

隔离级别 未提交读 已提交读 可重复读 可串行化
脏读
不可重复读
幻读