一.背景
通过上节分析锁的基本概念,我们知道锁的模式有:
行级别的:(排他行锁)X、(共享行锁)S。
在加行锁之前需要先对库表页加意向锁:(意向排他锁)IX、(意向共享锁)IS。
以及实现锁的三种算法:
1>.record lock:锁住当前记录
2>.gap lock:锁住记录中间
3>.next-key lock:record lock+gap lock
这一节我们详细的分析这些锁的使用场景。
二.整体使用场景
1.普通的select操作(一致性非锁定读)不加任何锁(Serializable隔离级别加锁)。
2.UPDATE、DELETE和INSERT以及SELECT ... FOR UPDATE语句加IX和X锁。
3.SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE、
唯一key Duplicate后当前事务会对该key加S锁。
create table new_table select * from old_talbe; 会对old_talbe加S锁
三.详细的分析加锁情况
注:不同的隔离级别解决的问题不同,比如读已提交解决脏读,重复读解决幻读,所以不同的隔离级别加锁机制不同(这点很重要,很多人上来看sql就说加什么锁,这种很不负责任)。
下图是一张整体的加锁这里只讨论Read Commited和Repeatable Read,其他两种基本不会用讨论意义不大。
分析加锁,首先要知道下面几点:(不同的条件加锁机制也不相同)
1>.隔离级别
2>.索引类型和执行计划
3>.更新的字段中是否有索引字段
先贴一下表结构
1>.场景一:Read Commited+无索引
设置隔离级别:set session transaction isolation level read uncommitted;
不走索引语句:update t_user set name ='mbj111' where text='text1';
图1:事务1模拟使用无索引字段更新未提交
图2:事务2模拟使用无索引字段更新
图3:确认下隔离级别:是Read Commited
图4:冲突锁展示,在主键索引加的X锁,id都是1。注:表字段解释见上篇。
结论:
Read Commited+无索引 对满足条件的记录对应的主键索引加X锁。
值得注意的是:此场景因为是全表扫描,实际处理过程较复杂如下:
1>.mysql server向innodb发起一笔当前读(一致性锁定读):select * from t_user where text='text1'
2>.innodb收到该当前读请求发现全表扫描,会对所有主键索引的每一个节点加X锁并返回。
3>.mysql server收到结果集过滤,对不满足条件的记录解锁。
4>.mysql server对满足条件的记录发起更新的操作。
5>.innodb收到请求执行更新操作。
6>.mysql server发起commit命令给innodb
7>.innodb释放锁。
(更具体的流程后面的章节会详细的说)
从上面简易的流程可以看出,没有索引innodb对所有数据会执行加锁解锁的操作,这是由于MySQL的实现决定的如果一个条件无法通过索引快速过滤,那么存储引擎层面就会将所有记录加锁后返回,然后由MySQL Server层进行过滤。
在实际的实现中,MySQL有一些优化,在MySQL Server过滤条件,发现不满足后,会调用unlock_row方法,把不满足条件的记录放锁 (违背了2PL的约束,两阶段锁(Two-phase locking)约定加锁和解锁都在同一阶段执行,来保证加锁和解锁无交叉,在同一个事务中不会出现死锁)。这样做,保证了最后只会持有满足条件记录上的锁,但是每条记录的加锁操作还是不能省略的,并发下会影响性能。
2>.场景二:Read Commited+普通索引
普通索引语句:update t_user set name ='mbj123' where name='mbj111';
图1:数据展示
图2:执行计划展示
图3:RC隔离级别+模拟事务1根据普通索引更新未提交
图4:RC隔离级别+模拟事务2根据普通索引更新未提交
图5:冲突锁展示,可以看出对普通索引加了锁。
图6:事务2模式使用主键更新。
图7:冲突锁展示,可以看出对主键索引加了锁。
图8:模拟使用唯一索引更新(注意unique_no是字符型,值不带''不走索引)
图9:冲突锁展示,可以看出使用唯一索引更新依然对主键索引加了锁。
图10:模拟不使用索引。
图11:冲突锁展示,不使用索引意味着对主键加锁,引起主键锁冲突。
结论:RC+普通索引,会对所有满足条件的记录的普通索引加锁,并且会对对应的主键索引加锁。而对其他索引不加锁。
3>.场景三:Read Commited+唯一索引
图1:模拟使用唯一索引未提交
图2:模拟事务2使用唯一索引更新
图3:冲突锁展示,可以看出对唯一索引加锁
图4:事务2模拟主键更新
图5:冲突锁展示,可以看出引起主键锁冲突。
图6:事务2模拟使用非索引更新
图7:冲突锁展示:可以看出主键锁冲突了
图8:普通索引更新
图9:冲突锁展示:可以看出主键锁冲突了,因为普通所以会对主键加锁,而当前主键被事务1已经上锁。
结论:Read Commited+唯一索引:会对唯一索引和主键加锁,其他不加锁。
4>.场景四:Read Commited+主键索引
图1:事务1模拟主键事务未提交
图2:事务2模拟通过主键修改
图3:冲突锁展示:主键X锁冲突了
结论:Read Commited+主键索引,则对满足条件的主键索引加X锁。此场景性能最高。
5>.场景五:Repeatable Read+无索引
图1:事务1模拟无索引事务更新未提交
图2:事务2模拟无索引事务更新
图3:隔离级别展示,可以看出是可重复读
图4:锁展示,看出对主键加X锁了。
图5:事务2模拟更新其他行
图6:锁展示:等待id=1的主键X锁。
图7:事务2模拟执行插入
图8:锁展示:等待id=1的主键X锁。
结论:Repeatable Read+无索引,会对所有数据的主键加X锁,并且在记录的缝隙之间加GAP锁防止新记录插入来解决幻读。
也可以通过开启innodb_locks_unsafe_for_binlog来关闭GAP锁实现与RC隔离级别的处理方式。
mysql默认使用Repeatable Read隔离级别关闭innodb_locks_unsafe_for_binlog。
6>.场景六:Repeatable Read+普通索引
图0:看下目前的数据
图1:事务1模拟根据普通索引修改未提交
图2:事务2根据普通索引修改同一行
图3:锁展示:锁定了普通索引。
图4:事务2根据id修改
图5:锁展示,锁定了主键
图6:事务2插入name=mbj14向(mbj12,mbj15]之间插入,被阻塞
图7:锁展示,可以看出等待锁模式为X+GAP。在普通索引上。
结论:对满足条件的记录加X锁+GAP锁,对应的主键加X锁。防止幻读。
注:上图的数据可以看出,普通索引mbj15上加X锁,在(mbj12,mbj15)和(mbj15,mbj18)加GAP锁,之间不会被插入数据。注意GAP锁是加在普通索引之间的不是唯一索引。
7>.场景七:Repeatable Read+唯一索引
图1:事务1根据唯一索引修改,未提交
图2:事务2根据唯一索引,修改同一行
图3:锁展示,对唯一索引加了X锁
图4:事务2根据其他字段修改,被阻塞
图5.锁展示,主键X锁。
图6.事务2插入name=mbj14向(mbj12,mbj15]之间插入,成功。
结论:Repeatable Read+唯一索引,同场景三,对满足条件的唯一索引和对应的主键索引+X锁。此时没有GAP锁,因为唯一性索引next key锁会降级为X锁。
8>.场景八:Repeatable Read+主键索引
图1:事务1模拟根据普通索引更新未提交
图2:更新其他行字段,发现更新成功,未阻塞。
图3:事务2模拟修改相同行,被阻塞
图4:锁冲突展示,id=15的主键索引加了X锁
图5:事务2模拟插入id=16的值,成功
结论:Repeatable Read+主键索引,同场景四,仅对主键加X锁。
场景九:update字段中含索引
图1:事务1模拟修改索引字段name,未提交。
图2:事务2根据name修改,对name索引上X锁。被阻塞
图3:锁展示,发现name索引冲突了。说明事务1正在占用。
结论:所有场景加锁的基础上,对索引字段修改会对该索引加锁。
场景十:S锁的使用场景之LOCK IN SHARE MODE
图1:事务1使用中
图2:select * from t_user LOCK IN SHARE MODE加S锁
图3:锁展示,可以看出加了S锁,与X锁冲突。
结论:S锁与X锁冲突。具体兼容性如下:
场景十一:S锁的使用场景之Duplicate entry for key
图1:事务1发生Duplicate key
图2:事务2等待事务1
图3:锁冲突展示
结论:发生Duplicate key的事物会对该key持有S锁,直到回滚释放。此时对该索引加X锁是排斥的。
四.总结
整理以上结论如下图, 后面章节会根据锁机制去编写适合高并发的SQL。