2.1 锁介绍

按照锁的粒度来说,MySQL主要包含三种类型(级别)的锁定机制:

  • 全局锁:锁的是整个database。由MySQL的SQL layer层实现的
  • 表级锁:锁的是某个table。由MySQL的SQL layer层实现的
  • ⾏级锁:锁的是某⾏数据,也可能锁定⾏之间的间隙。由某些存储引擎实现,⽐如InnoDB。

按照锁的功能来说分为:共享读锁和排他写锁。

按照锁的实现⽅式分为:悲观锁和乐观锁(使⽤某⼀版本列或者唯⼀列进⾏逻辑控制)

image-20210225160908774.png

2.2 表级锁

2.2.1 表锁

表锁有两种表现形式:表共享读锁(Table Read Lock)表独占写锁(Table Write Lock)

  1. LOCK TABLES
  2. tbl_name [[AS] alias] lock_type
  3. [, tbl_name [[AS] alias] lock_type] ...
  4. lock_type: {
  5. READ [LOCAL]
  6. | [LOW_PRIORITY] WRITE
  7. }
  8. UNLOCK TABLES

例如 :

session1: lock table tb_user read; -- 给mylock表加读锁

session1: select * from tb_user; -- 可以查询
session1:select * from tb_dep; --不能访问⾮锁定表

session2:select * from tb_user; -- 可以查询 没有锁
session2:update tb_user set name='x' where id=2; -- 修改阻塞,⾃动加⾏写锁

session1:unlock tables; -- 释放表锁

session2:Rows matched: 1 Changed: 1 Warnings: 0 -- 修改执⾏完成

session1:select * from tb_dep; --可以访问

2.2.2 元数据锁

元数据锁不需要显式使⽤,在访问⼀个表的时候会被⾃动加上。元数据锁的作⽤是,保证读写的正确性。

可以想象⼀下,如果⼀个查询正在遍历⼀个表中的数据,⽽执⾏期间另⼀个线程对这个表结构做变更,删了⼀列,那么查询线程拿到的结果跟表结构对不上,肯定是不⾏的。

因此,在 MySQL 5.5 版本中引⼊了 元数据锁,当对⼀个表做增删改查操作的时候,加元数据读锁;当要对表做结构变更操作的时候,加 元数据写锁。

读锁之间不互斥,因此你可以有多个线程同时对⼀张执行查询。

读写锁之间、写锁之间是互斥的,⽤来保证变更表结构操作的安全性。

因此,如果有两个线程要同时给⼀个表加字段,其中⼀个要等另⼀个执⾏完才能开始执⾏。

例如 :

session1: begin;--开启事务
session1: select * from tb_user;--加MDL读锁

session2: alter table tb_user add email varchar(20); -- 修改阻塞

session1:commit; --提交事务 或者 rollback 释放读锁

session2:Query OK, 0 rows affected (38.67 sec) --修改完成

2.2.3 意向锁

InnoDB 支持多粒度的锁,允许表级锁和行级锁共存。一个类似于 LOCK TABLES … WRITE 的语句会获得这个表的 x 锁。为了实现多粒度锁,InnoDB 使用了意向锁(简称 I 锁)。I 锁是表明一个事务稍后要获得针对一行记录的某种锁(s or x)的对应表的表级锁,有两种:

  • 意向排它锁(简称 IX 锁)表明一个事务意图在某个表中设置某些行的 x 锁
    SELECT * FROM table_name WHERE ... FOR UPDATE
    
  • 意向共享锁(简称 IS 锁)表明一个事务意图在某个表中设置某些行的 s 锁
    SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
    

意向锁的原则如下:

  • 一个事务必须先持有该表上的 IS 或者更强的锁才能持有该表中某行的 S 锁
  • 一个事务必须先持有该表上的 IX 锁才能持有该表中某行的 X 锁
-- 意向排它锁

session1 : begin ;
session1 : select * from tb_user where id = 1 for update ; -- 设置tb_user的排它锁


session2 : select * from tb_user where id = 1 for update ;  --阻塞

session1 : commit ;  --提交事务

session2 : -- session2执行查询
+----+------+------+------+--------+--------+-------+
| id | age  | name | sex  | deptId | addrId | email |
+----+------+------+------+--------+--------+-------+
|  1 |   34 | Bob  | man  |      1 |      2 | NULL  |
+----+------+------+------+--------+--------+-------+
1 row in set (8.64 sec)


-- 意向共享锁

session1 : begin ;
session1 : select * from tb_user where id = 1 LOCK IN SHARE MODE ; -- 设置tb_user的共享

session2 : select * from tb_user where id = 1  LOCK IN SHARE MODE ;  -- 查询
+----+------+------+------+--------+--------+-------+
| id | age  | name | sex  | deptId | addrId | email |
+----+------+------+------+--------+--------+-------+
|  1 |   34 | Bob  | man  |      1 |      2 | NULL  |
+----+------+------+------+--------+--------+-------+

2.4 行级锁

MySQL的⾏级锁,是由存储引擎来实现的,利⽤存储引擎锁住索引来实现的

InnoDB的⾏级锁,按照锁定范围来说,分为三种:

  • 记录锁(Record Locks):锁定索引中⼀条记录。 id=1
  • 间隙锁(Gap Locks):要么锁住索引记录中间的值,要么锁住第⼀个索引记录前⾯的值或者最后⼀个索引记录后⾯的值。
  • Next-Key Locks:是索引记录上的记录锁和在索引记录之前的间隙锁的组合。

InnoDB的⾏级锁,按照功能来说,分为两种:

  • 共享锁:允许⼀个事务去读⼀⾏,阻⽌其他事务获得相同数据集的排他锁。
  • 排他锁(独占锁):允许获得排他锁的事务更新数据,阻⽌其他事务取得相同数据集的共享读锁(不是读)
    和排他写锁。

2.4.1 记录锁

记录锁针对索引记录。举个例子, SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE; 会阻止其他所有事务插入、修改或者删除 t.c1 是 10 的行。

记录锁总是锁定索引记录,即使表没有索引(这种情况下,InnoDB会创建隐式的索引,并使用这个索引实施记录锁)

2.4.2 间隙锁

间隙锁(gap)是索引记录之间上的锁,或者说第一个索引记录之前或最后一个索引记录之后的间隔上的锁。例如,SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE; 阻止其他事务插入 t.c1 = 15 的记录,不管是否已经有这种值在本列中,因为这个范围内的所有值都被上锁了。

一个间隙可能是一个索引值、多个索引值,甚至是空的。

间隙锁是性能与并发的部分折中,并只适用于一些事务隔离级别。

使用唯一索引的时候用不上间隙锁。例如,id 列有唯一索引,下面的语句只是用索引记录锁(针对id=100的行)不管其他会话是否在前面的间隙中插入行。

SELECT * FROM child WHERE id = 100;

如果id列没有索引或者是非唯一索引,那么这条语句的确会锁住前面的间隙。

session1 : begin ;
session1 : SELECT c1 FROM tb_user WHERE age BETWEEN 20 and 40 FOR UPDATE; -- 加

session2 : insert into tb_user values(null,30,'JAY','man',1,1,'JAY@163.com'); --阻塞

间隙锁(Gap Lock)是Innodb在可重复读提交下为了解决幻读问题时引入的锁机制

2.4.3 Next-Key Locks

Next-Key Locks (简称 NK 锁)是记录锁和间隙锁的组合。

Innodb是这样执行行级别锁的,它搜索或者扫描一个表的索引,在他遇上的索引记录上设置共享或者排他锁,这样行级锁实际就是索引记录锁,一个NK 锁同样影响索引记录之前的间隙。所以,NK 锁是一个索引记录锁和索引记录之前的间隙上的间隙锁。如果一个会话在一行 R 上有一个共享、排它锁,其他会话不能立即在R之前的间隙中插入新的索引记录。

假设一个索引包含值 10,11,13和20,索引上可能的NK 锁包括如下几个区间(注意开闭区间)

(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)

对于最后一个区间,NK 锁锁住了索引中最大值和比索引值中任何值都大的上确界伪值之上的间隙。上确界不是一个真正的索引记录,所以事实上NK锁只锁住了最大索引值上的间隙。

默认情况下,Innodb 是可重复读隔离级别,这样的话,Innodb使用NK 锁来进行索引搜索和扫描,阻止了幻读

2.4.1 共享锁与独占锁

InnoDB 实现了标准的行级锁,包括两种:共享锁(简称 s 锁)、排它锁(简称 x 锁)

  • 共享锁允许持锁事务读取数据
  • 排它锁允许持锁事务更新或者删除数据

如果事务 T1 持有行 r 的 s 锁,那么另一个事务 T2 请求 r 的锁时,会做如下处理:

  • T2 请求 s 锁立即被允许,结果 T1 T2 都持有 r 行的 s 锁
  • T2 请求 x 锁不能被立即允许

如果 T1 持有 r 的 x 锁,那么 T2 请求 r 的 x、s 锁都不能被立即允许,T2 必须等待T1释放 x 锁才行。

2.6 死锁

所谓死锁(DeadLock)是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去.此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。表级锁不会产生死锁 , 只有行级锁才会产生死锁

session1 : begin ; 
session1 : update tb_user set name = 'JIMY' where id = 1 ; 

session2 : begin ; 
session2 : update tb_user set name = 'JIMY' where id = 2 ;   --阻塞

session1 : update tb_user set name = 'Heim' where id = 2 ; 

session2 : 
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

2.6.1 处理死锁

对待死锁常见的两种策略:
通过 innodblockwait_timeout 来设置超时时间,一直等待直到超时;
发起死锁检测,发现死锁之后,主动回滚死锁中的某一个事务,让其它事务继续执行。

2.6.2 如何查看死锁

使用命令 show engine innodb status 查看最近的一次死锁。
InnoDB Lock Monitor 打开锁监控,每 15s 输出一次日志。使用完毕后建议关闭,否则会影响数据库性能。

2.6.3 如何避免死锁

可以在事务开始时通过SELECT … FOR UPDATE 语句来获取意向锁
在事务中,如果要更新记录,应该直接申请排他锁,而不应先申请共享锁、更新时再申请排他锁
如果事务需要修改或锁定多个表,则应在每个事务中以相同的顺序使用加锁语句。
通过 SELECT … LOCK IN SHARE MODE 获取行的读锁后,如果当前事务再需要对该记录进行更新操作,则很有可能造成死锁。
改变事务隔离级别。

2.6.4 InnoDB 默认是如何对待死锁的?

InnoDB 默认是使用设置死锁时间来让死锁超时的策略,默认 innodblockwait_timeout 设置的时长是 50s。

2.6.5 什么是全局锁?它的应用场景有哪些?

全局锁就是对整个数据库实例加锁,它的典型使用场景就是做全库逻辑备份。 这个命令可以使整个库处于只读状态。使用该命令之后,数据更新语句、数据定义语句、更新类事务的提交语句等操作都会被阻塞。

2.6.6 什么是共享锁

共享锁又称读锁 (read lock),是读取操作创建的锁。其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。当如果事务对读锁进行修改操作,很可能会造成死锁。

2.6.7 什么是排它锁?

排他锁 exclusive lock(也叫 writer lock)又称写锁。
若某个事物对某一行加上了排他锁,只能这个事务对其进行读写,在此事务结束之前,其他事务不能对其进行加任何锁,其他进程可以读取,不能进行写操作,需等待其释放

2.6.8 InnoDB 存储引擎有几种锁算法

Record Lock — 单个行记录上的锁;
Gap Lock — 间隙锁,锁定一个范围,不包括记录本身;
Next-Key Lock — 锁定一个范围,包括记录本身。

数据库的乐观锁和悲观锁。

悲观锁:
悲观锁她专一且缺乏安全感了,她的心只属于当前事务,每时每刻都担心着它心爱的数据可能被别的事务修改,所以一个事务拥有(获得)悲观锁后,其他任何事务都不能对数据进行修改啦,只能等待锁被释放才可以执行。
乐观锁:
乐观锁的“乐观情绪”体现在,它认为数据的变动不会太频繁。因此,它允许多个事务同时对数据进行变动。实现方式:乐观锁一般会使用版本号机制或CAS算法实现。

https://www.cnblogs.com/setalone/p/14851000.html