
使用MVCC(多版本并发控制)属于一致性读,性能相比加锁更好,但是在一些情况下不得不加锁。
共享锁,Shared Lock,简称S锁。在事务读取一条记录时,需要先获得S锁。
独占锁,Exclusive Lock,简称X锁。在事务改动一条记录时,需要先获取该记录的X锁。
S锁和S锁之间是兼容的,S锁和X锁是不兼容的,X锁和X锁之间也是不兼容的。
在事务A获取S锁后,事务B还可以申请S锁,如果申请X锁会阻塞。
先申请X锁后,无法申请X锁和S锁。
SELECT ... LOCK IN SHARE MODE
:对读取到的记录加S锁SELECT ... FOR UPDATE
:对读取的记录加X锁锁的粒度比较粗的比如表级锁,影响的记录是全表,对于行级锁针对单个记录,称其锁粒度比较低,锁粒度低可以实现精准的控制。
flush tables with read lock
,之后整个数据库处于只读状态,其他线程操作会被阻塞吗,如对数据的增删改,对表结构的更改。unlock tables
,释放全局锁。如果数据库的引擎支持的事务支持可重复读的隔离级别,那么在备份数据库之前先开启事务,会先创建 Read View,然后整个事务执行期间都在用这个 Read View,而且由于 MVCC 的支持,备份期间业务依然可以对数据进行更新操作。
像MyISAM不支持事务的引擎,备份数据库时需要使用全局锁。
MySQL 里面表级别的锁有这几种:
lock tables t_student read;
lock tables t_stuent write;
unlock tables
,释放锁。表锁会限制所有线程的读写操作。尽量避免在使用 InnoDB 引擎的表使用表锁,因为表锁的颗粒度太大,会影响并发性能。
简称MDL,我们不需要显示的使用 MDL,因为当我们对数据库表进行操作时,会自动给这个表加上 MDL:
MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。
MDL 不需要显示调用,那它是在什么时候释放的?
MDL 是在事务提交后才会释放,这意味着事务执行期间,MDL 是一直持有的。
为什么线程 C 因为申请不到 MDL 写锁,而导致后续的申请读锁的查询操作也会被阻塞?
这是因为申请 MDL 锁的操作会形成一个队列,队列中写锁获取优先级高于读锁,一旦出现 MDL 写锁等待,会阻塞后续该表的所有 CRUD 操作。
所以为了能安全的对表结构进行变更,在对表结构变更前,先要看看数据库中的长事务,是否有事务已经对表加上了 MDL 读锁,如果可以考虑 kill 掉这个长事务,然后再做表结构的变更。
在InnoDB存储引擎当中,在对表SELECT、INSERT、DELETE、UPDATE时,不会为这个表添加表级别的S锁和X锁,在server层使用了MDL来防止表结构的变化。
由于S锁和X锁和是互斥的,对于整个表加上S锁和X锁前,需要检查整个表是否有记录存在锁,由此有了意向锁:在事务准备在某条记录上加上S锁或者X锁时,在表级别上增加意向锁。
意向共享锁和意向独占锁是表级锁,之间不会产生冲突,IX锁和X锁和S锁不兼容,IS锁只和X锁不兼容。
IX锁和IS锁是用来快速判断记录是否上锁的,避免用遍历的方式查看。
表里的主键通常都会设置成自增的,这是通过对主键字段声明 AUTO_INCREMENT
属性实现的。之后可以在插入数据时,可以不指定主键的值,数据库会自动给主键赋值递增的值,这主要是通过 AUTO-INC 锁实现的。
AUTO-INC 锁是特殊的表锁机制,锁不是在一个事务提交后才释放,而是再执行完插入语句后就会立即释放。
在插入数据时,会加一个表级别的 AUTO-INC 锁,然后为被
AUTO_INCREMENT
修饰的字段赋值递增的值,等插入语句执行完成后,才会把 AUTO-INC 锁释放掉。那么,一个事务在持有 AUTO-INC 锁的过程中,其他事务的如果要向该表插入语句都会被阻塞,从而保证插入数据时,被AUTO_INCREMENT
修饰的字段的值是连续递增的。
但是, AUTO-INC 锁再对大量数据进行插入的时候,会影响插入性能,因为另一个事务中的插入会被阻塞。
在 MySQL 5.1.22 版本开始,InnoDB 存储引擎提供了一种轻量级的锁来实现自增。
在插入数据的时候,会为被 AUTO_INCREMENT
修饰的字段加上轻量级锁,然后给该字段赋值一个自增的值,就把这个轻量级锁释放了,而不需要等待整个插入语句执行完后才释放锁。
一般插入语句在执行前就可以确定具体插入的记录数,就是用轻量级的锁,避免锁定表,提升插入性能。
InnoDB 引擎是支持行级锁的,而 MyISAM 引擎并不支持行级锁,其支持事务不相关的表级锁。
行级锁的类型主要有三类:
X
,说明是 X 型的 next-key 锁;X, REC_NOT_GAP
,说明是 X 型的记录锁;X, GAP
,说明是 X 型的间隙锁;Record Lock 称为记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的:
Gap Lock 称为间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。
给记录加上Gap锁,避免当前记录和前一条记录之间插入新的记录造成幻读现象,只有Gap锁释放了才会释放阻塞。
针对末尾,对Supermum记录(记录页面最大记录的,在最后)加上gap锁。
间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系。
Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock的组合,锁住某条记录,同时组织前面的间隙插入新记录。
next-key lock 即能保护该记录,又能阻止其他事务将新纪录插入到被保护记录前面的间隙中。
如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的。
因为Next-Key Lock是Record Lock和Gap Lock的组合,Record Lock区分S锁和X锁,是不兼容的。
对于这种范围为 (1006, +∞] 的 next-key lock,两个事务是可以同时持有的,不会冲突。因为 +∞ 并不是一个真实的记录,自然就不需要考虑 X 型与 S 型关系。
一个事务在插入一条记录的时候,需要判断插入位置是否已被其他事务加了Gap锁(next-key lock 也包含Gap锁)。
如果有的话,插入操作就会发生阻塞,直到拥有间隙锁的那个事务提交为止(释放Gap锁的时刻),在此期间会生成一个插入意向锁,表明有事务想在某个区间插入新记录,但是现在处于等待状态。
插入意向锁之间是可以兼容的,实际上是不会阻碍其他事务获取该记录上的任何锁。
插入意向锁与间隙锁的另一个非常重要的差别是:尽管「插入意向锁」也属于间隙锁,但两个事务却不能在同一时间内,一个拥有间隙锁,另一个拥有该间隙区间内的插入意向锁(当然,插入意向锁如果不在间隙锁区间内则是可以的)。所以,插入意向锁和间隙锁之间是冲突的。
在内存当中生成锁结构并不是零成本的事,所以提出了隐式锁的概念,比如一般情况下INSERT不需要生成锁结构。
在插入记录的同时,此时进行读写,可能会出现脏写、脏读情况
对于聚簇索引,是有隐藏列trx_id的,通过判断之前的记录是否为活跃事务,不是的话说明已经提交可以获取,否则的话未这个事务创建一个X锁,锁状态为false,再为自己创建一个X锁,状态为true。
对于二级索引,查看Page Header当中的,PAGE_MAX_TRX_ID,对该页面做改动的最大的事务ID,如果小于当前活跃的最小事务id,说明对该页面的修改已经提交,否则就需要定位到二级索引记录位置,通过回表到聚簇索引,重复上述的操作。
可以看到通过事务ID,隐式锁延缓了生成锁结构,只有在可能发生冲突时才会枷锁,从而减少锁的数量,提高系统整体性能。
隐式锁就是在 Insert 过程中不加锁,只有在特殊情况下,才会将隐式锁转换为显示锁,这里我们列举两个场景。