一、锁的分类
根据锁的粒度分类
- 全局锁
- 表级锁
- 行级锁
二、全局锁
全局锁用于锁定整个数据库。
实现方式
- 全局读锁:使用
FLUSH TABLES WITH READ LOCK (FTWRL)命令,整个数据库处于只读状态,适用于备份操作。
使用场景
- 全局读锁:在进行全库备份时,为了确保数据一致性,需要对整个数据库加全局读锁,防止其他事务进行写操作。
三、表级锁
表级锁用于锁定单个表,主要有两种:表锁和元数据锁(Meta Data Lock, MDL)。
表锁
表锁的粒度较粗,适用于短时间内对整张表进行的操作。
实现方式
- 读锁(共享锁,S 锁):多个事务可以同时对表进行读操作,但不能进行写操作。
- 写锁(排他锁,X 锁):只有一个事务可以对表进行写操作,其他事务无法进行任何操作。
使用场景
- 表读锁:当需要对表进行长时间的大量读操作,而不允许任何写操作时。
- 表写锁:当需要对表进行批量更新或删除操作时,为了防止其他事务进行读写操作,可以加表写锁。
元数据锁(MDL)
元数据锁在每次访问表的结构或数据时都会被隐式使用。
实现方式
- 自动管理:当一个事务执行 DDL(数据定义语言)操作(如
ALTER TABLE、DROP TABLE)时,系统会自动加上 MDL。
使用场景
- MDL 锁:用于确保在表结构发生变化时,表的数据一致性和结构一致性。例如,当一个事务正在修改表结构时,其他事务不能同时进行修改操作。
四、行级锁
行级锁是 MySQL 锁机制中最细粒度的锁,用于锁定单行数据。行级锁主要包括共享锁和排他锁。
优缺点:锁的粒度小,支持并发能力高,但是占用的内存比表锁和页锁都大,如果需要全表扫描,其它锁的性能要高于行锁(因为相当于锁的是每一行)
锁索引:行锁锁的是索引而非行数据,对于二级索引,会对涉及到的二级索引和回表后的主键索引都加锁,对主键索引加锁的原因其他事务可能会通过其他索引命中同一行数据。只对主键索引加锁也是可以的,但对二级索引加锁可以提高锁冲突检查的性能,当两个事物使用同一个二级索引时,就能够直接在该索引上检查出锁冲突而不需要回表到主键索引中检查
全表扫描加表锁:如果未命中索引而全表扫描,此时不会加行锁(成本太高)会直接加表锁
实现方式
- 共享锁(S 锁,读锁):允许多个事务同时读取一行数据,但不允许修改。
- 排他锁(X 锁,写锁):一个事务获取排他锁后,其他事务不能读取或修改该行数据。
使用场景
-
共享锁
在需要对特定行数据进行读取而不允许修改时使用,例如:
SELECT * FROM table_name WHERE id = 1 LOCK IN SHARE MODE; -
排他锁
在需要对特定行数据进行修改时使用,例如:
SELECT * FROM table_name WHERE id = 1 FOR UPDATE;
五、意向锁(Intention Locks)
属于行级锁:意向锁是 InnoDB 中的一种表级锁,用于指出事务即将在表中的某些行上加行级锁。
目的:修改的是行,理论上要对行加锁,为何会对表加意向锁呢?加行锁之前就会对表加意向锁,目的是在提高锁兼容性检查性能,如果不加意向锁直接加行锁,就需要在加行锁时做锁兼容性检查,而行锁的成本是高于表锁的(首先得先找到对应的行,而且可能不止一行数据,所以成本更高),如果加表级意向锁,就能够发现一部分兼容性重图就不会尝试去申请锁
兼容性

实现方式
- 意向共享锁(IS 锁):事务打算在表的某些行上加共享锁。
- 意向排他锁(IX 锁):事务打算在表的某些行上加排他锁。
使用场景
意向锁用于提高多事务环境下的锁兼容性检查性能,避免表级锁与行级锁的冲突。例如,当一个事务需要在表的某些行上加排他锁时,先申请 IX 锁,其他事务申请表级共享锁时会被阻塞。

@startuml
participant 事务A as a
participant 事务B as b
participant 表 as t
opt#yellow 在表级检查出冲突
a -> t:update table set x = ?修改整张表
t -> t:加表级排它锁
b -> t:update table set x = ? where id = 1,修改某行数据
t -> t:加意向排它锁
t -> t:锁重图检查:表级排它锁与意向排它锁冲突\n<color:green>提前在表级做锁冲突检查比加行锁时做冲突检查性能更高
end
opt#yellow 表级无法检查出冲突
a -> t:update table set x = ? where id = 1,修改某行数据
t -> t:加意向排它锁
t -> t:加行锁
b -> t:update table set x = ? where id = 1,修改某行数据
t -> t:加意向排它锁
t -> t:表级锁重图检查:<color:green>意向排他与意向排他不冲突
t -> t:行级锁冲突检查:行锁冲突
end
@enduml
六、间隙锁(Gap Lock)
用于防止幻读:间隙锁用于在索引记录之间的间隙上加锁,防止幻读。
锁间隙而非范围:锁定索引记录之间的间隙,而不是具体的记录。
select for update会加间隙锁:如果是范围查询,for update会加间隙锁,准确地讲是临键锁
使用场景
在可重复读隔离级别下,为了防止幻读,InnoDB 在执行范围查询时会在索引记录之间加间隙锁。例如:
SELECT * FROM table_name WHERE column BETWEEN 10 AND 20 FOR UPDATE;
七、临键锁(Next-Key Lock)
间隙锁与行锁的组合:键锁是行锁和间隙锁的组合,用于在执行范围查询时锁定索引记录和间隙。
使用场景
在可重复读隔离级别下,防止幻读。例如:
SELECT * FROM table_name WHERE column > 10 FOR UPDATE;
八、自增长锁(AUTO-INC Lock)
保证id顺序增长:自增长锁用于保证表中的自增长列的唯一性和顺序性。
实现方式:当一个事务向表中插入带有自增长列的新记录时,InnoDB 会自动加锁,防止其他事务插入记录。
使用场景
用于保证插入操作中自增长列的唯一性和顺序性。例如:
INSERT INTO table_name (name) VALUES ('John');
共享锁与排它锁
区别:一行数据可以被加多把共享锁,但排他锁只能加一把,且共享锁与排他锁互斥,排它锁与排他锁业互斥
场景:
- 任何的写操作都是排他锁
加的是排它锁select ... for update加的是共享锁select ... LOCK IN SHARE MODE- 四个隔离级别中,除了序列化其他三个隔离级别的非当前读都是没有加任何锁,序列化给普通读加了共享锁(不止共享锁,靠共享锁是无法解决幻读的,只能解决脏读问题)
锁的应用场景
隔离级别-序列化
加共享锁:序列化的的普通读会加上共享锁,防止数据被修改,可解决不可重复读问题
加临键锁:可解决幻读问题
死锁
形成原因:死锁是因为多个事务相互等待对方持有的资源,一般发生在多资源的场景下,比如对多行数据的修改,但但资源也可能存在死锁
锁的概念是通用的:锁的概率并非是mysql特有的,任何存在并发的场景都会出现锁,而锁的原理都是一样的
出现死锁如何解决:出现死锁必然要回滚其中一个事务,mysql会回滚持有资源更少的那个事务,因为这样影响的数据会更少
单行资源导致的死锁
因共享锁与互斥锁导致

@startuml
participant 事务A as a
participant 事务B as b
participant 表 as t
a -[#red]> t:select ... where id = 1 lock in share mode
t -> t:对id1加共享锁(<color:green>第一把共享锁)
b -[#blue]> t:select ... where id = 1 lock in share mode
t -> t:对id1家共享锁(<color:green>此时两把共享锁)
a -[#red]> t:update ... where id = 1
t -> t:加排他锁(<color:green>由于此时事务b持有共享锁无法加锁事物a阻塞)
b -[#blue]> t:update ... where id = 1
t -> t:加排他锁(<color:green>出现死锁:由于此时事务a持有共享锁无法加锁导致\n<color:green>事务b阻塞,a.b都阻塞等待对方释放资源形成死锁)
@enduml
插入数据引起的死锁
本质是上面的读写锁引起的同一行数据导致的死锁

@startuml
participant 事务A as a
participant 事务B as b
participant 事务C as c
participant 表 as t
a -[#red]> t:插入数据id=1
t -> t:校验是否存在唯一键冲突,使用当前读加共享锁
t -> t:检验不存在冲突插入数据**加排它锁**,**此时排他锁和共享锁都存在**
b -[#blue]> t:插入数据id=1
t -> t:检查唯一键冲突,**由于a持有排它锁,阻塞**
c -[#purple]> t:插入数据id=1
t -> t:检查唯一键冲突,**由于a持有排它锁,阻塞**,\n由于b.c之间都是在等待a释放资源所以并非是相互等待所以不存在死锁
a -[#red]> t:回滚
t -> t:**释放排它锁和共享锁**
t -> t:b尝试加排他锁,由于此时c持有共享锁无法加锁,等待c释放共享锁
t -> t:同样,c尝试加排他锁,由于此时b持有共享锁无法加锁,等待b释放共享锁,\n**b.c存在相互等待对方持有的资源,死锁**
@enduml