本文将以 Mysql 为例,探讨数据库的事务隔离。

总览

  • 未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据
  • 提交读(Read Committed):只能读取到已经提交的数据。Oracle 等多数数据库默认都是该级别 (不重复读)
  • 可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB 默认级别。在 SQL 标准中,该隔离级别消除了不可重复读,但是还存在幻象读
  • 串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞

未提交读和串行读,一般很少用,在这里不做讨论。

提交读

提交读对读操作不加锁,但对插入、修改、删除等操作加行锁。

设置 session 隔离级别为 RC。

SET session transaction isolation level read committed;

加锁过程分两种情况讨论:
有索引:直接锁住索引命中的行;没有索引,mysql 首先默认锁住全表,但在过滤完筛选条件后,不满足条件的行会被释放锁(虽然这违背了二段锁的协议),从而保证高性能和高效率。

重复读

可重复读,顾名思义,在同一事务里,不同阶段的读取结果是一样的。

不可重复读和幻读

不同点在于锁的机制。

不可重复读:提交读对读操作不加锁,所以同一事务中不同阶段的两次读取结果可能不一致。
幻读:重复读对读操作添加行锁,保证对应行的 update、delete 操作无法进行,但却没有对 insert 操作限制,导致出现幻读。

以上讨论的,是以悲观锁机制来处理问题,在 mysql 和其它成熟的数据库中,出于性能考虑,都是使用以乐观锁为理论基础的 MVVC(多版本并发控制)来避免这两个问题。

悲观锁和乐观锁

悲观锁:指的是对于数据被外界的修改持有悲观的态度(总是假设数据可能被修改),因此,由数据库自身提供锁机制,保证悲观锁的实现。
乐观锁:大多基于数据版本记录机制实现的。(在表中增加 version 字段,读取数据时,读出版本号,更新时,版本号 +1,若版本号大于数据库中的版本号时,允许更新,否则认为是过期数据。

MVVC 在 InnoDB 中的实现

0 每个表加上 2 个字段,一个存储创建版本号,一个存储过期版本号(或已删除);
1 在每个事务开启的时候版本号 +1;

  • SELECT:当前版本号 >= 创建版本号,当前版本号 < 过期版本号(或过期版本号为空);
  • UPDATE:插入新的记录,原本记录的过期版本号为当前版本号,新的记录的创建版本号为当前版本号;
  • DELETE:当前版本号为过期版本号;
  • INSERT:当前版本号为创建版本号;

幻读的问题解决了,但还有并发写的问题。

“读”与”读””

数据库的读:快照读(snapshot read)select
事务隔离的读:当前读(current read)update insert delete

快照读,不加锁,当前读,加锁(Next-key 锁)

Next-key 锁

Next-key 锁 = 行锁 + Gap 锁

Gap Lock

假设有 id 为 5,30 两条数据,更新修改 30
有索引:加 30 行锁,同时给(5-30)(30,+∞)加 Gap 锁。
没索引:全表 Gap 锁

假设有 id 为 5,30 两条数据,事务 A 修改 20,事务 B 增加 10,id 为普通索引
事务 A:加(5,30)Gap 锁
事务 B:等待 A 完成

事务 A:解锁,完成
事务 B:插入数据,完成

由上可以看出,即使没有修改,数据库也会给数据加 Gap 锁,从而影响其它事务任务的执行。

假设有 id 为 5,30 两条数据,事务 A 修改 20,事务 B 增加 10,事务 C 增加 40,id 为普通索引
事务 A:加(5,30)Gap 锁
事务 B:等待 A 完成
事务 C:插入 40,完成

事务 A:解锁,完成
事务 B:插入数据,完成

由上面可以看出,Gap 影响范围以外的行不受影响


Database   Mysql      DB

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

上一篇
Day 0 下一篇