锁分类

锁分类:

角度 分类
处理锁的态度 悲观锁、乐观锁
锁的粒度 行锁、表锁、全局锁
锁是否互斥的特性 共享锁、排他锁
算法锁 临键锁、间隙锁、记录锁
状态锁 意向共享锁、意向排他锁

按粒度分

MySQL 中的锁可以按照粒度分为锁定整个表的表级锁(table-level locking)和锁定数据行的行级锁(row-level locking):

  • 表级锁具有开销小、加锁快的特性;但锁定粒度较大,发生锁冲突的概率高,支持的并发度低。
  • 行级锁具有开销大,加锁慢的特性;但锁定粒度较小,发生锁冲突的概率低,支持的并发度高。

InnoDB 存储引擎同时支持行级锁和表级锁,默认情况下采用行级锁。

InnoDB的行锁是实现在索引上的,而不是锁在物理行记录上。所以如果访问没有命中索引,就无法使用行锁,将退化为表锁(共享行锁上升为共享表锁,排他行锁上升为排他表锁)。(Oracle的行锁实现机制不同。)

全局锁:

全局锁就是对整个数据库实例加锁,获得全局锁后的数据库无法进行数据的更新操作与表结构修改操作。

让数据库变为只读状态,且数据的更新操作会被阻塞

1
Flush tables with read lock

解锁命令

1
UNLOCK TABLES

表锁

表级锁有两种:表数据锁(常说的表锁),元数据表锁(MDL,metadata lock,Mysql5.5版本之后加的)

元数据表锁

MDL锁在语句开始执行时申请,在事务提交后释放。只要有事务在执行,mysql 就会自动加上元数据表锁(MDL),这样在执行过程中就不能发生表结构变更。

对表进行增删改查,加 MDL 读锁,执行表结构变更(DDL命令)加 MDL 写锁。读读不互斥、读写和写写都互斥

表数据锁

即表锁,主动加锁,可以使用 lock 和 unlock 关键词来加锁和释放锁: lock tables 表名 read/write。比如:lock tables A read,B write。未解锁前不能其它表进行CRUD操作。加上读锁后,任何线程都只能执行都操作,写操作都会被阻塞,包括加锁的线程也是这样。

自动加表级锁

1.ALTER TABLE操作:在执行ALTER TABLE操作时,MySQL会获取一个排它锁,防止其他事务对该表进行读写操作;ALTER TABLE 加字段在Mysql5.6版本之后新增了ONLINE DDL的功能,可以使该表不能使用的时间大大缩短。

ALTER TABLE 加字段的时候,如果该表的数据量非常大,不要设置default值。

例如,当前有2000w+数据量的表。如果加字段加了default值。Mysql会在执行Online DDL之后,对整个表的数据进行更新默认值的操作。这样就相当于是更新了2000w+的数据,而且是在同一个事务里。也就是说这个事务会把整个表都锁住,直到所有的数据记录都更新完默认值以后,才会提交。

这个时间非常长,而且由于会锁全表的记录,所以该表不可用的时间会非常长。

2.大事务操作:如果一个事务中包含大量的数据操作,比如更新或删除大量数据,那么MySQL会自动将该事务的隔离级别提升为SERIALIZABLE,也就是串行化,表级共享锁,此时会对涉及到的所有表进行锁定,避免其他事务对该表的并发操作;

3.锁冲突较多:如果系统中存在较多的锁冲突,比如同一个表上有多个事务争夺同一个行锁或排它锁时,MySQL会自动将该表的锁级别提升为表锁,避免锁冲突的出现,提高系统的性能。 需要注意的是,表锁是对整张表进行锁定,因此会对其他事务的并发操作产生较大的影响,会导致系统的响应时间变慢,降低系统的并发性能。因此,在实际应用中,应该尽量避免使用表锁,而是采用行锁或其他更细粒度的锁机制来保证数据的一致性和并发性。

意向锁

意向锁(Intention Lock)分为意向共享锁 (intention shared lock, IS) 、意向排他锁 (intention exclusive lock, IX),分别表示事务有意向对表里的某些记录加共享行锁和排他行锁。

1
事务要获取某些行的 S/X 锁,必须先获得表的 IS /IX锁。

即,意向锁是有数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享 / 排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。

意向锁存在意义:

意向共享锁(IS) 意向排他锁(IX)
意向共享锁(IS) 兼容 兼容
意向排他锁(IX) 兼容 兼容

意向锁之间互相兼容,但是它会与普通的表级排他 / 共享锁互斥。

意向锁不会与行级的共享 / 排他锁互斥。

例如:

事务 A 获取了某一行的排他锁,并未提交:

1
SELECT * FROM users WHERE id = 6 FOR UPDATE;

事务 B 想要获取 users 表的表锁:

1
LOCK TABLES users READ;

事务 B 在试图对 users 表加共享锁的时候,必须保证:

1当前没有其他事务持有 users 表的排他锁。

2当前没有其他事务持有 users 表中任意一行的排他锁。

为检测是否满足第二个条件,事务 B 必须在确保 users表不存在任何排他锁(表级)的前提下,去检测表中的每一行是否存在排他锁。很明显这是一个效率很差的做法,但是有了意向锁之后,情况就不一样了:

事务 A 获取了某一行的排他锁,并未提交:

1
SELECT * FROM users WHERE id = 6 FOR UPDATE;

此时 users 表存在两把锁:users 表上的意向排他锁与 id 为 6 的数据行上的排他锁

事务 B 想要获取 users 表的共享锁:

1
LOCK TABLES users READ;

此时事务 B 检测事务 A 持有 users 表的意向排他锁,就可以得知事务 A 必然持有该表中某些数据行的排他锁,那么事务 Busers 表的加锁请求就会被排斥(阻塞),而无需去检测表中的每一行数据是否存在排他锁。

由于,意向锁不会与行级的共享 / 排他锁互斥,意向锁并不会影响到多个事务对不同数据行加排他锁时的并发性。

此外,

InnoDB 支持多粒度锁(multiple granularity locking),它允许行级锁与表级锁共存,而意向锁就是其中的一种表锁,是一种不与行级锁冲突表级锁。

由 InnoDB 自动添加,不需要用户干预,是一种表级锁弱锁,仅仅用于表明意向。

自增锁

是一种特殊的表级别锁,专门针对事务插入AUTO_INCREMENT类型的列。最简单的情况,如果一个事务正在往表中插入记录,所有其他事务的插入必须等待,以便第一个事务插入的行,是连续的主键值。控制同一sql 语句插入的所有记录的自增id是连续的。

innodb通过 innodb_autoinc_lock_mode可以查看自增锁的状态。取值为0/1/2,默认为 1,

行锁

排他 X 锁和共享 S 锁是 Innodb 的行级概念锁。保证同一行记录修改与删除的串行性,从而保证数据的强一致。

共享锁是读锁,多个事务可以拿到同一行记录的共享锁,所以读-读可以并发。

排他锁是写锁,同一行记录的排他锁在同一时刻只能有一个事务获得,所以写-写是互斥的。实际上读写也是互斥的,也就是有排他锁就不能加共享锁,有共享锁就不能加排他锁。

共享锁

加了lock in share mode 的 select 语句, 比如:select … lock in share mode

普通 select 实施不加锁多版本快照读(与MVCC多版本并发控制相关)

排它锁

update, delete, insert 都是行级排它锁

主动加锁,比如 for update 的 select 语句,比如 select … for update

算法分

记录锁

Record Locks,加锁对象是索引节点,锁定一个记录上的索引,而不是记录本身。如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks依然可以使用。

手动或者自动加了for update 才会加记录锁,否则不加锁,实施快照读。

间隙锁

Gap Locks,实施在索引上,它用于锁定的索引之间的间隙,即键值在条件范围内但并不存在的记录,它不会包含记录本身,GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。

可以防止间隙内有新数据被插入,以及防止已存在的记录,更新成间隙内的记录。适用于在唯一索引上使用了范围查询条件。

间隙是指索引中两个索引键之间的空间,比如,并发2个事务:

A:

1
2
BEGIN;
SELECT * FROM `products` WHERE `product_id` BETWEEN 100 and 200 FOR UPDATE;

B:

1
2
BEGIN;
INSERT INTO `products` (`product_id`, `name`) VALUES (150, 'Product 150');

事务A会在products表中product_id值在 100 和 200 之间的范围上设置间隙锁,在事务A运行期间,其他事务无法在这个范围内插入新的数据,在事务B尝试插入product_id为150的记录时,由于该记录位于事务A锁定的间隙范围内,事务B将被阻塞,直到事务A释放间隙锁为止。

临键锁

Next-Key Lock:实施在索引上,Record Lock和Gap Lock的组合,锁定一个范围的记录和间隙。是mysql的一种锁机制,对于行的查询,都是采用该方法,主要目的是解决幻读的问题。

GAP锁和Next Key Lock区别

区别在于锁定范围,Gap锁只锁定索引记录之间的间隙,而Next Key Lock则同时锁定索引记录和间隙。另外,Next Key Lock是MySQL中的一种锁机制,它是在Gap锁的基础上引入的。

A.唯一索引,精确等值检索,Next-Key Locks就退化为记录锁,不会加gap锁。

B.**非唯一索引,**精确等值检索,Next-Key Locks会对间隙加gap锁(至于区间是多大稍后讨论),以及对应检索到的记录加记录锁。

C.范围检索,会锁住where条件中相应的范围,范围中的记录以及间隙,换言之就是加上记录锁和gap 锁

D.不走索引检索,全表间隙加gap锁、全表记录加记录锁。

插入意向锁

间隙锁的一种。

作用:多个事务,在同一个索引,同一个范围区间插入记录时,如果插入的位置不冲突,不会阻塞彼此。用于提高插入并发,因为如果使用间隙锁的话,不允许多个事务同时往同一个索引间隙插入记录,但是使用插入意向锁可以。如果插入的位置冲突呢?怎么办?另一个回滚吗?

比如两个事务都想往id(10,20)插入一条记录,但是两个事务插入记录的id分别是 11 和 12,插入的位置不冲突,所以不会阻塞对方。

处理锁的态度分

悲观锁

指在读取数据的时候总是认为别人会修改它,于是在取数据的时候会对当前数据加一个锁,在结束事务前(提交事务前),不允许其他事务对当前数据进行更改。应用于数据更新比较频繁的场景。

MySQL层常用的 悲观锁 实现方式是加一个 排他锁

  • 排他锁的解释是:“通过在事务中使用 select xx for update 语句来实现”。排他锁会在当前行加一个行级锁,在当前事务提交前,其他事务无法进行更改操作。”

例如:

1
2
3
4
begin;
select * from emp where id = 1 for update;
update emp set name = 'zhang3' where id = 1;
commit;

乐观锁

指在读取数据的时候总会天真的认为没有人会去修改它,在更改操作的时候再去检查冲突。适用于读多写少的场景

乐观锁 机制采取了更加宽松的加锁机制。

悲观锁 大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本(Version)记录机制实现。即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个"version"字段来实现。

乐观锁 的工作原理:

读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。需要重新查询再修改。

比如,事务A在更改数据的时候,要先看是否匹配版本号,如果匹配成功再修改,失败则不修改:

1
2
3
4
begin;
select * from emp where id = 1;
update emp set name = 'zhang3' where id = 1 and version = 1;
commit;

简而言之,操作数据时不会上锁,但是更新时会判断在此期间有没有别的事务更新这个数据,若被更新过,则失败重试。