MySQL中的锁事

打印 上一主题 下一主题

主题 1751|帖子 1751|积分 5253

一、概述

锁是盘算机在实行多线程或线程时用于并发访问同一共享资源时的同步机制,MySQL中的锁是在服务器层大概存储引擎层实现的,保证了数据访问的一致性与有效性。
事件的隔离性是由的锁来实现。
二、MySQL并发事件访问的问题

我们已经知道事件并发实行时大概带来的各种问题,最大的一个难点是:一方面要最大水平地利用数据库的并发访问,另外一方面还要确保每个用户能以一致的方式读取和修改数据,尤其是一个事件举行读取操作,另一个同时举行改动操作的环境下。
一个事件举行读取操作,另一个举行改动操作,我们前边说过,这种环境下大概发生脏读、不可重复读、幻读的问题。怎么解决脏读、不可重复读、幻读这些问题呢?其实有两种可选的解决方案:
方案一:读操作MVCC,写操作举行加锁

事件利用MVCC举行的读取操作称之为一致性读,大概一致性无锁读,也称之为快照读,但是每每读取的是历史版本数据。全部普通的SELECT语句(plain SELECT)在READ COMMITTED、REPEATABLE READ隔离级别下都算是一致性读。
一致性读并不会对表中的任何记录做加锁操作,其他事件可以自由的对表中的记录做改动。
   普通的SELECT语句在READ COMMITTED和REPEATABLE READ隔离级别下会使用到MVCC读取记录。
  

  • 在READ COMMITTED 隔离级别下,一个事件在实行过程中每次实行SELECT操作时都会生成一个ReadView,ReadView的存在本身就保证了事件不可以读取到未提交的事件所做的更改,也就是制止了脏读现象;
  • 在REPEATABLE READ 隔离级别下,一个事件在实行过程中只有第一次实行SELECT操作才会生成一个ReadView,之后的SELECT操作都复用这个ReadView,如许也就制止了不可重复读和幻读的问题。
  方案二:读、写操作都采用加锁的方式

适用场景:
业务场景不允许读取记录的旧版本,而是每次都必须去读取记录的最新版本,
比方在银行存款的事件中,你需要先把账户的余额读出来,然后将其加上本次存款的数额,最后再写到数据库中。在将账户余额读取出来后,就不想让别的事件再访问该余额,直到本次存款事件实行完成,其他事件才可以访问账户的余额。如许在读取记录的时候也就需要对其举行加锁操作,如许也就意味着读操作和写操作也像写-写操作那样排队实行。
小结对比发现:


  • 采用MVCC 方式的话, 读-写操作相互并不冲突, 性能更高。
  • 采用加锁方式的话, 读-写操作相互需要排队实行,影响性能。
很显着,采用MVCC方式的话,读-写操作相互并不冲突,性能更高采用加锁方式的话,读-写操作相互需要排队实行,影响性能。一般环境下我们固然乐意采用MVCC来解决读-写操作并发实行的问题,但是业务在某些情下,要求必须采用加锁的方式实行。下属的全部内容讲MySQL的锁事:
三、锁的不同角度的分类


3.1、从数据操作的范例划分

共享锁(也称为读锁,英文名:Shared Locks,简称S锁):在事件要读取一条记录时,需要先获取该记录的S锁。如果事件E1起首获取了一条记录的S锁之后,事件E2接着也要访问这条记录:如果事件E2想要再获取一个记录的S锁,那么事件E2也会获得该锁,也就意味着事件E1和E2在该记录上同时持有S锁。
排他锁(也称为独占锁,写锁,英文名:Exclusive Locks,简称X锁):在事件要改动一条记录时,需要先获取该记录的X锁。如果事件E2想要再获取一个记录的X锁,那么此操作会被壅闭,直到事件E1提交之后将S锁开释掉。如果事件E1起首获取了一条记录的X锁之后,那么不管事件E2接着想获取该记录的S锁还是X锁都会被壅闭,直到事件E1提交。
以是我们说S锁和S锁是兼容的,S锁和X锁是不兼容的,X锁和X锁也是不兼容的,画个表表现一下就是如许:
X 不兼容X 不兼容S
S 不兼容X 兼容S
数据环境准备:
  1. CREATE DATABASE test_db;
  2. USE test_db;
  3. CREATE TABLE products
  4. (
  5.     id    INT AUTO_INCREMENT PRIMARY KEY,
  6.     name  VARCHAR(100),
  7.     price DECIMAL(10, 2)
  8. );
  9. -- 随机插入一些数据
  10. INSERT INTO products (name, price)
  11. VALUES ('Product A', 10.99),
  12.        ('Product B', 20.49),
  13.        ('Product C', 15.75);
复制代码
演示共享锁(S锁)
  1. -- 事务E1:获取S锁
  2. START TRANSACTION;
复制代码
  1. SELECT * FROM products WHERE id = 1 LOCK IN SHARE MODE;
复制代码

  1. -- 事务E2:再次获取S锁
  2. START TRANSACTION;
复制代码
  1. SELECT * FROM products WHERE id = 1 LOCK IN SHARE MODE;
复制代码

此时,两个事件都可以读取 id = 1 的记录,因为S锁是互相兼容的。
  1. COMMIT;
复制代码


演示排他锁(X锁)
接下来,演示排他锁(X锁)。
  1. -- 事务E1:获取X锁
  2. START TRANSACTION;
复制代码
  1. SELECT * FROM products WHERE id = 1 FOR UPDATE;
复制代码

  1. -- 事务E2:尝试获取S锁或X锁
  2. START TRANSACTION;
复制代码
  1. -- 这个操作会被壅闭SELECT * FROM products WHERE id = 1 LOCK IN SHARE MODE;
复制代码
  1. -- 大概:这个操作也会被壅闭SELECT * FROM products WHERE id = 1 FOR UPDATE;
复制代码

当事件E1已经获取了 id = 1 的X锁时,任何其他事件E2尝试获取该记录的S锁或X锁时都会被壅闭,直到事件E1提交或回滚。
提交或回滚事件
当E1事件提交或回滚后,E2事件才能继续举行。
  1. -- 事务E1提交
  2. COMMIT;
复制代码


  1. -- 事务E2现在可以继续
  2. COMMIT;
复制代码

3.2、从数据操作的粒度划分:表级锁

MySQL支持多种存储引擎,不同存储引擎对锁的支持也是不一样的。固然,我们重点还是讨论InnoDB存储引擎中的锁,其他的存储引擎只是稍微看看。
InnoDB存储引擎既支持表锁,也支持行锁。表锁实现简朴,占用资源较少,不过粒度很粗,有时候你仅仅需要锁住几条记录,但使用表锁的话相当于为表中的全部记录都加锁,以是性能比力差。行锁粒度更细,可以实现更精准的并发控制。下边我们具体看一下。
3.2.1、表级别的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锁大家相识一下就可以了。
生存中的实例:
为了更好的理解这个表级别的S锁和X锁和背面的意向锁,我们举一个现实生存中的例子。我们用曾经很火爆的互联网风口项目共享Office来分析加锁:
共享Office有栋大楼,楼自然有许多层。办公室都是共享的,客户可以随便选办公室办公。每层楼可以容纳客户同时办公,每当一个客户进去办公,就相当于在每层的入口处挂了一把S锁,如果许多客户进去办公,相当于每层的入口处挂了许多把S锁(类似行级别的S锁)。
有的时候楼层会举行检修,比方说换地板,换天花板,检查水电啥的,这些维修项目并不能同时开展。如果楼层针对某个项目举行检修,就不允许客户来办公,也不允许其他维修项目举行,此时相当于楼层门口会挂一把X锁(类似行级别的X锁)。
上边提到的这两种锁都是针对楼层而言的,不过有时候我们会有一些特殊的需求:

A、有投资人要来观察Office的环境。
投资人和公司并不想影响客户进去办公,但是此时不能有楼层举行检修,以是可以在大楼门口放置一把S锁(类似表级别的S锁)。此时:
来办公的客户们看到大楼门口有S锁,可以继续进入大楼办公。
修理工看到大楼门口有S锁,则先在大楼门口等着,啥时候投资人走了,把大楼的S锁撤掉再进入大楼维修。
B、公司要和房东谈条件。
此时不允许大楼中有正在办公的楼层,也不允许对楼层举行维修。以是可以在大楼门口放置一把X锁(类似表级别的X锁)。此时:
来办公的客户们看到大楼门口有X锁,则需要在大楼门口等着,啥时候条件谈好,把大楼的X锁撤掉再进入大楼办公。
修理工看到大楼门口有X锁,则先在大楼门口等着,啥时候谈判结束,把大楼的X锁撤掉再进入大楼维修。
3.2.2、意向锁 (intention lock)

但是在上面的例子这里头有两个问题:
如果我们想对大楼团体上S锁,起首需要确保大楼中的没有正在维修的楼层,如果有正在维修的楼层,需要等到维修结束才可以对大楼团体上S锁。
如果我们想对大楼团体上X锁,起首需要确保大楼中的没有办公的楼层以及正在维修的楼层,如果有办公的楼层大概正在维修的楼层,需要等到全部办公的同学都办公离开,以及维修工维修完楼层离开后才可以对大楼团体上X锁。
我们在对大楼团体上锁(表锁)时,怎么知道大楼中有没有楼层已经被上锁(行锁)了呢?依次检查每一楼层门口有没有上锁?那这服从也太慢了吧!于是InnoDB提出了一种意向锁(英文名:Intention Locks):
意向共享锁 ,英文名:Intention Shared Lock,简称IS锁。当事件准备在某条记录上加S锁时,需要先在表级别加一个IS锁。
意向独占锁 ,英文名:Intention Exclusive Lock,简称IX锁。当事件准备在某条记录上加X锁时,需要先在表级别加一个IX锁。
视角回到大楼和楼层上来:

如果有客户到楼层中办公,那么他先在整栋大楼门口放一把IS锁(表级锁),然后再到楼层门口放一把S锁(行锁)。
如果有维修工到楼层中维修,那么它先在整栋大楼门口放一把IX锁(表级锁),然后再到楼层门口放一把X锁(行锁)。
之后:
如果有投资人要观光大楼,也就是想在大楼门口前放S锁(表锁)时,起首要看一下大楼门口有没有IX锁,如果有,意味着有楼层在维修,需要等到维修结束把IX锁撤掉后才可以在整栋大楼上加S锁。
如果有谈条件要占用大楼,也就是想在大楼门口前放X锁(表锁)时,起首要看一下大楼门口有没有IS锁或IX锁,如果有,意味着有楼层在办公大概维修,需要等到客户们办完公以及维修结束把IS锁和IX锁撤掉后才可以在整栋大楼上加X锁。
留意: 客户在大楼门口加IS锁时,是不关心大楼门口是否有IX锁的,维修工在大楼门口加IX锁时,是不关心大楼门口是否有IS锁大概其他IX锁的。IS和IX锁只是为了判定当前时间大楼里有没有被占用的楼层用的,也就是在对大楼加S锁大概X锁时才会用到。
下面我们具体的来讲述意向锁:
概念
意向锁是表锁,为了和谐行锁和表锁的关系,支持多粒度(表锁与行锁)的锁并存。
InnoDB 支持多粒度锁(multiple granularity locking) ,它允许行级锁与表级锁共存,而意向锁就是其中的一种表锁。
意向锁分为两种:


  • 意向共享锁(intention shared lock, IS):事件故意向对表中的某些行加共享锁(S锁)
    1. -- 事务要获取某些行的 S 锁,必须先获得表的 IS 锁。
    2. SELECT column FROM table ... LOCK IN SHARE MODE;
    复制代码
  • 意向排他锁(intention exclusive lock, IX):事件故意向对表中的某些行加排他锁(X锁)
    1. -- 事务要获取某些行的 X 锁,必须先获得表的 IX 锁。
    2. SELECT column FROM table ... FOR UPDATE;
    复制代码
即:意向锁是由存储引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享 / 排他锁之前InooDB 会先获取该数据行所在数据表的对应意向锁。
作用
当有事件A有行锁时,MySQL会自动为该表添加意向锁,事件B如果想申请整个表的写锁,那么不需要遍历每一行判定是否存在行锁,而直接判定是否存在意向锁,增强性能。
为什么意向锁是表级锁
当我们需要加一个排他锁时,需要根据意向锁去判定表中有没有数据行被锁定(行锁);
(1)如果意向锁是行锁,则需要遍历每一行数据去确认;
(2)如果意向锁是表锁,则只需要判定一次即可知道有没数据行被锁定,提升性能。
意向锁怎么支持表锁和行锁并存?
(1)起首明确并存的概念是指数据库同时支持表、行锁,而不是任何环境都支持一个表中同时有一个事件A持有行锁、又有一个事件B持有表锁,因为表一旦被上了一个表级的写锁,肯定不能再上一个行级的锁。
(2)如果事件A对某一行上锁,其他事件就不大概修改这一行。这与“事件B锁住整个表就能修改表中的任意一行”形成了冲突。以是,没故意向锁的时候,让行锁与表锁共存,就会带来许多问题。于是有了意向锁的出现,如前面所言,数据库不需要在检查每一行数据是否有锁,而是直接判定一次意向锁是否存在即可,能提升许多性能。
意向锁的兼容互斥性
意向共享锁(IS)意向排他锁(IX)共享锁(S)兼容互斥排他锁(X)互斥互斥 锁的组合性:(意向锁没有行锁
组合性XIXSIS表锁有有有有行锁有有 总结:
当我们在对使用InnoDB存储引擎的表的某些记录加S锁之前,那就需要先在表级别加一个IS锁,当我们在对使用InnoDB存储引擎的表的某些记录加X锁之前,那就需要先在表级别加一个IX锁。
IS锁和IX锁的使命只是为了后续在加表级别的S锁和X锁时判定表中是否有已经被加锁的记录,以制止用遍历的方式来查看表中有没有上锁的记录。我们并不能手动添加意向锁,只能由InnoDB存储引擎自行添加。
3.2.3、自增锁(AUTO-INC锁)

在使用MySQL过程中,我们可以为表的某个列添加AUTO_INCREMENT 属性。举例:
  1. CREATE TABLE `teacher`
  2. (
  3.     `id`   int          NOT NULL AUTO_INCREMENT,
  4.     `name` varchar(255) NOT NULL,
  5.     PRIMARY KEY (`id`)
  6. ) ENGINE = InnoDB
  7.   DEFAULT CHARSET = utf8mb4
  8.   COLLATE = utf8mb4_0900_ai_ci;
复制代码
由于这个表的id字段声明白AUTO_INCREMENT,意味着在誊写插入语句时不需要为其赋值,SQL语句修改如下所示:
  1. INSERT INTO `teacher` (name)
  2. VALUES ('zhangsan'),
  3.        ('lisi');
复制代码
  1. select * from teacher;
复制代码

现在我们看到的上面插入数据只是一种简朴的插入模式,全部插入数据的方式总共分为三类,分别是“ Simple inserts ”,“ Bulk inserts ”和“ Mixed-mode inserts ”。

  • “Simple inserts” (简朴插入)
    可以预先确定要插入的行数(当语句被初始处置处罚时)的语句。包括没有嵌套子查询的单行和多行INSERT…VALUES() 和REPLACE 语句。比如我们上面举的例子就属于该类插入,已经确定要插入的行
    数。
  • “Bulk inserts” (批量插入)
    事先不知道要插入的行数(和所需自动递增值的数量)的语句。比如INSERT … SELECT , REPLACE… SELECT 和LOAD DATA 语句,但不包括纯INSERT。 InnoDB在每处置处罚一行,为AUTO_INCREMENT列分配一个新值。
  • “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 。
InnoDB提供了一个称之为innodb_autoinc_lock_mode的系统变量来控制到底使用上述两种方式中的哪种来为AUTO_INCREMENT修饰的列举行赋值,当innodb_autoinc_lock_mode值为0时,同等采用AUTO-INC锁;当innodb_autoinc_lock_mode值为1时,同等采用轻量级锁;当innodb_autoinc_lock_mode值为2时,两种方式混着来(也就是在插入记录数量确定时采用轻量级锁,不确定时使用AUTO-INC锁)。
不过当innodb_autoinc_lock_mode值为2时,大概会造成不同事件中的插入语句为AUTO_INCREMENT修饰的列生成的值是交叉的,在有主从复制的场景中是不安全的。
具体的举行分析一下:
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(“连续”锁定模式)
在 MySQL 8.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(“交错”锁定模式)
从 MySQL 8.0 开始,交错锁模式是默认设置。
在此锁定模式下,自动递增值保证在全部并发实行的全部范例的insert语句中是唯一且单调递增的。但是,由于多个语句可以同时生成数字(即,跨语句交叉编号),为任何给定语句插入的行生成的值大概不是连续的。
  1. show variables like 'innodb_autoinc_lock_mode' ;
复制代码

MySQL5.7.X中缺省为1。

3.2.4、元数据锁(MDL锁)

MySQL5.5引入了meta data lock,简称MDL锁,属于表锁范畴。MDL 的作用是,保证读写的正确性。比如,如果一个查询正在遍历一个表中的数据,而实行期间另一个线程对这个表布局做变动,增长了一列,那么查询线程拿到的结果跟表布局对不上,肯定是不可的。
因此,当对一个表做增删改查操作的时候,加 MDL读锁;当要对表做布局变动操作的时候,加 MDL 写锁。
3.2.5、表锁的总结

表级锁是MySQL中最基本的锁策略,是MySQL最早采用的锁策略。表级锁的特点是开销小,加锁快,不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
在MySQL中,对MyISAM表的读操作,会自动加上读锁,对MyISAM表的写操作,会自动加上写锁。InnoDB引擎在须要环境下会使用表锁,但重要是使用行锁来实现多版本并发控制(MVCC),它能提供更好的并发性能和更少的锁冲突。
总的来说,表锁适用于读操作多、写操作少的应用,当并发争用不是特别猛烈,以及记录级锁并发控制开销大于访问冲突开销的环境。在并发度高,大概写操作较多的环境下,表锁大概会成为瓶颈。
表级锁有哪些使用场景

  • 读麋集型应用:如果你的应用重要举行读取操作,很少举行写入操作,那么使用表级锁大概是一个好选择。因为表级读锁不会壅闭其他的读锁,以是这种场景下表级锁能够提供很高的性能。
  • 写操作不频仍的场景:表级锁对写操作的处置处罚并不高效,因为一个写锁会壅闭全部其他的锁,无论它们是读锁还是写锁。但是,如果你的应用不需要频仍地举行写操作,大概可以容忍写操作的耽误,那么使用表级锁大概是可行的。
  • 数据量不大的简朴应用:如果数据库的数据量不大,那么即使在写操作中,由于锁定整张表,对性能的影响也不大。
  • 全表更新大概删除:在某些环境下,大概需要对一张表举行全表的更新大概删除操作,例如,删除表中的全部记录,大概更新表中全部记录的某个字段的值。在这种环境下,使用表级锁是符合的。
但要留意,虽然表级锁的开销较小,但由于其锁定粒度大,大概会导致并发度下降,特别是在写操作较多大概并发度较高的场景下。以是,如果应用的并发度较高,大概需要频仍举行写操作,那么大概需要考虑使用更精致粒度的锁,如行锁。
MySQL哪些命令会发生表级锁

  • ALTER TABLE:这个命令用于更改表的布局,如添加列、删除列、改变列的范例等。实行这个命令的时候,MySQL需要锁定整个表以防止在更改过程中有新的数据写入。
  • DROP TABLE 和 TRUNCATE TABLE:这两个命令都会导致表级锁。DROP TABLE命令会删除整个表,而TRUNCATE TABLE命令会删除表中的全部数据。在实行这些命令的时候,MySQL需要锁定整个表以防止在删除过程中有新的数据写入。
  • LOCK TABLES:这个命令可以显式地为一个或多个表加上读锁或写锁。LOCK TABLES命令背面可以跟上一系列的表名和锁模式,用来指定需要锁定哪些表,以及使用什么样的锁模式。例如,LOCK TABLES t1 WRITE, t2 READ;命令会给表t1加上写锁,给表t2加上读锁。
  • 全表扫描或大范围扫描:对于MyISAM存储引擎,全表扫描或大范围扫描会触发表级锁。
  • FLUSH TABLES WITH READ LOCK(FTWRL):这个命令可以给全部表加上全局读锁,其他会话在此期间不能对数据举行修改。
请留意,InnoDB存储引擎重要使用行级锁,并在一些环境下使用表级锁,比如在实行某些ALTER TABLE命令大概LOCK TABLES命令时。MyISAM存储引擎只支持表级锁。
MySQL表锁风险点

  • 性能下降:因为表锁会锁定整个表,以是在高并发的环境中,它大概导致大量的哀求壅闭,从而低落性能。对于读取和写入混合麋集的负载,表锁大概会成为一个性能瓶颈。
  • 并发性能差:表锁的最大问题在于其并发性能。一旦一个线程对表获得了写锁,其他线程的任何读写操作都会被壅闭,直到写锁被开释。同样的,如果一个读锁被持有,那么其他的写操作将被壅闭。这就使得并发性能大大低落。
  • 大概导致锁等待和超时:在高并发的环境中,由于表级锁的粒度较大,大概会有许多线程在等待锁,如果等待的时间过长,大概会导致锁超时,进一步影响应用的性能和可用性。
  • 写操作影响大:如果一个长时间运行的写操作(比如大数据量的UPDATE大概INSERT语句)获取了写锁,那么会壅闭全部其他的读操作和写操作,直到这个写操作完成。
  • 死锁的大概性:虽然表锁本身不会出现死锁,但在多表操作中,如果没有按照肯定的次序获得锁,大概会导致死锁。
为了制止这些问题,我们通常会选择InnoDB存储引擎,它重要使用行级锁,可以提供更好的并发性能,而且在肯定水平上减少了锁争用的问题。而且,InnoDB还支持事件,可以保证数据的一致性和完整性。在实际应用中,我们应该根据具体的业务需求和系统负载,选择符合的存储引擎和锁策略。
3.3、从数据操作的粒度划分:行锁

mysql数据库 innoDB存储引擎 锁的是索引
锁的算法

t2 这张表 id 有一个主键索引。我们插入了 4 行数据,主键 id 分别是 1、4、7、10。
我们这里的划分标准是主键 id。
这些数据库内里存在的主键值,我们把它叫做 Record,记录,那么这里我们就有 4 个 Record。
根据主键,这些存在的 Record 隔开的数据不存在的区间,我们把它叫做 Gap,间隙,它是一个左开右开的区间。
假设我们有 N 个 Record,那么全部的数据会被划分成多少个 Gap 区间?答案是 N+1,就像我们把一条绳子砍 N 刀,它最后肯定是变成 N+1 段。
最后一个,间隙(Gap)连同它左边的记录(Record),我们把它叫做临键的区间,它是一个左开右闭的区间。
如果主键索引不是整型,是字符怎么办呢?字符可以排序吗? 基于 ASCII 码
记录锁
第一种环境,当我们对于唯一性的索引(包括唯一索引和主键索引)使用等值查询,精准匹配到一
条记录的时候,这个时候使用的就是记录锁。
比如 where id = 1 4 7 10 。
间隙锁
第二种环境,当我们查询的记录不存在,无论是用等值查询还是范围查询的时候,它使用的都是间隙锁。
临键锁
第三种环境,当我们使用了范围查询,不仅仅掷中了 Record 记录,还包罗了 Gap 间隙,在这种环境下我们使用的就是临键锁,它是 MySQL 内里默认的行锁算法,相当于记录锁加上间隙锁。
比如我们使用>5 <9 , 它包罗了不存在的区间,也包罗了一个 Record 7。
锁住最后一个 key 的下一个左开右闭的区间。
select * from t2 where id >5 and id <=7 for update; 锁住(4,7]和(7,10]
select * from t2 where id >8 and id <=10 for update; 锁住 (7,10],(10,+∞)**
总结:为什么要锁住下一个左开右闭的区间?——就是为相识决幻读的问题。
下面来具体讲述这些锁:
3.3.1、记录锁(Record Locks)

记录锁(Record Lock)是MySQL数据库中InnoDB存储引擎的一种锁定机制,重要用于锁定和控制对单个行记录的访问。记录锁是在索引记录上设置的,对于表没有主键或唯一索引的表,InnoDB会生成一个隐藏的聚簇索引,并在这个隐藏索引上加锁。
在实际操作中,记录锁通常会在举行数据查询、更新或删除等操作时自动被数据库引擎应用。例如,当实行以下查询时,MySQL会在Orders表的OrderID为1的行上设置记录锁:
  1. SELECT * FROM Orders WHERE OrderID = 1 FOR UPDATE;
复制代码
记录锁也就是仅仅把一条记录锁上,官方的范例名称为: LOCK_REC_NOT_GAP 。比如我们把id值为8的那条记录加一个记录锁的示意图如图所示。仅仅是锁住了id值为8的记录,对四周的数据没有影响。

记录锁是有S锁和X锁之分的,称之为S型记录锁和X型记录锁。


  • 当一个事件获取了一条记录的S型记录锁后,其他事件也可以继续获取该记录的S型记录锁,但不可以继续获取X型记录锁;
  • 当一个事件获取了一条记录的X型记录锁后,其他事件既不可以继续获取该记录的S型记录锁,也不可以继续获取X型记录锁。
记录锁有什么使用场景?
记录锁(Record Lock)在数据库的许多常见场景中都会用到,以下是几个重要的使用场景:

  • 单行数据修改:当你需要修改一行数据而且希望在修改期间防止其他事件修改同一行数据时,可以使用记录锁。例如,如果你正在更新一个订单状态,你大概不希望其他事件在更新过程中修改这个订单。
    1. UPDATE Orders SET Status = 'Processed' WHERE OrderID = 1;
    复制代码
  • 单行数据查询:在某些环境下,你大概需要查询一行数据,而且希望在查询过程中防止其他事件修改这行数据。例如,如果你正在读取一个订单的状态并做出相应的操作,你大概希望在读取过程中订单的状态不被改变。
    1. SELECT * FROM Orders WHERE OrderID = 1 FOR UPDATE;
    复制代码
  • 保持数据一致性:在某些需要保证数据一致性的场景中,也需要使用记录锁。例如,在转账操作中,你大概需要在两个账户之间转移资金,这需要对两个账户的余额举行修改。在修改期间,你大概希望防止其他事件修改这两个账户的余额,以防止数据不一致。
以上例子中的UPDATE和SELECT … FOR UPDATE语句在实行时会自动获取记录锁。在操作完成或事件提交(或回滚)后,数据库会自动开释记录锁,无需手动操作。
创建数据库与表布局
  1. -- 创建数据库
  2. CREATE DATABASE IF NOT EXISTS demo_db;
  3. USE demo_db;
  4. -- 创建示例表
  5. CREATE TABLE student
  6. (
  7.     id    INT PRIMARY KEY,
  8.     name  VARCHAR(50),
  9.     class VARCHAR(50)
  10. );
  11. -- 插入随机数据
  12. INSERT INTO student (id, name, class)
  13. VALUES (1, '张三', '一班'),
  14.        (3, '李四', '一班'),
  15.        (8, '王五', '二班'),
  16.        (15, '赵六', '二班'),
  17.        (20, '钱七', '三班');
复制代码
演示记录锁
为了演示记录锁,假设我们有两个会话(事件)。在第一个事件中,我们获取一条记录的锁,然后在第二个事件中尝试访问相同的记录。
  1. -- 事务1:获取id为8的记录的X锁
  2. START TRANSACTION;
复制代码
  1. SELECT * FROM student WHERE id = 8 FOR UPDATE;
复制代码

在此时开启另一个会话(事件2)并实行以下操作:
  1. -- 事务2:尝试获取id为8的记录的X锁(会被阻塞,直到事务1提交)
  2. START TRANSACTION;
复制代码
  1. SELECT * FROM student WHERE id = 8 FOR UPDATE;
复制代码

回到事件1,可以选择提交或回滚事件,然后事件2将解除壅闭:
  1. -- 事件1提交或回滚COMMIT;
  2.   -- 或 ROLLBACK;
复制代码


3.3.2、间隙锁(Gap Locks)

间隙锁(Gap Locks)是MySQL InnoDB存储引擎提供的一种锁定机制。它锁定的不是具体的行记录,而是两个索引之间的间隙(大概说区间),如许可以防止新的记录插入到该间隙,确保数据的一致性和事件的隔离性。
间隙锁经常与记录锁(Record Locks)一起使用,共同形成Next-Key锁,保护索引记录的范围查询和扫描操作。
以下是间隙锁的重要范例:

  • 区间-区间间隙锁:锁定两个索引键之间的间隙,大概是第一个索引键之前的间隙。
  • 区间-记录间隙锁:锁定一个索引键和一个记录之间的间隙。
  • 记录-区间间隙锁:锁定一个记录和一个索引键之间的间隙。
MySQL 在REPEATABLE READ 隔离级别下是可以解决幻读问题的,解决方案有两种,可以使用MVCC 方案解决,也可以采用加锁方案解决。但是在使用加锁方案解决时有个大问题,就是事件在第一次实行读取操作时,那些幻影记录尚不存在,我们无法给这些幻影记录加上记录锁。InnoDB提出了一种称之为Gap Locks 的锁,官方的范例名称为: LOCK_GAP ,我们可以简称为gap锁。比如,把id值为8的那条记录加一个gap锁的示意图如下。

图中id值为8的记录加了gap锁,意味着不允许别的事件在id值为8的记录前边的间隙插入新记录,其实就是id列的值(3, 8)这个区间的新记录是不允许立即插入的。比如,有另外一个事件再想插入一条id值为4的新记录,它定位到该条新记录的下一条记录的id值为8,而这条记录上又有一个gap锁,以是就会壅闭插入操作,直到拥有这个gap锁的事件提交了之后,id列的值在区间(3, 8)中的新记录才可以被插入。
gap锁的提出仅仅是为了防止插入幻影记录而提出的。
间隙锁有哪些使用场景
间隙锁(Gap Locks)在MySQL数据库的InnoDB存储引擎中重要用于以下场景:

  • 防止幻读:间隙锁的重要目标是防止其他事件在已经锁定的范围内插入新的行。这可以制止"幻读"问题,即一个事件在读取某个范围内的全部行时,另一个事件插入了一个新行,当第一个事件再次读取该范围时,会发现有一个"幻影"行。
  • 范围查询:在实行范围查询时,如果事件需要对查询结果举行更新或删除,那么间隙锁可以保证在事件实行期间,不会有新的行插入到查询范围中。
例如,以下事件会在Orders表的OrderID列值在1到100之间的全部行上设置排他锁,并在这些行的间隙上设置间隙锁:
  1. START TRANSACTION;
  2. SELECT * FROM Orders WHERE OrderID BETWEEN 1 AND 100 FOR UPDATE;
  3. COMMIT;
复制代码

  • 防止死锁:在某些环境下,间隙锁可以帮助防止死锁。如果没有间隙锁,那么两个事件大概都会试图在同一位置插入一个新行,导致相互等待对方开释锁,从而形成死锁。
需要留意的是,间隙锁在可重复读(REPEATABLE READ)和序列化(SERIALIZABLE)这两个隔离级别下才会使用,在读已提交(READ COMMITTED)和读未提交(READ UNCOMMITTED)这两个隔离级别下,InnoDB不会使用间隙锁。
间隙锁有什么缺点?
间隙锁(Gap Locks)是MySQL的InnoDB存储引擎用于防止幻读问题的一种锁定机制,虽然它在某些场景下非常有效,但也存在一些潜伏的缺点,包括:

  • 性能影响:间隙锁会阻止其他事件在已经锁定的范围内插入新的行,这大概会影响到数据库的并发性能,尤其在需要大量插入操作的高并发场景下。
  • 死锁风险:虽然间隙锁可以在某些环境下防止死锁,但在其他环境下,它大概会增长死锁的风险。比如,两个事件都想在同一间隙中插入新的行,就大概发生死锁。
  • 复杂性:理解间隙锁及其对事件的影响大概需要相当深入的数据库知识,尤其是在处置处罚并发问题和调优数据库性能时。
  • 锁定范围大概过大:间隙锁锁定的是索引之间的间隙,这大概会比实际需要锁定的行要多。如果一个事件需要锁定的只是表中的一小部门行,但由于间隙锁的存在,大概会锁定更大范围的数据,导致不须要的锁定冲突。
请留意,以上所述的缺点重要取决于具体的使用场景和工作负载,有时候,为了保持数据的一致性和防止并发问题,这些缺点大概是可以担当的。
创建数据库与表布局
  1. -- 创建数据库
  2. CREATE DATABASE IF NOT EXISTS demo_gap_lock_db;
  3. USE demo_gap_lock_db;
  4. -- 创建示例表
  5. CREATE TABLE student
  6. (
  7.     id    INT PRIMARY KEY,
  8.     name  VARCHAR(50),
  9.     class VARCHAR(50)
  10. );
  11. -- 插入随机数据
  12. INSERT INTO student (id, name, class)
  13. VALUES (1, '张三', '一班'),
  14.        (3, '李四', '一班'),
  15.        (8, '王五', '二班'),
  16.        (15, '赵六', '二班'),
  17.        (20, '钱七', '三班');
复制代码
1. 设置事件隔离级别为REPEATABLE READ
  1. -- 设置事务隔离级别为REPEATABLE READ
  2. SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
复制代码


2. 事件1:对id为8的记录加锁
  1. -- 事务1:对id为8的记录前的间隙加锁(Gap Lock)
  2. START TRANSACTION;
复制代码
  1. SELECT * FROM student WHERE id = 8 FOR UPDATE;
复制代码

此时,MySQL会在id为8的记录上加上Gap Lock,锁住id为3和id为8之间的间隙,防止其他事件在此间隙中插入新记录。
3. 事件2:尝试在间隙中插入新记录
在另一个会话(事件2)中尝试插入一个新记录:
  1. -- 事务2:尝试在id值为3和8之间插入新记录(会被阻塞)
  2. START TRANSACTION;
复制代码
  1. INSERT INTO student (id, name, class) VALUES (4, '新生', '一班');
复制代码
由于事件1在id为3和id为8之间加了Gap Lock,事件2的插入操作会被壅闭(抱负环境下),直到事件1提交或回滚。

没有出现间隙锁的缘故起因:
小范围内的数据量:如果表的数据量非常小,MySQL有时大概不会对某些范围自动使用Gap Lock。尝试增长数据量或测试多个区间。
索引的使用:Gap Lock一般会出现在使用索引的查询上。如果上述查询没有引发Gap Lock,大概是因为MySQL没有效到索引。确保id列是主键或具有索引。
独立的会话:确保事件1和事件2在不同的会话中运行,而且在同一个会话中,不要同时运行这些事件。
4.提交或回滚事件1
  1. -- 事件1:提交或回滚COMMIT;
  2.   -- 或 ROLLBACK;
复制代码
在事件1提交或回滚后,事件2将被解除壅闭(抱负环境),可以完成插入操作。
3.3.3、临键锁(Next-Key Locks)

有时候我们既想锁住某条记录,又想阻止其他事件在该记录前边的间隙插入新记录,以是InnoDB就提出了一种称之为Next-Key Locks 的锁,官方的范例名称为: LOCK_ORDINARY ,我们也可以简称为next-key锁。Next-Key Locks是在存储引擎innodb 、事件级别在可重复读的环境下使用的数据库锁,innodb默认的锁就是Next-Key locks。
Next-Key 可以理解为一种特殊的间隙锁,也可以理解为一种特殊的算法。通过临建锁可以解决幻读的问题。 每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事件持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。需要夸大的一点是,InnoDB 中行级锁是基于索引实现的,临键锁只与非唯一索引列有关,在唯一索引列(包括主键列)上不存在临键锁。
idagename110张三324李四532王五745赵六 该表中 age 列潜伏的临键锁有:(-∞, 10],(10, 24],(24, 32],(32, 45],(45, +∞], 临键锁有哪些使用场景?
临键锁(Next-Key Lock)在MySQL数据库的InnoDB存储引擎中重要用于以下场景:

  • 防止幻读:幻读是指在一个事件实行过程中,同样的查询操作返回了不同的结果集,这是由于其他事件在这两次查询操作之间插入了新的记录。临键锁可以防止在锁定范围内插入新的行,从而制止了幻读问题。
  • 范围查询和修改:在实行范围查询和修改操作时,临键锁可以确保数据的一致性。比如,当你在一个事件中实行了如下查询,而且哀求了一个排他锁:
  1. SELECT * FROM Orders WHERE OrderID BETWEEN 100 AND 200 FOR UPDATE;
复制代码
在这种环境下,InnoDB会对OrderID值在100到200之间的全部记录加上行锁,而且对这个范围内的间隙加上间隙锁,合在一起就形成了临键锁。如许,直到事件结束,其他事件都不能在这个范围内插入新的记录。
防止死锁:临键锁可以在某些环境下防止死锁。比如,两个事件都想在同一间隙中插入新的行,由于临键锁的存在,这大概防止了死锁的发生。
需要留意的是,临键锁只在可重复读(REPEATABLE READ)和序列化(SERIALIZABLE)这两个隔离级别下使用,在读已提交(READ COMMITTED)和读未提交(READ UNCOMMITTED)这两个隔离级别下,InnoDB不会使用临键锁。
临键锁有什么缺点?
临键锁(Next-Key Lock)是一种强盛的锁定机制,可以帮助保持数据库的一致性并防止幻读(phantom reads)。然而,它也有一些潜伏的缺点,包括:

  • 性能影响:临键锁可以防止其他事件在已经锁定的范围内插入新的行。这种锁定级别大概会影响到数据库的并发性能,尤其是在高并发的插入操作中。
  • 过度锁定:临键锁大概导致比实际需要锁定的行更多的数据被锁定。因为临键锁不仅锁定了数据行,还锁定了索引间隙,这大概会导致更多的数据被锁定,从而影响到其他事件。
  • 复杂性:理解临键锁及其对数据库操作的影响大概需要相当深入的数据库知识,特别是在处置处罚并发问题和调优数据库性能时。
  • 死锁风险:虽然临键锁可以在某些环境下防止死锁,但在其他环境下,它大概会增长死锁的风险。比如,如果两个事件都试图在同一个间隙中插入新行,就大概发生死锁。
请留意,这些潜伏的缺点重要取决于具体的使用场景和工作负载,有时候,为了保持数据的一致性和防止并发问题,这些缺点大概是可以担当的。
3.3.4、插入意向锁(Insert Intention Locks)

我们说一个事件在插入一条记录时需要判定一下插入位置是不是被别的事件加了gap锁( next-key锁也包罗gap锁),如果有的话,插入操作需要等待,直到拥有gap锁的那个事件提交。但是InnoDB规定事件在等待的时候也需要在内存中生成一个锁布局,表明有事件想在某个间隙中插入新记录,但是现在在等待。InnoDB就把这种范例的锁命名为Insert Intention Locks ,官方的范例名称为:LOCK_INSERT_INTENTION ,我们称为插入意向锁。插入意向锁是一种Gap锁,不是意向锁,在insert操作时产生。
插入意向锁是在插入一条记录行前,由 INSERT 操作产生的一种间隙锁。
事实上插入意向锁并不会阻止别的事件继续获取该记录上任何范例的锁。
3.3.5、行锁的总结

行级锁是MySQL中的一种锁定机制,它可以对数据库表中的单独一行举行锁定。相比于表级锁和页锁,行级锁的粒度更小,因此在处置处罚高并发事件时,能提供更好的并发性能和更少的锁冲突。然而,行级锁也需要更多的内存和CPU资源,因为需要对每一行都举行管理。
在MySQL中,行级锁重要由InnoDB存储引擎提供。InnoDB支持两种范例的行级锁:共享锁(S锁)和排他锁(X锁)。在实际使用中,InnoDB还提供了一种名为“间隙锁”(Gap Lock)的特性。间隙锁不仅锁定一个具体的行,还锁定它前后的“间隙”,即这一行之前的行和之后的行之间的空间。间隙锁可以防止其他事件插入新的行到已锁定行的前后,从而可以解决一些并发问题。
值得留意的是,行级锁只在事件中有效,也就是说,只有在一个事件开始(BEGIN)后并在事件提交(COMMIT)或回滚(ROLLBACK)之前,才能对数据行举行锁定。如果在非事件环境中实行SQL语句,那么InnoDB会在语句实行结束后立即开释全部的锁。
MySQL行锁有哪些使用场景
MySQL中的行级锁(Row Level Locks)通常在以下几种场景中被使用:

  • 高并发读写操作:在需要高并发读写操作的场景中,行级锁可以提高性能和并发性,因为它允许多个事件并发地操作不同的行。
  • 单行操作:对于需要操作单行数据的SQL语句(例如基于主键大概唯一索引的UPDATE、DELETE和INSERT语句),行级锁可以提供较好的并发性和性能。
  • 短期锁:在需要对数据行举行短时间锁定的环境下,行级锁可以防止长时间壅闭其他事件。
  • 实现并发控制:在需要确保数据一致性和隔离性的事件中,行级锁是实现并发控制的重要机制。
  • 复杂的事件处置处罚:在需要对多行数据举行复杂处置处罚的事件中,可以使用行级锁来锁定这些行,防止在事件处置处罚过程中数据被其他事件修改。
使用行级锁需要留意,由于行级锁的锁定粒度较小,它大概会消耗更多的系统资源(例如内存和CPU),特别是在处置处罚大量数据时。此外,使用行级锁也大概导致死锁,需要使用符合的策略来制止死锁,例如在事件中按照肯定的次序锁定行。
MySQL那些命令会导致发生行锁?
在MySQL中,重要是InnoDB存储引擎提供了行级锁(Row Level Locking)。一般来说,以下这些范例的操作会导致InnoDB对数据行举行加锁:


  • SELECT … FOR UPDATE:这种查询会对选定的行添加一个排他锁(X锁),这意味着其他事件不能修改这些行,也不能对这些行添加共享锁。
  • SELECT … LOCK IN SHARE MODE:这种查询会对选定的行添加一个共享锁(S锁),这意味着其他事件不能修改这些行,但可以对这些行添加共享锁。
  • INSERT:插入操作会对新添加的行添加一个排他锁(X锁)。
  • UPDATE:更新操作会对被更新的行添加一个排他锁(X锁)。
  • DELETE:删除操作会对被删除的行添加一个排他锁(X锁)。
这些加锁操作都是在事件中举行的,即只有在事件开始(BEGIN)后并在事件提交(COMMIT)或回滚(ROLLBACK)之前,才会对数据行举行加锁。如果在非事件环境中实行上述SQL语句,那么InnoDB会在语句实行结束后立即开释全部的锁。
请留意,加锁的粒度和范围取决于WHERE子句中用到的索引。如果WHERE子句中用到了唯一索引(例如主键索引),那么InnoDB只会锁定匹配的行。如果没有效到唯一索引,那么InnoDB大概会锁定更多的行,乃至是整个表,这就大概导致锁冲突和性能问题。
此外,InnoDB还支持间隙锁(Gap Locks)和临键锁(Next-Key Locks),这两种锁都可以在某些环境下提供更好的并发控制。
MySQL行锁有什么风险点?
尽管行级锁(Row-Level Locking)可以提供高并发性并减少锁冲突,但在使用过程中也大概遇到一些风险和问题,重要包括以下几点:

  • 死锁:当两个或更多的事件相互等待对方开释资源时,就会发生死锁。例如,事件1锁定了行A并试图锁定行B,同时事件2锁定了行B并试图锁定行A,这就形成了死锁。MySQL会检测到死锁并终止其中一个事件,但这仍大概导致性能问题和事件失败。
  • 锁升级:如果一个事件试图锁定的行过多,InnoDB大概会将锁从行级升级为表级,这就大概导致更多的锁冲突。
  • 锁等待:如果一个事件已经锁定了某行,其他试图访问这行的事件就必须等待,这大概导致性能下降。如果有大量的事件在等待锁,就大概导致系统出现性能瓶颈。
  • 资源消耗:行级锁需要更多的内存来存储锁信息,而且需要更多的CPU时间来处置处罚锁哀求和开释锁。如果数据库中的行数非常多,大概并发事件的数量非常多,这大概会导致显着的资源消耗。
  • 难以调试和排查:由于行级锁的粒度较小,如果出现性能问题或锁冲突,大概需要复杂的调试和排查工作来找出问题的缘故起因。
  • 事件隔离级别:不同的事件隔离级别会影响锁的行为和性能,大概需要根据具体的应用场景来调解事件隔离级别。
为了制止上述问题,需要公道地计划数据库表和索引,公道地编写SQL语句,公道地管理事件,以及公道地设置事件隔离级别。
3.4、从数据操作的粒度划分:页级锁

页锁就是在页的粒度上举行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录。当我们使用页锁的时候,会出现数据浪费的现象,但如许的浪费最多也就是一个页上的数据行。页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。
每个层级的锁数量是有限制的,因为锁会占用内存空间, 锁空间的大小是有限的。当某个层级的锁数量凌驾了这个层级的阈值时,就会举行锁升级。锁升级就是用更大粒度的锁更换多个更小粒度的锁,比如InnoDB 中行锁升级为表锁,如许做的好处是占用的锁空间低落了,但同时数据的并发度也下降了。
3.5、从对待锁的态度划分:乐观锁、悲观锁

悲观锁
正如其名,它指的是对数据被外界(包括本系统当前的其他事件,以及来自外部系统的事件处置处罚)修改持保守态度,因此,在整个数据处置处罚过程中,将数据处于锁定状态。悲观锁的实现,每每依赖数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
在悲观锁的环境下,为了保证事件的隔离性,就需要一致性锁定读。读取数据时给加锁,别的事件无法修改这些数据。修改删除数据时也要加锁,别的事件无法读取这些数据。
乐观锁
相对悲观锁而言,乐观锁机制接纳了更加宽松的加锁机制。悲观锁大多数环境下依赖数据库的锁机制实现,以保证操作最大水平的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事件而言,如许的开销每每无法承受。
而乐观锁机制在肯定水平上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增长一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增长一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息举行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
3.6、按加锁的方式划分:显式锁、隐式锁

3.6.1、隐式锁

**情景一:**对于聚簇索引记录来说,有一个trx_id 隐藏列,该隐藏列记录着最后改动该记录的事件id 。那么如果在当前事件中新插入一条聚簇索引记录后,该记录的trx_id 隐藏列代表的的就是当前事件的事件id ,如果其他事件此时想对该记录添加S锁大概X锁时,起首会看一下该记录的trx_id 隐藏列代表的事件是否是当前的活跃事件,如果是的话,那么就帮助当前事件创建一个X锁(也就是为当前事件创建一个锁布局, is_waiting 属性是false ),然后自己进入等待状态(也就是为自己也创建一个锁布局, is_waiting 属性是true )。
**情景二:**对于二级索引记录来说,本身并没有trx_id 隐藏列,但是在二级索引页面的PageHeader 部门有一个PAGE_MAX_TRX_ID 属性,该属性代表对该页面做改动的最大的事件id ,如果PAGE_MAX_TRX_ID 属性值小于当前最小的活跃事件id ,那么分析对该页面做修改的事件都已经提交了,否则就需要在页面中定位到对应的二级索引记录,然后回表找到它对应的聚簇索引记录,然后再重复情景一的做法。
数据库环境准备:
  1. CREATE DATABASE locking_demo;
  2. USE locking_demo;
  3. CREATE TABLE student
  4. (
  5.     id    INT PRIMARY KEY,
  6.     name  VARCHAR(50),
  7.     class VARCHAR(20)
  8. ) ENGINE = InnoDB;
  9. CREATE TABLE grades
  10. (
  11.     id         INT PRIMARY KEY,
  12.     student_id INT,
  13.     subject    VARCHAR(50),
  14.     grade      CHAR(1),
  15.     FOREIGN KEY (student_id) REFERENCES student (id)
  16. ) ENGINE = InnoDB;
  17. INSERT INTO student (id, name, class)
  18. VALUES (1, 'Alice', 'Class A'),
  19.        (2, 'Bob', 'Class A'),
  20.        (3, 'Charlie', 'Class B'),
  21.        (4, 'David', 'Class B');
  22. INSERT INTO grades (id, student_id, subject, grade)
  23. VALUES (1, 1, 'Math', 'A'),
  24.        (2, 2, 'Math', 'B'),
  25.        (3, 3, 'Math', 'C'),
  26.        (4, 4, 'Math', 'D');
复制代码
场景一:聚簇索引中的隐式锁

  • Session 1: 在第一个事件中插入一个新记录,该记录会自动获得一个隐式的X锁。
  1. BEGIN;
复制代码
  1. INSERT INTO student VALUES(5, 'Eve', 'Class C');
  2. -- 事务保持未提交状态
复制代码


  • Session 2: 在第二个事件中,尝试查询全部记录并添加共享锁 (LOCK IN SHARE MODE)。由于 Eve 的记录持有隐式X锁,查询将被壅闭。
  1. BEGIN;
复制代码
  1. SELECT * FROM student LOCK IN SHARE MODE;
  2. -- 这将被阻塞,直到Session 1提交或回滚
复制代码

场景二:二级索引中的隐式锁

  • Session 1: 在第一个事件中更新 grades 表中的一条记录。这会更新 PAGE_MAX_TRX_ID,并锁住对应的二级索引记录。
  1. BEGIN;
复制代码
  1. UPDATE grades SET grade = 'A' WHERE student_id = 2;
  2. -- 事务保持未提交状态
复制代码


  • Session 2: 在第二个事件中,尝试对 grades 表举行共享锁查询。由于二级索引记录锁被占用,这个查询将被壅闭。
  1. BEGIN;
复制代码
  1. SELECT * FROM grades WHERE student_id = 2 LOCK IN SHARE MODE;
  2. -- 这将被阻塞,直到Session 1提交或回滚
复制代码



查看锁信息
使用 performance_schema.data_lock_waits 来查看锁的具体信息:
  1. SELECT * FROM performance_schema.data_lock_waits\G;
复制代码
隐式锁的逻辑过程如下:


  • A. InnoDB的每条记录中都一个隐含的trx_id字段,这个字段存在于聚簇索引的B+Tree中。
  • B. 在操作一条记录前,起首根据记录中的trx_id检查该事件是否是活动的事件(未提交或回滚)。如果是活
  • 动的事件,起首将隐式锁转换为显式锁(就是为该事件添加一个锁)。
  • C. 检查是否有锁冲突,如果有冲突,创建锁,并设置为waiting状态。如果没有冲突不加锁,跳到E。
  • D. 等待加锁乐成,被唤醒,大概超时。
  • E. 写数据,并将自己的trx_id写入trx_id字段。
3.6.2、显式锁

通过特定的语句举行加锁,我们一般称之为表现加锁,例如:
表现加共享锁:
  1. select .... lock in share mode
复制代码
表现加排它锁:
  1. select .... for update
复制代码
3.7、别的锁

3.7.1、全局锁

全局锁就是对整个数据库实例加锁。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被壅闭:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表布局等)和更新类事件的提交语句。全局锁的范例使用场景是:做全库逻辑备份。
全局锁的命令:
  1. Flush tables with read lock
复制代码
MySQL全局锁的范例使用场景是,举行一些需要确保整个数据库一致性的操作,例如全库备份、全库导出等。
在MySQL中,可以使用FLUSH TABLES WITH READ LOCK(FTWRL)语句来添加全局读锁,这将阻止其他线程举行更新操作。使用UNLOCK TABLES语句来开释锁定。
请留意,全局锁的开销非常大,因为它会阻止其他全部的数据修改操作,而且在高并发环境下大概导致大量的线程等待锁定。因此,你应该尽量制止在生产环境中使用全局锁,大概尽量减少全局锁的持有时间。
虽然全局锁有其应用场景,但是过度使用或不正确使用全局锁大概导致性能问题。因此,根据应用的特性和需求选择得当的锁策略是很重要的。对于大多数应用,优先使用更精致粒度的锁,如行锁和表锁,可以更有效地处置处罚并发哀求,同时制止全局锁的开销。

全局锁有哪些应用场景

  • 备份全库:使用全局锁可以确保在备份过程中,数据库的全部表都保持一致的状态。例如,可以使用FLUSH TABLES WITH READ LOCK命令在备份期间阻止任何写操作,以确保备份数据的一致性。
  • 团体数据迁徙:如果你需要将整个数据库从一个服务器迁徙到另一个服务器,那么在迁徙过程中,你大概希望阻止任何写操作,以确保全部的数据都被正确地迁徙到新的服务器。
  • 全库只读:在某些环境下,你大概希望将整个数据库设置为只读模式。例如,如果你在举行一些大概粉碎数据完整性的操作,大概在举行系统维护时,大概需要将数据库设置为只读模式。在这种环境下,可以使用全局读锁
请留意,全局锁会对整个数据库举行锁定,大概会导致大量的哀求壅闭,对性能影响很大。因此,尽管全局锁在某些环境下大概有效,但通常应当尽量制止在需要高并发处置处罚本领的生产环境中使用全局锁。尤其是在处置处罚大量并发写哀求的应用中,过度使用全局锁大概会导致严重的性能问题。
3.7.2、死锁

3.7.2.1、概念

是指两个或两个以上的历程在实行过程中,由于竞争资源大概由于相互通讯而造成的一种壅闭的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
举个例子:A和B去按摩洗脚,都想在洗脚的时候,同时顺便做个头部按摩,13技师擅长足底按摩,14擅长头部按摩。
这个时候A先抢到14,B先抢到13,两个人都想同时洗脚和头部按摩,于是就互不相让,扬言我死也不让你,如许的话,A抢到14,想要13,B抢到13,想要14,在这个想同时洗脚和头部按摩的事情上A和B就产生了死锁。怎么解决这个问题呢?
第一种,如果这个时候,来了个15,刚好也是擅长头部按摩的,A又没有两个脑壳,自然就归了B,于是B就美滋滋的洗脚和做头部按摩,剩下A在旁边气鼓鼓的,这个时候死锁这种环境就被冲破了,不存在了。
第二种,C出场了,用武力强迫A和B,必须先做洗脚,再头部按摩,这种环境下,A和B谁先抢到13,谁就可以举行下去,另外一个没抢到的,就等着,这种环境下,也不会产生死锁。
以是总结一下:
1、死锁是必然发生在多操作者(M>=2个)环境下,争夺多个资源(N>=2个,且N<=M)才会发生这种环境。很显着,单线程自然不会有死锁,只有B一个去,不要2个,打十个都没问题;单资源呢?只有13,A和B也只会产生猛烈竞争,打得不可开交,谁抢到就是谁的,但不会产生死锁。同时,死锁还有几个要求,1、争夺资源的次序不对,如果争夺资源的次序是一样的,也不会产生死锁;
2、争夺者拿到资源不放手。
3.7.2.2、MySQL中的死锁

MySQL中的死锁的成因是一样的。
数据库环境与准备:
起首,创建一个名为 school_db 的数据库,并在其中创建一张 teacher 表,用于存储教师的编号和姓名。
  1. CREATE DATABASE school_db;
  2. USE school_db;
  3. CREATE TABLE teacher
  4. (
  5.     number INT PRIMARY KEY,
  6.     name   VARCHAR(50)
  7. );
  8. -- 随机插入一些数据
  9. INSERT INTO teacher (number, name)
  10. VALUES (1, 'Teacher A'),
  11.        (2, 'Teacher B'),
  12.        (3, 'Teacher C'),
  13.        (4, 'Teacher D');
复制代码
模仿死锁
接下来,我们通过两个会话来模仿死锁的产生。假设有两个事件,分别在不同的会话中操作。
  1. -- 事务1:锁定 number=1 的记录
  2. START TRANSACTION;
复制代码
  1. SELECT * FROM teacher WHERE number = 1 FOR UPDATE;
复制代码

  1. -- 事务2:锁定 number=3 的记录
  2. START TRANSACTION;
复制代码
  1. SELECT * FROM teacher WHERE number = 3 FOR UPDATE;
复制代码

此时,会话1锁定了 number=1 的记录,会话2锁定了 number=3 的记录。
  1. -- 事务1:尝试锁定 number=3 的记录(此操作将被阻塞)
  2. SELECT * FROM teacher WHERE number = 3 FOR UPDATE;
复制代码

  1. -- 事务2:尝试锁定 number=1 的记录(此操作将导致死锁)
  2. SELECT * FROM teacher WHERE number = 1 FOR UPDATE;
复制代码


在这个场景中,由于会话1已经锁定了 number=1 的记录,而会话2锁定了 number=3 的记录,因此当它们各自尝试获取对方已经持有的锁时,便产生了死锁。MySQL会自动检测到这一死锁,并选择其中一个事件回滚,以冲破死锁。
检查死锁环境
在会话2中,MySQL会检测到死锁,并终止其中一个事件(通常是会话2)。这时会话1中的壅闭操作会继续实行。
要查看死锁的具体环境,可以使用以下命令:
  1. SHOW ENGINE INNODB STATUS\G
复制代码
此命令将输出当前InnoDB存储引擎的状态,包括死锁的具体信息。
在实行 SHOW ENGINE INNODB STATUS\G
后,在 LATEST DETECTED DEADLOCK 部门找到关于死锁的具体信息。具体而言:

  • LATEST DETECTED DEADLOCK: 这是死锁的最新检测信息。此部门会表现最后一次检测到的死锁变乱。
  • TRANSACTION: 这里列出了导致死锁的事件,包括它们的活动状态、持有的锁以及等待的锁。

    • HOLDS THE LOCK(S): 这部门信息表现当前事件持有的锁。
    • WAITING FOR THIS LOCK TO BE GRANTED: 这部门表现当前事件在等待的锁。

  • WE ROLL BACK TRANSACTION: 这个部门会分析MySQL决定回滚哪个事件来解除死锁。


关键部门如下:


  • LATEST DETECTED DEADLOCK 标志了死锁发生的时间:
    1. LATEST DETECTED DEADLOCK
    2. 2024-08-13 08:15:02 140591473407744
    复制代码
  • TRANSACTION 84777TRANSACTION 84778 是导致死锁的两个事件。具体信息包括:

    • 事件 84777 持有 number = 1 的锁,并等待 number = 3 的锁。
    • 事件 84778 持有 number = 3 的锁,并等待 number = 1 的锁。







  • 最终,MySQL 决定回滚 TRANSACTION 84778 来解除死锁:
    1. *** WE ROLL BACK TRANSACTION (2)
    复制代码


查看事件加锁环境
为了更好地理解事件加锁的环境,可以开启 innodb_status_output_locks 变量来查看哪些锁被哪些事件持有:
  1. SHOW VARIABLES LIKE 'innodb_status_output_locks';
复制代码
  1. -- 将其设置为ON
  2. SET GLOBAL innodb_status_output_locks = ON;
复制代码
3.8、其他存储引擎中的锁

对于MyISAM、MEMORY、MERGE这些存储引擎来说,它们只支持表级锁,而且这些引擎并不支持事件,以是使用这些存储引擎的锁一般都是针对当前会话来说的。比方说在Session 1中对一个表实行SELECT操作,就相当于为这个表加了一个表级别的S锁,如果在SELECT操作未完成时,Session 2中对这个表实行UPDATE操作,相当于要获取表的X锁,此操作会被壅闭,直到Session 1中的SELECT操作完成,开释掉表级别的S锁后,Session 2中对这个表实行UPDATE操作才能继续获取X锁,然后实行具体的更新语句。
因为使用MyISAM、MEMORY、MERGE这些存储引擎的表在同一时刻只允许一个会话对表举行写操作,以是这些存储引擎实际上最好用在只读,大概大部门都是读操作,大概单用户的情景下。
另外,在MyISAM存储引擎中有一个称之为Concurrent Inserts的特性,支持在对MyISAM表读取时同时插入记录,如许可以提升一些插入速率。关于更多Concurrent Inserts的细节,详情可以参考文档。
分析:



免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

天空闲话

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表