mysql insert 执行_Mysql insert语句执行原理

加锁流程一直很迷,尤其几个session搅在一起。自以为的初窥门径其实根本不值一提。今天碰巧搜到有大佬从源码层面分析insert加锁,就顺便记一下。

原文博客:https://www.aneasystone.com/archives/2018/06/insert-locks-via-mysql-source-code.html

附前提知识讲接链接:

锁的种类-Mysql官方文档

常见 SQL 语句的加锁分析

select, select for update, select lock in share mode详解

问题

隔离级别为RR,两个session分别用insert into T where id=xxx;和select ...lock in share mode;是否会引起幻读?

insert可以看作两步:1.获取意向锁,2.写数据,获取互斥锁。那么如果两步之间,另一个session使用了select,读到的数据会与在第二步之后读到的数据条目数不一致——幻读。

session1

session2

1.insert拿意向写锁成功

(1).如果在这个节骨眼上执行select,上意向读锁(IS),意向写锁(IX)与select的意向读锁(IS)不冲突,因此可以读取,读到的是insert前的东西

2.insert插入数据

3.insert完成后,id的聚簇索引对应条目上加行锁防止其他session读到

4.commit,释放行锁

(2).如果这时候执行select,由于insert所在的session执行了commit,行锁被释放,仍然可以读取,读到的是insert后的东西,与(1)数据条目不一致,幻读

探究

那么究竟有没有问题呢?还真没有。这里就要了解一下几样锁的实现了。

insert执行时的加锁

实际上,大佬看源码后发现,insert语句的底层实现中,insert语句如果没遇到冲突,那么什么锁都不会加。并没有所谓“意向锁”,以及insert之后的"行锁"。

那么我们之前对锁的理解岂不是出了好大的问题?也不是。我们理解中的insert加锁,实际上是通过“隐式锁”实现的。

隐式锁

隐式锁的关键在于转换。即:本来没有锁,其他session运行时检测到这个带着隐式锁的session也在操作同一条记录,那么会帮那个session把锁加上,然后自己乖乖排队等锁释放。

比如要运行一条需要加锁的select语句,这条语句会首先判断是否存在其他session活跃,然后检查活跃session是否已存在排他记录锁,如果session活跃且不存在锁,则为该session加上排他记录锁(行锁)。然后本session的锁是在之后调用 lock_rec_lock 函数来加的。

所以现在实际流程变成了这样:

执行 insert 语句,判断是否有和插入意向锁冲突的锁,如果有,加插入意向锁,进入锁等待;如果没有,直接写数据,不加任何锁;

执行 select ... lock in share mode 语句,判断记录上是否存在活跃的事务,如果存在,则为 insert 事务创建一个排他记录锁,并将自己加入到锁等待队列;

session1

session2

1.插入意向锁不与其它锁冲突(因为目前没有其它活跃session)

(1).执行select,根据前面的流程分析,虽然insert所在session处于活跃状态,但还没插入数据,因此不需要给insert-session加行级X锁,直接返回数据,过程中加上gap锁

但这里没有begin和commit,gap锁直接释放

2.啥锁也不加,直接insert插入数据

3.commit

(2).如果这时候执行select,仍然可以读取到insert后的东西,与(1)数据条目不一致,幻读

还是幻读?

不慌,分析一下幻读的原因。可以发现,由于意向锁的检测与实际insert数据之间存在时间差,因此select总能有机会趁机而入,造成幻读。

好在Mysql开发者大佬先我们一步考虑到这一点,于是引入了一项更轻量级的锁机制来保证insert这两步操作的原子性,即Latch

latch

Latch,一般也把它翻译成 “锁”,但它和我们之前接触的行锁表锁(Lock)是有区别的。这是一种轻量级的锁,锁定时间一般非常短,它是用来保证并发线程可以安全的操作临界资源,通常没有死锁检测机制。Latch 可以分为两种:MUTEX(互斥量)和 RW-LOCK(读写锁)。这里我们用到的是 RW-LOCK。

引入这个概念之后我们再回头看insert。之前所说insert过程中加“隐式锁”或者直白点儿不加锁,指的统统都是我们传统意义上的Lock, 而实际上,insert在插入数据前后,各有一步关于Latch的操作:mtr_start(&mtr)和mtr_commit(&mtr),即mini-transaction迷你事务的启动和提交。既然叫做事务,那这个函数的操作肯定是原子性的,事实上确实如此,insert 会在检查锁冲突和写数据之前,会对记录所在的页加一个 RW-X-LATCH 锁,执行完写数据之后再释放该锁(实际上写数据的操作就是写 redo log,将脏页加入 flush list,这个后面有时间再深入分析了)。这个锁的释放非常快,但是这个锁足以保证在插入数据的过程中其他事务无法访问记录所在的页。mini-transaction 也可以包含子事务,实际上在 insert 的执行过程中就会加多个 mini-transaction,这中间的过程可以参考这篇博客:大佬的另一篇博文

每个 mini-transaction 会遵守下面的几个规则:

修改一个页需要获得该页的 X-LATCH;

访问一个页需要获得该页的 S-LATCH 或 X-LATCH;

持有该页的 LATCH 直到修改或者访问该页的操作完成。

所以,最后的最后,真相只有一个:insert 和 select ... lock in share mode 不会发生幻读。整个流程如下:

执行 insert 语句,对要操作的页加 RW-X-LATCH,然后判断是否有和插入意向锁冲突的锁,如果有,加插入意向锁,进入锁等待;如果没有,直接写数据,不加任何锁,结束后释放 RW-X-LATCH;

执行 select ... lock in share mode 语句,对要操作的页加 RW-S-LATCH,如果页面上存在 RW-X-LATCH 会被阻塞,没有的话则判断记录上是否存在活跃的事务,如果存在,则为 insert 事务创建一个排他记录锁,并将自己加入到锁等待队列,最后也会释放 RW-S-LATCH;

再放个表格吧

session1

session2

1.insert加RW-X-LATCH成功

2.insert判断没有其它活跃session存在

(1-1).此时select执行,RW-S-LATCH获取时发现insert的RW-S-LATCH卡在那里,阻塞

3.insert插入数据

4.insert插入完成,释放RW-X-LATCH

(1-2).终于等到RW-S-LATCH了,结果发现insert所在session还没提交,只好帮它加上行级X锁然后自己默默回去排队

5.commit;

(1-3).获取到行级S锁了,读取数据

(2).由于(1)一直被阻塞到session1提交才能返回结果,因此如果这时候执行select,显然与(1)读取到的数据一致

呼,分析结束,再次鸣谢aneasystone大佬不嫌麻烦自己调代码帮助大家理解原理

你可能感兴趣的:(mysql,insert,执行)