事务的隔离级别及实现

发布时间 2023-06-14 10:13:46作者: 上好佳28

事物的隔离级别

1.Read Uncommitted 读未提交,存在脏读,不可重复读看,幻读等问题

2.Read Committed 读已提交,存在不可重复读看,幻读等问题

3.Repeatable Read 可重复读,存在幻读

4.Serializable 串行,脏读,不可重复读看,幻读均可避免

MVCC

MVCC(多版本控制)只在 Read Committed 和 Repeatable Read两个隔离级别下工作。其他两个隔离级别和MVCC不兼容,

Read Uncommitted下总是读取最新的记录行,而不是符合当前事务版本的记录行;
Serializable下则会对所有读取的记录行都加锁

MySQL的InnoDB存储引擎默认事务隔离级别是RR(可重复读),是通过 "行级锁+MVCC"一起实现的,正常读的时候不加锁,写的时候加锁

而 MCVV 的实现依赖于:隐藏字段、Read View、Undo log。

隐藏字段

1、DB_TRX_ID(6字节):表示最近一次对本记录行作修改(insert | update)的事务ID

2、 DB_ROLL_PTR(7字节):回滚指针,指向当前记录行的undo log信息

3、DB_ROW_ID(6字节):随着新行插入而单调递增的行ID。(理解:当表没有主键或唯一非空索引时,innodb就会使用这个行ID自动产生聚簇索引。如果表有主键或唯一非空索引,聚簇索引就不会包含这个行ID了。这个DB_ROW_ID跟MVCC关系不大。)

Read View(读视图)

其实Read View跟快照、snapshot是一个概念。 Read View主要是用来做可见性判断的, 里面保存了“对本事务不可见的其他活跃事务”。

Read View 结构源码,其中包括几个变量

① low_limit_id:目前出现过的最大的事务ID+1,即下一个将被分配的事务ID

② up_limit_id:活跃事务列表trx_ids中最小的事务ID,如果trx_ids为空,则up_limit_id 为 low_limit_id。

③ trx_ids:Read View创建时其他未提交的活跃事务ID列表。

④ creator_trx_id:当前创建事务的ID,是一个递增的编号

Undo log

Undo log中存储的是老版本数据,当一个事务需要读取记录行时,如果当前记录行不可见,可以顺着undo log链找到满足其可见性条件的记录行版本。

①insert undo log : 事务对insert新记录时产生的undo log, 只在事务回滚时需要, 在事务提交后就可以立即丢弃。

②update undo log : 事务对记录进行delete和update操作时产生的undo log,不仅在事务回滚时需要,快照读也需要,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被purge线程删除。

Purge线程:为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下旧记录的deleted_bit,并不真正将旧记录删除。 为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。purge线程自己也维护了一个read view,如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的

 

具体实现

不同事务或者相同事务的对同一记录行的修改,会使该记录行的undo log成为一条链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录。

1、如果当前记录值的事务id小于活跃事务列表trx_ids中最小的事务ID,说明该记录已被提交,将该值返回

2、如果当前记录值的事务id大于当前查询事务的id,即当前记录值是在“当前事务”创建快照之后才修改该行,是不可见的,那么顺着版本链找到下一个旧一些的版本再次判断(即步骤4)

3、如果当前记录值的事务id大于活跃事务列表trx_ids中最小的事务ID并且小于当前查询事务的id,说明最新修改该行的事务在当前查询事务创建快照的时候可能处于“活动状态”或者“已提交状态”,就要对活跃事务列表trx_ids进行查找(二分,因为是有序的)

(1) 如果在活跃事务列表trx_ids中能找到 id 为 trx_id 的事务,表明还未没有提交,那么找下一版本

(2)在活跃事务列表中找不到,则表明“id为trx_id的事务”在修改“该记录行的值”后,在“当前事务”创建快照前就已经提交了,所以记录行对当前事务可见,直接返回

4、在该记录行的 DB_ROLL_PTR 指针所指向的undo log回滚段中,取出最新的的旧事务号DB_TRX_ID, 将它赋给trx_id,然后跳到步骤1重新开始判断

注意:只靠 MVCC 实现RR隔离级别时,可以保证可重复读并不能完全防止幻读,因此,InnoDB在实现RR隔离级别时,不仅使用了MVCC,还会对“当前读语句”读取的记录行加记录锁(record lock)和间隙锁(gap lock),禁止其他事务在间隙间插入记录行,来防止幻读,也就是前文说的"行级锁+MVCC"

共享锁、独占锁、意向锁

共享锁S LOCK 允许事务读一行数据(select…lock in share mode)

排他锁 X LOCK 允许事务删除或更新一行数据(select…for update)

意向共享锁 IS LOCK 事务想要获得一张表中某几行的共享锁

意向排他锁 IX LOCK 事务想要获得一张表中某几行的排他锁

(1)申请意向锁的动作是数据库完成的,就是说,事务A申请一行的行锁的时候,数据库会自动先开始申请表的意向锁,不需要我们程序员使用代码来申请。

(2)IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突**

行锁

两阶段锁协议

在InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放

如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放

假设你负责实现一个电影票在线交易业务,顾客A要在影院B购买电影票。我们简化一点,这个业务需要涉及到以下操作:

  1. 从顾客A账户余额中扣除电影票价;

  2. 给影院B的账户余额增加这张电影票价;

  3. 记录一条交易日志。

也就是说,要完成这个交易,我们需要update两条记录,并insert一条记录。当然,为了保证交易的原子性,我们要把这三个操作放在一个事务中。那么,你会怎样安排这三个语句在事务中的顺序呢?

试想如果同时有另外一个顾客C要在影院B买票,那么这两个事务冲突的部分就是语句2了。因为它们要更新同一个影院账户的余额,需要修改同一行数据。

根据两阶段锁协议,不论你怎样安排语句顺序,所有的操作需要的行锁都是在事务提交的时候才释放的。所以,如果你把语句2安排在最后,比如按照3、1、2这样的顺序,那么影院账户余额这一行的锁时间就最少。这就最大程度地减少了事务之间的锁等待,提升了并发度

行锁的三种算法

记录锁 Record Lock

单个记录上的锁,Record Lock总会锁定索引记录,如果InnoDB没有设置任何一个索引,那么InnoDB存储引擎会使用隐式的主键来进行锁定

间隙锁 Gap Lock

锁定一个范围,但不包含记录本身,开区间(5,10)

Next-Key Lock

Gap Lock + Record Lock,锁定一个范围,并且锁定记录本身 前开后闭区间 (5,10],(10,15]

快照读和当前读

快照读是通过MVVC(多版本控制)和undo log来实现的,当前读是通过加record lock(记录锁)和gap lock(间隙锁)来实现的

快照读,即一致性非锁定读

一致性非锁定读(consistent nonlocking read)是指InnoDB存储引擎通过多版本控制(MVCC)的方式来读取当前执行时间数据库中行的数据,如果读取的行正在执行DELETE或UPDATE操作,这是读取操作不会因此等待行上锁的释放。相反的,InnoDB会去读取行的一个快照数据

之所以称为非锁定读,因为不需要等待访问的行上X锁的释放

快照数据是指该行之前版本的数据,该实现是通过undo段来完成。而undo用来事务中的回滚数据,因此快照数据本身没有额外的开销,此外,读取快照数据不需要上锁,因为没有事务需要对历史数据进行修改操作

事务隔离级别RC(读已提交)和RR(可重复读)下,InnoDB存储引擎引擎使用非锁定的一致性读。然而,对于快照数据的定义却不相同。在RC事务隔离级别下,对于快照数据,非一致性读总是被锁定行的最新一份快照数据(即如果在事务期间其他事务修改了数据并提交后再读是读取到的最新的数据);而在RR事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。

当前读,即一致性锁定读

默认配置下,即事务的隔离级别为RR(可重复读)模式下,InnoDB存储引擎的SELECT操作使用一致性非锁定读。但在某些情况下,用户需要显式得对数据库读取操作加锁以保证数据逻辑的一致性。这要求数据库支持加锁语句。 InnoDB存储引擎对于SELECT语句支持两种一致性的锁定读操作:

SELECT … FOR UPDATE
SELECT … LOCK IN SHARE MODE

SELECT … FOR UPDATE对读取的行记录加一个X锁,其他事务不能对已经锁定的行加上任何锁。SELECT …LOCK IN SHARE MODE对读取的行记录加一个S锁,其他事务可以对被锁定的行加S锁,但是不能加X锁,否则会被阻塞。 而且上面这两种操作必须要在事务中,当事务提交了,锁也就释放了

总结

在mysql中,提供了两种事务隔离技术,第一个是mvcc,第二个是next-key技术。这个在使用不同的语句的时候可以动态选择。不加lock inshare mode之类的就使用mvcc。否则使用next-key。mvcc的优势是不加锁,并发性高。缺点是不是实时数据。next-key的优势是获取实时数据,