所在文集:数据库
本文的内容参考了:
本文会涉及到数据库的锁,请先参见:MySQL InnoDB 锁 学习笔记
数据库隔离级别
用于处理 多事务并发 的情况。
InnoDB 对四种类型都支持:
- READ_UNCOMMITTED:读取未提交。这是并发最高,一致性最差的隔离级别。
- READ_COMMITTED(RC):读提交。这是互联网最常用的隔离级别。
- REPEATABLE_READ(RR):重复读。
- SERIALIZABLE:串行化。这是一致性最好的,但并发性最差的隔离级别。
不同事务的隔离级别,实际上是一致性与并发性的一个权衡与折衷。
InnoDB 使用不同的锁策略来实现不同的隔离级别。
Read UnCommitted 读取未提交内容
InnoDB 实现方式:select
语句不加锁。
导致出现脏读:
A 事务读取了 B 事务 尚未提交 的数据并在此基础上操作,而 B 事务回滚,则 A 事务读到的数据为脏数据。例如:
A: start transaction
B: start transaction
B: update account set balance = 50 where id = 1
A: select balance from account where id = 1
B: rollback
A: update account set balance = 50 + 100 where id = 1
A: commit
- B 事务读取账户余额 100 块,取钱,将余额修改为 50 块,但并没有提交。
- A 事务读取了 尚未提交 的数据,认为余额是 50 块。
- B 事务回滚。
- A 事务存钱 100 块,将余额计算为 50 + 100 = 150 块,此为脏数据,实际余额应该为 200 块。
Read Committed(RC)读取提交内容
- 这是大多数数据库系统的默认隔离级别(但不是 MySQL 默认的)
- 它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变
InnoDB 实现方式:
- 普通的
select
使用快照读(snapshot read),这是一种不加锁的一致性读。 - 加锁的
select
,update
,delete
等语句,除了在外键约束检查以及重复键检查时会封锁区间,其他时刻都只使用记录锁;
导致出现不可重复读:
意味着我们在同一个事务中执行完全相同的 select
语句时可能看到不一样的结果。
A 事务重复读取前面读取过的数据,发现数据变化了,即被其他事务 B 修改并提交过了。例如:
A: start transaction
B: start transaction
A: select balance from account where id = 1
B: update account set balance = 50 where id = 1
B: commit
A: select balance from account where id = 1
A: commit
- A 事务第一次读取余额 100 块。
- B 事务读取账户余额 100 块,取钱,将余额修改为 50 块,并提交。
- A 事务第二次读取余额,变成了 50 块。与前一次读取的结果不同。
Repeatable Read(RR)可重读
- 这是 MySQL InnoDB 的默认事务隔离级别
- 它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行
- 此级别可能出现的问题——幻读(Phantom Read):当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行,例如:
A: start transaction
B: start transaction
A: select * from account // 只有id = 1
B: insert into account(id, balance) values(2, 0)
B: commit
A: select * from account // 发现有id = 1 和 id = 2
A: commit
- 很多人容易搞混不可重复读和幻读,确实这两者有些相似。但不可重复读重点在于
update
和delete
,而幻读的重点在于insert
。 - InnoDB 存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题
InnoDB 实现方式:
- 普通的
select
使用快照读(snapshot read),这是一种不加锁的一致性读。 - 加锁的
select
,update
,delete
等语句,它们的锁,依赖于它们是否在唯一索引上使用了唯一的查询条件,或者范围查询条件:- 在唯一索引上使用唯一的查询条件,会使用记录锁(record lock),而不会封锁记录之间的间隔,即不会使用间隙锁(gap lock)与临键锁(next-key lock)。
- 范围查询条件,会使用间隙锁与临键锁,锁住索引记录之间的范围,避免范围间插入记录,以避免产生幻影行记录,以及避免不可重复的读。
Serializable 可串行化
- 这是最高的隔离级别
- 它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。
- 在这个级别,可能导致大量的超时现象和锁竞争
InnoDB 实现方式:这种事务的隔离级别下,所有 select
语句都会被隐式的转化为 select ... in share mode
.
总结
InnoDB 实现了SQL92标准中的四种隔离级别
- 读未提交:
select
不加锁,可能出现读脏; - 读提交(RC):普通
select
快照读,带锁的select
/update
/delete
会使用记录锁,可能出现不可重复读; - 可重复读(RR):普通
select
快照读,带锁的select
/update
/delete
根据查询条件情况,会选择记录锁,或者间隙锁/临键锁,以防止读取到幻影记录; - 串行化:
select
隐式转化为select ... in share mode
,会被update
与delete
互斥; - InnoDB默认的隔离级别是RR,用得最多的隔离级别是RC
InnoDB,快照读,在RR和RC下有何差异?
MySQL 数据库,InnoDB 存储引擎,为了提高并发,使用 MVCC 机制,在并发事务时,通过读取数据行的历史数据版本,不加锁,来提高并发的一种不加锁一致性读(Consistent Nonlocking Read)。
- 事务总能够读取到,自己写入(
update
/insert
/delete
)的行记录 - 在 Read Committed(RC)下,快照读总是能读到最新的行数据快照,当然,必须是已提交事务写入的
- 在 Repeatable Read(RR)下,假设某个事务首次 read 记录的时间为 T,则未来不会读取到 T 时间之后已提交事务写入的记录,以保证连续相同的 read 读到相同的结果集
例如:数据表中的数据为 {1, 2, 3}
,现在两个并发事务 A,B 执行的时间序列如下:
A1: start transaction;
B1: start transaction;
A2: select * from t;
B2: insert into t values (4, wangwu);
A3: select * from t;
B3: commit;
A4: select * from t;
在 Read Committed(RC) 模式下:
- A2 读到的结果集是
{1, 2, 3}
; - A3 读到的结果集也是
{1, 2, 3}
,因为 B 还没有提交; - A4 读到的结果集还是
{1, 2, 3, 4}
,因为事务 B 已经提交;
在 Repeatable Read(RR) 模式下:
- A2 读到的结果集肯定是
{1, 2, 3}
,这是事务A的第一个 read,假设为时间T; - A3 读到的结果集也是
{1, 2, 3}
,因为 B 还没有提交; - A4 读到的结果集还是
{1, 2, 3}
,因为事务 B 是在时间 T 之后提交的,A4 得读到和 A2 一样的记录;