MySQL 数据库隔离级别 脏读 VS 不可重读读 VS 幻读 VS 可串行化

所在文集:数据库


本文的内容参考了:

  • [Mysql]——通过例子理解事务的4种隔离级别
  • 架构师之路 - 4种事务的隔离级别,InnoDB如何巧妙实现?

本文会涉及到数据库的锁,请先参见: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),这是一种不加锁的一致性读。
  • 加锁的 selectupdatedelete 等语句,除了在外键约束检查以及重复键检查时会封锁区间,其他时刻都只使用记录锁;

导致出现不可重复读:
意味着我们在同一个事务中执行完全相同的 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
  • 很多人容易搞混不可重复读和幻读,确实这两者有些相似。但不可重复读重点在于 updatedelete,而幻读的重点在于 insert
  • InnoDB 存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题

InnoDB 实现方式:

  • 普通的 select 使用快照读(snapshot read),这是一种不加锁的一致性读。
  • 加锁的 selectupdatedelete 等语句,它们的锁,依赖于它们是否在唯一索引上使用了唯一的查询条件,或者范围查询条件:
    • 在唯一索引上使用唯一的查询条件,会使用记录锁(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,会被 updatedelete 互斥;
  • InnoDB默认的隔离级别是RR,用得最多的隔离级别是RC

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 一样的记录;

你可能感兴趣的:(MySQL 数据库隔离级别 脏读 VS 不可重读读 VS 幻读 VS 可串行化)