0%

MySQL事务与隔离级别

前言

  1. 事务的特性
  2. MySQL隔离级别

事务

进行数据库的操作离不开事务,事务也是各大厂招聘的必问环节。

事务的特性

ACID是事务的基本特性,也是事务这个概念成立的基础。

  • Atomic(原子性):表示一个事务中的所有操作,要么全部成功,要么全部失败回滚。
  • Consistance(一致性):事务执行前后,数据库的数据要保持一致,逻辑上的一致。(这个实现必须要依赖于其他三个特性)
    • 只有满足一致性,事务才有意义,事务的提交才是正确的。
  • Isolation(隔离性):并发情况下,多用户开启的事务相互之间不能受影响。
  • Duration(持久性):事务的提交是持久性的,会永久改变数据库。

原子性、一致性和持久性比较容易理解,主要是隔离性的满足需要深入探讨。

隔离性主要是为了满足高并发情况下事务的隔离问题,因此先明确一下常见的隔离问题。

事务的隔离问题

  1. 脏读:一个事务读到了另一个事务未提交的内容。
  2. 不可重复读:A事务中,多次读同一条数据,读到的数据不一致(可能是B事务提交了对这条数据的修改)。
  3. 幻读:A事务的两次读取中,读到了未出现过的数据行(可能是B事务插入了这条数据)。
  4. 丢失修改:A事务和B事务同时对一条数据进行了修改,B事务的提交覆盖了A事务的提交。

在事务的隔离问题中,其中丢失修改是“写”的问题,而其余是“读”的问题。

事务的隔离级别不是用来解决“写”的问题而存在的,也因此我们将“丢失修改”的问题单独拿出来说。

解决丢失修改的问题

两种策略:

  • 悲观策略:事务A对于某项数据进行修改时,在数据上加上排它锁。
  • 乐观策略:数据表上加上version字段,每次修改数据都要检验一下version是否正确,不正确就回滚。

MVCC

在讲事务的隔离级别时,MVCC的概念必须要了解。MVCC是Innodb引擎实现隔离级别的一种具体方式,MVCC通俗点讲就是:给数据一个快照。这与CopyOnWrite的思想类似。这里有三个概念:

  • 一个事务只能读取已经提交的快照
  • 当前事务能够读到自身未提交的快照
  • 数据的读不更新快照,数据的写更新快照

基本概念

使用MVCC之后,所有的select操作都不会加锁,而是用快照读的方式。

版本号:快照是基于版本号的

  • 系统版本号sys_id:每开启一个事务,版本号都会递增
  • 事务版本号trx_id:事务开启前的当前版本号

Undo日志

每一条数据行都有三个隐藏的字段:隐藏id(主键)、trx_id、roll_pt。其中roll_pt是回滚指针,用来指向undo日志。在事务提交前,undo日志会记录数据行一开始的状态;提交后,undo日志不会立刻删除,而是会放在删除区等待删除。

举例:行更新的过程

  1. 初始数据行
字段1 字段2 字段3 字段4 字段5 字段6 主键id trx_id rool_p
122 2 3 4 5 6 01 00 Null

2.事务1修改了这行的所有内容,首先会给这行数据加写锁:

字段1 字段2 字段3 字段4 字段5 主键id trx_id roll_pt
10 20 30 40 50 01 01 1

生成一个undo日志1,该数据行的roll_pt会指向undo日志1的主键id,而undo日志保留了初始数据行的状态:

undo日志1:

字段1 字段2 字段3 字段4 字段5 主键id trx_id roll_pt
1 2 3 4 5 01 00 Null

3.事务一提交,释放写锁,事务二进行了修改:

字段1 字段2 字段3 字段4 字段5 主键id trx_id roll_pt
11 21 31 41 51 01 02 2

此时会生成一个undo日志2保存事务二修改前的状态,该数据行的roll_pt指向undo日志2的主键id上:

undo日志2

字段1 字段2 字段3 字段4 字段5 主键id trx_id roll_pt
10 20 30 40 50 01 01 1

由于undo日志1的值不会立即被删除,因此undo日志2的roll_pt会指向undo日志1的主键id上,形成一条版本链。

ReadView

MVCC中进行快照读的基准是ReadView。执行select操作,可能会创建一个ReadView(依赖于隔离级别),ReadView保存着系统中所有未提交事务的trx_id

TRX_ID
TRX_ID_MIN TRX_ID_MAX
  • TRX_ID_MIN:表示在生成readview时当前系统中活跃的读写事务中最小的事务id
  • TRX_ID_MAX:表示生成readview时系统中应该分配给下一个事务的id值(一般情况下是当前事务id + 1,高并发情况下可能更大)

当用户想要读取某条数据行时,innodb会将这个行的版本号与readview中的版本号进行比较:

  • trx_id < TRX_ID_MIN:当前事务开启时,生成这个版本记录的事务已经提交了,所以一定可以读取
  • trx_id > TRX_ID_MAX:生成这个版本的事务是本次事务开启readview之后才开启的,对当前事务不可见,不可读取数据(这种情况相爱trx_id的数据一定是无法提交成功的,因为我select的时候会加读锁,之后的事务的修改想要提交成功必须要等我释放锁)
  • TRX_ID_MIN < trx_id < TRX_ID_MAX:如果trx_id不在readview里,表示生成该版本号的事务已经提交, 可以读取数据;否则,说明未提交,这就不可以读了。

补充:如果出现不可读的情况,将会沿着undo日志通过版本链查找最新可用的版本号进行读取。

事务的隔离级别

读未提交(Read Uncommited)

顾名思义,可以读到其他事务未提交的数据。这种隔离级别是不加锁的,效率最高,也不用MVCC。

读已提交(Read Commited)

事务提交后,可以被其他事务读取。但是事务B在两次读取的时间间隔内,事务A提交了修改,则事务B两次读取的数据就会不一样,这就是”不可重复读“的问题。

实现原理

事务A在修改数据的时候加写锁,直到事务结束。

此时这种解决方法会遇到一个问题:其他事务想要读数据时,会等待锁的释放。

因此Innodb引擎使用了MVCC来解决读写的一个并发情况。在这种隔离级别下:每次select都会生成一个readview,从而保证每次读取都只能读到已经提交的数据。但是,因为每次select都会生成一个新的readview,因此在这个事务执行期间,可以生成多个不同的readview从而读取到不一致的数据。无法解决不可重复读的情况。

可重复读(Repeatable Read):MySQL默认

事务在执行期间,不会读取到其他事务对数据的修改。但是其他事务插入的数据依然会被update等操作捕捉到。

实现原理

事务开启后的第一次select操作生成了一次readview,后续的所有select都是读的第一次生成的快照。

但是所有updatedelete操作都是当前读,所谓当前读就是读取到最新已提交版本号的数据,所以当前读不走快照的。因此,当事务A在执行update或者delete操作时,会读取到其他事务提交的数据,这些新数据和之间select的数据不一致,间接产生了幻读问题。

由此可见,MVCC可以解决由select操作引起的直接幻读,但是不能解决修改操作引起的间接幻读。

业务场景:

快手公司准备给次日24:00之间注册的用户发放1元红包,这时开启事务后会执行update操作,但是24:00之后依然会有大量新用户注册入库,此时update会同时修改这些新用户的红包信息(错误发钱),这就产生了幻读问题,并给公司带来业务损失。

如何解决幻读?

Innodb其实在RR隔离级别下实现了幻读的解决:使用了Next-Key Locks

当开启一个select的操作时,会锁住索引前后的间隔(左开右闭,行锁),这时其他事务进行insert操作会阻塞。

假设一个数据表如下:

Id a
1 5
2 8
3 11
4 18

此时Next-Key Locks对于索引a的锁的间隔是:$(-oo, 5], (5, 8], (8, 11], (11, 18], (18, +oo]$

如果此时我们执行:select * from table_name where a = 8

此时会锁住:$ (5, 8] U (8, 11] = (5, 11]$,此时其他事务无法这个范围内的行进行任何操作。

可串行化

所有事务串行处理,效率很低,但是足够安全。