文章目录
学习资料
【MySQL数据库教程天花板,mysql安装到mysql高级,强!硬!-哔哩哔哩】
【阿里巴巴Java开发手册】https://www.w3cschool.cn/alibaba_java
锁的不同角度分类
锁的分类图如下
从数据操作的粒度划分:表级锁、页级锁、行锁
为了尽可能提高数据库的并发度,每次锁定的数据范围越小越好,理论上每次只锁定当前操作的数据的方案会得到最大的并发度,但是管理锁是很
耗资源
的事情(涉及获取、检查、释放锁等动作)。因此数据库系统需要在高并发响应
和系统性能
两方面进行平衡,这样就产生了锁粒度(Lock granularity)
的概念。
对一条记录加锁影响的也只是这条记录而已,我们就说这个锁的粒度比较细;其实一个事务也可以在
表级别
进行加锁,自然就被称之为表级锁
或者表锁
,对一个表加锁影响整个表的记录,我们就说这个锁的粒度比较粗。锁的粒度主要分为表级锁、页级锁和行锁。
表锁(Table Lock)
该锁会锁定整张表,它是MySQL中最基本的锁策略,并
不依赖于存储引擎
(不管你是MySQL的什么存储引擎,对于表锁的策略都是一样的),并且表锁是开销最小
的策略(因为粒度比较大)。由于表级锁一次会将整个表锁定,所以可以很好的避免死锁
问题。当然,锁的粒度大所带来的最大的负面影响就是出现锁资源争用的概率也会最高,导致并发率大打折扣
。
表级别的S锁、X锁
在对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,InnoDB存储引擎是不会为这个表添加表级别的
S锁
或者X锁
的。在对某个表执行一些诸如ALTER TABLE、DROP TABLE
这类的DDL
语句时,其他事务对这个表并发执行诸如SELECT、INSERT、DELETE、UPDATE的语句会发生阻塞。同理,某个事务中对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,在其他会话中对这个表执行DDL
语句会发生阻塞。这个过程其实是通过在server层
使用一种称之为元数据锁
(英文名:Metadata Locks
,简称MDL
)结构来实现的。
一般情况下,不会使用InnoDB存储引擎提供的表级别的
S锁
和X锁
。只会在一些特殊情况下,比方说崩溃恢复
过程中用到。比如,在系统变量autocommit=0,innodb_table_locks=1
时,手动
获取InnoDB存储引擎提供的表t的S锁
或者X锁
可以这么写:
LOCK TABLES t READ
:InnoDB存储引擎会对表t
加表级别的S锁
。
LOCK TABLES t WRITE
:InnoDB存储引擎会对表t
加表级别的X锁
。
不过尽量避免在使用InnoDB存储引擎的表上使用LOCK TABLES
这样的手动锁表语句,它们并不会提供什么额外的保护,只是会降低并发能力而已。InnoDB的厉害之处还是实现了更细粒度的行锁
,关于InnoDB表级别的S锁
和X锁
大家了解一下就可以了。
# 查看表上加过的锁,主要关注In_use字段的值
SHOW OPEN TABLES;
# 或者
SHOW OPEN TABLES where In_use > 0;
# 手动增加表锁命令
LOCK TABLES t READ;# 存储引擎会对表t加表级别的共享锁。共享锁也叫读锁或S锁(Share的缩写)
LOCK TABLES t WRITE;# 存储引擎会对表t加表级别的排他锁。排他锁也叫独占锁、写锁或 X锁(是eXclusive的缩写)
# 解锁当前加锁的表
UNLOCK TABLES;
意向锁(intention lock)
InnoDB支持
多粒度锁(multiple granularity locking)
,它允许行级锁
与表级锁
共存,而意向锁
就是其中的一种表锁
。
1、意向锁的存在是为了协调行锁和表锁的关系,支持多粒度(表锁与行锁)的锁并存。
2、意向锁是一种不与行级锁冲突表级锁
,这一点非常重要。
3、表明“某个事务正在某些行持有了锁或该事务准备去持有锁”。
意向锁分为两种:
意向共享锁
(intention shared lock,IS):事务有意向对表中的某些行加共享锁
(S锁)。
# 事务要获取某些行的S锁,必须先获得表的IS锁
SELECT column FROM table ... LOCK IN SHARE MODE;
意向排他锁
(intention exclusive lock,IX):事务有意向对表中的某些行加排他锁
(X锁)。
# 事务要获取某些行的X锁,必须先获得表的IX锁。
SELECT column FROM table ... FOR UPDATE;
即:意向锁是由存储引擎
自己维护的
,用户无法手动操作意向锁,在位数据行加共享/排他锁之前,InnoDB会先获取该数据行所在数据表的对应意向锁
。
意向锁的并发性
:意向锁不会与行级的共享/排他锁互斥!正因为如此,意向锁并不会影响到多个事务对不同数据行加排他锁时的并发性(不然我们直接用普通的表锁就行了)。
总结
:
1、InnoDB支持多粒度锁
,特定场景下,行级锁可以与表级锁共存。
2、意向锁之间互不排斥,但除了IS与S兼容外,意向锁会与 共享锁/排他锁 互斥
。
3、IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突。
4、意向锁在保证并发性的前提下,实现了行锁和表锁共存
且满足事务隔离性
的要求。
自增锁(AUTO-INC锁)
所有插入数据的方式总共分为三类,分别是“
Simple inserts
”,“Bulk inserts
”和“Mixed-mode inserts
”。
1、“Simple inserts”(简单插入)
可以预先确定要插入的行数
(当语句被初始处理时)的语句。包括没有嵌套子查询的单行和多行INSERT ... VALUES()
和REPLACE
语句。
2、“Bulk inserts”(批量插入)
事先不知道要插入的行数
(和所需自动递增值的数量)的语句。比如INSERT ... SELECT
,REPLACE ... SELECT
和LOAD DATA
语句,但不包括纯INSERT。InnoDB在每处理一行,为AUTO_INCREMENT列分配一个新值。
3、“Mixed-mode inserts”(混合模式插入)
这些是“Simple inserts”语句但是指定部分新行的自动递增值。例如INSERT INTO teacher (id,name) VALUES (1,'a'),(NULL,'b'),(5,'c'),(NULL,'d');
只是指定了部分id的值。另一种类型的“混合模式插入”是INSERT ... ON DUPLICATE KEY UPDATE
。
对于上面数据插入的案例,MySQL中采用了
自增锁
的方式来实现,AUTO-INC锁是当向使用含有AUTO_INCREMENT列的表中插入数据时需要获取的一种特殊的表级锁
,在执行插入语句时就在表级别加一个AUTO-INC锁,然后为每条待插入记录的AUTO_INCREMENT修饰的列分配递增的值,在该语句执行结束后,再把AUTO-INC锁释放掉。一个事务在持有AUTO-INC锁的过程中,其他事务的插入语句都要被阻塞
,可以保证一个语句中分配的递增值是连续的。也正因为此,其并发性显然并不高,当我们向一个由AUTO_INCREMENT关键字的主键插入值的时候,每条语句都要对这个表锁进行竞争
,这样的并发潜力其实是很低下的,所以innodb通过innodb_autoinc_lock_mode
的不同取值来提供不同的锁定机制,来显著提高SQL语句的可伸缩性和性能。
innodb_autoinc_lock_mode有三种取值,分别对应与不同锁定模式:
(1)innodb_autoinc_lock_mode = 0(“传统”锁定模式)
在此锁定模式下,所有类型的insert语句都会获得一个特殊的表级AUTO-INC锁,用于插入具有AUTO_INCREMENT列的表。这种模式其实就如我们上面的例子,即每当执行insert的时候,都会得到一个表级锁(AUTO-INC锁),使得语句中生成的auto_increment为顺序,且在binlog中重放的时候,可以保证master与slave中数据的auto_increment是相同的。因为表级锁,当在同一时间多个事务中执行insert的时候,对于AUTO-INC锁的争夺会限制并发
能力。
(2)innodb_autoinc_lock_mode = 1(“连续”锁定模式)
在MySQL8.0之前,连续锁定模式是默认
的。
在这个模式下,“bulk inserts”仍然使用AUTO-INC表级锁,并保持语句结束。这适用于所有INSERT … SELECT,REPLACE … SELECT和LOAD DATA语句。同一时刻只有一个语句可以持有AUTO-INC锁。
对于“Simple inserts”(要插入的行数事先已知),则通过mutex(轻量锁)
的控制下获得所需数量的自动递增值来避免表级AUTO-INC锁,它只在分配过程的持续时间内保持,而不是直到语句完成。不使用表级AUTO-INC锁,除非AUTO-INC锁由另一个事务保持。如果另一个事务保持AUTO-INC锁,则“Simple inserts”等待AUTO-INC锁,如果它是一个“bulk inserts”。
(3)innodb_autoinc_lock_mode = 2(“交错”锁定模式)
从MySQL8.0
开始,交错模式是默认
设置。
在这种锁定模式下,所有类INSERT语句都不会使用表级AUTO-INC锁,并且可以同时执行多个语句。这是最快和最可扩展的锁定模式,但是当使用基于语句的复制或恢复方案时,从二进制日志重播SQL语句时,这是不安全的。
在此锁定模式下,自动递增值保证
在所有并发执行的所有类型的insert语句中是唯一
且单调递增
的。但是,由于多个语句可以同时生成数字(即,跨语句交叉编号),为任何给定语句插入的行生成的值可能不是连续的
。
如果执行的语句是“simple inserts”,其中要插入的行数已提前知道,除了“Mixed-mode inserts”之外,为单个语句生成的数字不会有间隙。然而,当执行“bulk inserts”时,在由任何给定语句分配的自动递增值可能存在间隙。
元数据锁(MDL锁)
MySQL5.5引入了meta data lock,简称MDL锁,属于表锁范畴。MDL的作用是,保证读写的正确性。比如,如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个
表结构做变更
,增加了一列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的。
因此,当对一个表做增删改查操作的时候,加MDL读锁;当要对表做结构变更操作的时候,加MDL写锁。
读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。读写锁之间、写锁之间是互斥呃,用来保证变更表结构操作的安全性,解决了DML和DDL操作之间的一致性问题。不需要显式使用
,在访问一个表的时候会被自动加上。
InnoDB中的行锁
行锁(Row Lock)也称为记录锁,顾名思义,就是锁住某一行(某条记录row)。需要注意的是,MySQL服务器并没有实现行锁机制,
行级锁只在存储引擎层实现
。
优点
:锁定粒度小,发生锁冲突概率低
,可以实现的并发度高
。
缺点
:对于锁的开销比较大
,加锁会比较慢,容易出现死锁
情况。
记录锁(Record Locks)
记录锁也就是仅仅一条记录锁上,官方的类型名称为:
LOCK_REC_NOT GAP
。比如我们把id值为8的那条记录加一个记录锁的示意图如图所示。仅仅是锁住了id值为8的记录,对周围的数据没有影响。
记录锁是有S锁和X锁之分的,称之为S型记录锁
和X型记录锁
。
当一个事务获取了一条记录的S型锁后,其他事务也可以继续获取该记录的S型记录锁,但不可以继续获取X型记录锁;
当一个事务获取了一条记录的X型记录锁后,其他事务即不可以继续获取该记录的S型记录锁,也不可以继续获取X型记录锁。
间隙锁(Gap Locks)
MySQL
在REPEATABLE READ
隔离级别下是可以解决幻读问题的,解决方案有两种,可以使用MVCC
方案解决,也可以采用加锁
方案解决。但是在使用加锁方案解决时有个大问题,就是事务在第一次执行读取操作时,那些幻影记录上不存在,我们无法给这些幻影记录
加上记录锁
。InnoDB提出了一种称之为Gap Locks
的锁,官方的类型名称为:LOCK_GAP
,我们可以简称为gap锁
。比如,把id值为8的那条记录加一个gap锁的示意图如下。
图中id值为8的记录加了gap锁,意味着不允许别的事务在id值为8的记录前后边的间隙插入新纪录
,其实就是id列的值(3,8)、(8,15)这个区间的新纪录是不允许立即插入的。比如,有另外一个事务想在插入一条id值为4的新纪录,它定位到该条新纪录的下一条记录的id值为8,而这条记录上又有一个gap锁,所以就会阻塞插入操作,直到拥有这个gap锁的事务提交了之后,id列的值在区间(3,8)、(15,20)及20之后,中的新纪录才可以被插入。
gap锁的提出仅仅是为了防止插入幻影记录而提出的。
虽然有共享gap锁
和独占gap锁
这样的说法,但是它们起到的作用是相同的。而且如果对一条记录加了gap锁(不论是共享gap锁还是独占gap锁),并不会限制其他事务对这条记录加记录锁或者继续加gap锁。
临键锁(Next-Key Locks)
有时候我们即想
锁住某条记录
,又想阻止
其他事务在该记录前边的间隙插入新纪录
,所以InnoDB就提出了一种称之为Next-Key Locks
的锁,官方的类型名称为:LOCK_ORDINARY
,我们也可以简称为net-key锁
。Next-Key Locks是存储引擎innodb
、事务级别在可重复读
的情况下使用的数据库锁,innodb默认的锁就是Next-Key locks。比如,我们把id值为8的那条记录加一个next-key锁的示意图如下:
next-key锁
的本质就是一个记录锁
和一个gap锁
的合体,它即能保护该条记录,又能阻止别的事务将新纪录插入被保护纪录前边的间隙
。
插入意向锁(Insert Intention Locks)
我们说一个事务
插入
一条记录时需要判断一下插入位置是不是被别的事务加了gap锁
(next-key
也包含gap锁
),如果有的话,插入操作需要等待,直到拥有gap锁
的那个事务提交。但是InnoDB规定事务在等待的时候也需要在内存中生成一个锁结构
,表明有事务想在某个间隙
中插入
新纪录,但是现在在等待。InnoDB就把这种类型的锁命名为Insert Intention Locks
,官方的类型名称为:LOCK_INSERT_INTENTION
,我们称为插入意向锁
。插入意向锁是一种Gap锁
,不是意向锁,在insert操作时产生。
插入意向锁是在插入一条记录行前,由
INSERT操作产生的一种间隙锁
。该锁用以表示插入意向,当多个事务在同一区间(gap)插入位置不同的多条数据时,事务之间不需要互相等待。假设存在两条值分别为4和7的记录,两个不同的事务分别视图插入值为5和6的两条记录,每个事务在获取插入行独占的(排他)锁前,都会获取(4,7)之间的间隙锁,但是因为数据行之间并不冲突
,所以两个事务之间并不会产生冲突(阻塞等待)。
页锁
页锁就是在
页的粒度
上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录。当我们使用页锁的时候,会出现数据浪费的现象,但这样的浪费最多也就是一个页上的数据行。页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。
每个层级的锁数量是有限制的,因为锁会占用内存空间,
锁空间的大小是有限的
。当某个层级的锁数量超过了这个层级的阈值时,就会进行锁升级
。锁升级就是用更大粒度的锁替代多个更小粒度的锁,比如InnoDB中行锁升级为表锁,这样做的好处是占用锁空间降低了,但同时数据的并发度也下降了。