MySQL表锁,页锁,行锁,读写锁,全局锁,乐观锁,悲观锁深入了解

📅 2026/7/16 20:53:12
MySQL表锁,页锁,行锁,读写锁,全局锁,乐观锁,悲观锁深入了解
目录一. 锁的分类二. 读锁和写锁2.1 读锁(S锁)2.2 写锁(X锁)2.3 MySQL 8.0锁新特性三. 全局锁表锁页锁行锁(重点掌握)3.1 全局锁3.1.1 全局锁的使用场景3.1.2 全局锁的使用语法3.1.3 全局锁使用实示例3.1.4 全局锁的缺点3.2 表锁3.2.1 表级S锁X锁3.2.2 意向锁3.2.3 元数据锁(meta data lockMDL)3.2.4 自增锁(AUTO-INC锁)3.3 页锁3.4 行锁3.4.1 记录锁3.4.2 间隙锁3.4.3 临键锁3.4.4 插入意向锁四. 乐观锁与悲观锁4.1 乐观锁(Optimistic Locking)4.2 悲观锁(Pessimistic Locking)五. MySQL锁的升级一. 锁的分类MySQL 中的锁有很多种我们可以对它们进行分类分类的角度不同分类得出的结果也不相同下面我们就从分类的角度分析这些所的实现原理。从操作类型划分分为读锁和写锁从锁的粒度划分分为全局锁表锁页锁行锁从锁的态度划分分为乐观锁和悲观锁因为数据库常常在业务中会承受高并发的访问所以加锁的粒度越细并发能力越高所以对于全局锁表锁页锁行锁需要深刻理解。二. 读锁和写锁在数据库并发事务的时候就会产生读和写的各种操作.以两个事务为例就会有写-写写-读读-读这三种情况发生自然这些操作之间也会产生冲突。由此就有了读锁和写锁读锁(read lock)也被称为共享锁(Share Lock也叫S锁)写锁(write lock)也被称为排它锁(Exclusive Lock也叫X锁)来解决这种冲突情况的发生。2.1 读锁(S锁)顾名思义就是一个事务进行读取数据操作未对数据做修改所以多个获取读锁的事务可以共同读取同一份数据而且不会互相影响读锁也被称为共享锁读锁与读锁可以兼容数据库中加读锁有两种方式如下代码# 加读锁方式一SELECT ... LOCK IN SHARE MODE SELECT * FROM tb_user LOCK IN SHARE MODE; # 加读锁方式二SELECT ... FOR SHARE SELECT * FROM tb_user FOR SHARE;2.2 写锁(X锁)一个事务对数据进行写操作修改数据。如果没有写锁有另外一个事务来读取数据就有可能产生脏读所以没有写锁是会影响其他事务的操作结果的为了避片此种情况写锁就会阻塞其他事务的操作不管该事务要进行读操作还是写操作都会被阻塞因此写锁也被称为排它锁数据库中加写锁方式如下代码# 加写锁SELECT ... FOR UPDATE SELECT * FROM tb_user FOR UPDATE;2.3 MySQL 8.0锁新特性在 MySQL5.7 之前使用 SELECT ... FOR UPDATE 加写锁之后如果获取不到锁线程会一直等待直到超时在MySQL8.0 之后我们可以在 SELECT ... FOR SHARESELECT ... FOR UPDATE 后面添加 NOWAIT 或 SKIP LOCKED 跳过锁等待NOWAIT加了 NOWAIT 不会等待其他线程释放锁直接返回错误SKIP LOCKED返回不报错但返回的值中不包含被锁定的行数据返回数据不一定完整三. 全局锁表锁页锁行锁(重点掌握)全局锁表锁页锁行锁它们锁的粒度是由大到小的。锁的粒度越小并发能力越高。每一种锁它们的粒度不同使用场景也略有偏差。3.1 全局锁3.1.1 全局锁的使用场景全局锁通常应用于全库的逻辑备份此时为了保证数据的准确性我们需要对整个数据库进行加锁防止其它线程修改数据库中的数据导致我们备份的数据有偏差。在有全局锁的时候其他事务只能读取数据库中的数据不能写数据。3.1.2 全局锁的使用语法# 全局锁锁住整个数据库中的表 FLUSH TABLES WITH READ LOCK; # 解除全局锁 UNLOCK TABLES;3.1.3 全局锁使用实示例全局锁使用展示以 cloud_user 数据库为例对该数据库进行加锁执行如下OK表示加锁成功加完锁之后我们插入一条数据如下图所示显示无法插入成功下方提示我们执行 SELECT 查询操作就可以查询成功如下所示我们再将全局锁解除重新插入数据就可以插入成功了如下图所示然后我们刷新 tb_user 表张三那一条数据就已经被添加进去了3.1.4 全局锁的缺点通过上面的演示全局锁的缺点其实也是非常明显。1当使用全局锁进行数据备份时备份期间不能进行数据更新的操作会导致我们的业务暂停2数据库备份期间不能执行主库同步过来的二进制日志会导致主从延迟3.2 表锁表锁在 InnoDBMyISAM 等引擎中均有所使用是最基本的锁策略。由于表级锁每次操作会锁住整张表不让其他事务操作所以可以很好的避免死锁问题但并发能力也大大的降低。表锁是一个笼统的概念它里面还可以细分为S锁X锁元数据锁意向锁自增所三类。3.2.1 表级S锁X锁这里的S锁与X锁与上面分类说的读锁写锁非常类似只不过是在表层面上的S锁X锁不需要做过多详细的说明使用语法如下# 表级S锁X锁 lock tables tb_user READ/WRITE; # 释放锁 UNLOCK TABLES;读锁与写锁在并发上是有所区别的若我们添加的是读锁不会阻塞其他客户端的读操作但会阻塞其他客户端的写操作若我们添加的是写锁既会阻塞其他客户端的读操作又会阻塞其他客户端的写操作总结为下面这幅图。有一点需要特别注意在不同的存储引擎中表级S锁X锁的默认添加规则是不一样的以 InnoDB 和 MyISAM 引擎为例。MyISAM 存储引擎在执行SELECT 操作时会自动给表加上读锁在执行增删改操作时会自动给表加上写锁而 InnoDB 存储引擎不会给表添加读锁和写锁会添加粒度更细的行锁下面我会详细说到行锁。3.2.2 意向锁意向锁我直接解释可能会不好理解我给同学们说一个场景各位就懂了。假设现在有两个事务T1和T2同时操作用户user表。T1事务要给 user 表添加行锁(这里我还没有说到行锁如果不明白的可以先往下看行锁行锁懂了之后再回来看这个意向锁)T2事务要给user 表添加表锁。假设T1事务先执行它将 user 表中的某一行数据进行锁定当T2事务要给 user 表加上表锁的时候它需要先判断 user 表是否有其他事务添加过比表锁更细粒度的锁例如页锁和行锁。如果有添加过则T2事务添加表锁的操作就不能成功因为会产生冲突。而且T2事务判断是否有页锁或行锁的过程是有讲究的如果T1事务添加的是行锁那么T2事务在判断的时候它就要去进行全表扫描判断每个行数据是否添加过行锁如果这样做的话性能是非常非常差的。因此为了提高性能就有了意向锁这一说简单来说就是当T1事务添加了行锁之后可以理解为它会自动在行的上一级存储单元 页上或者整张表做一个标记标记该表中已经有细粒度的锁当有其他人想要添加表锁时不能添加成功要阻塞等待。因此说白了意向锁可以把它理解为一个标记不算是一个真正的锁。当某个表中有行锁时底层会自动给该表添加意向锁(即做一个标记表示有细粒度的锁正在操作该表)添加表锁会阻塞等待。主要就是为了解决添加表锁时全表扫描判断是否有行锁而设计的提高了表锁加锁时的性能。这个意向锁也可以分为IS和IX锁意向锁与意向锁之间都是兼容的因为它们本身可以说只是一个标记型锁。如果两个事务都是添加行锁那么都会添加意向锁由于意向锁是互相兼容的所以我们还需要进一步判断两个行锁操作的数据是否冲突。若不产生冲突则行锁可以添加成功若冲突则阻塞等待意向锁可以大致总结为以下两点1意向锁的存在是为了协调行锁和表锁的关系支持多粒度锁的并存2意向锁是一种不与行级锁冲突的表级锁它的作用就是对表做一个标记当有其他线程试图添加所的时候要先去做判断省去了全表扫描的时间3.2.3 元数据锁(meta data lockMDL)元数据锁是在 MySQL5.5 之后引入的也是属于表锁的范畴之内的。它的主要目的是为了保证表的结构的准确性怎么理解这句话呢假设有两个事务一个事务进行查询操作另一个事务要改变表的结构那么它们之间就可能产生冲突导致查询操作查询到的数据不准确为了避免发生此类问题就引入了元数据锁。当对一张表进行增删改查的时候会加元数据读锁当对表的结构进行变更操作的时候会加元数据写锁。 元数据读锁和元数据写锁是互斥的当我们正在进行表结构的变更时其他事务是不能进行查询操作的会阻塞等待同理当有事务在进行查询操作的时候其他事务不能进行表结构的变更操作也会阻塞等待。不过数据库表的结构在业务涉及的初期基本就已经需要确定好但是也不能避免此种情况的发生所以元数据锁的存在还是有一定的使用场景的。MDL加锁过程是系统自动控制的无法显式使用在访问同一张表的时候会自动添加元数据锁。元数据锁的主要作用是维护表元数据的数据一致性元数据读锁与元数据读锁之间是兼容的元数据读写锁之间是互斥的。3.2.4 自增锁(AUTO-INC锁)自增锁其实很简单这里作为简单了解即可数据库的自增长我们应该都是清楚的。有些时候我们会为主键设置一个自增长自增锁可以说就是服务于自增长这一属性的当有多个事务同时对同一张表执行INSERT插入操作时或者进行批量插入数据操作时我们的自增锁就会起作用让它们逐个添加防止发生线程安全问题。3.3 页锁了解数据库底层数据存储结构的同学都知道一张表可以存储成百上千万条数据而数据库底层是以页为单位的一页大小为16KB所以一张数据库的表有可能会有很多很多页的数据。页锁是介于表锁和行锁之间的一种锁锁定的资源比行多比表少并发能力一般。而且页锁可能会产生死锁问题假设 user 表现在有两页数据事务A需要先处理第一页的数据再处理第二页的数据事务B需要先处理第二页的数据再处理第一页的数据那么事务A就会先锁定第一页事务B就会先锁定第二页处理完成之后事务A等待事务B释放第二页的锁事务B等待事务A释放第一页的锁双方互相等待就会造成死锁问题。在实际开发过程中如果我们选择的是 InnoDB 存储引擎通常会选用行锁提高并发能力如果选择的是 MyISAM 存储引擎通常会选用默认的表锁即可所以页锁很少使用作为了解即可面试中页锁一般也不是重点会去问的。3.4 行锁行锁听名字也能知道它每次上锁只会锁住某一行的数据这里需要注意的是MySQL的行锁没有在服务器层实现只有在存储引擎中得以实现而在数据库中多存储引擎中只有 InnoDB 存储引擎支持行锁。行锁的优点锁的粒度非常小锁冲突的概率也很小数据库并发能力极高。行锁的缺点行锁的开销非常大加锁会比较慢容易出现死锁的情况。行锁其实还可以继续细分分为记录锁间隙锁临键锁插入意向锁。3.4.1 记录锁如下图所示是一个简单的 user 表记录锁中的记录说的就是表中的一条条记录当我们的事务操作某一条记录的时候可以获取它的读锁(S锁)或者写锁(X锁)的若是读事务获取的就是读锁写事务获取的就是写锁。行级别的读锁(S锁)与读锁(S锁)可以兼容读锁(S锁)与写锁(X锁)互斥不兼容写锁(X锁)与写锁(X锁)也互斥不兼容。3.4.2 间隙锁间隙锁的主要目的是为了解决并发情况下产生的幻读问题比如事务A来读取数据事务B插入数据事务A前后两次读取到的数据不一致第二次读取到了事务B插入之后的数据产生了幻读。因此我们就需要用到间隙锁。如下图所示假设我们要在id 3和id 8之间添加一条数据这两条数据之间就是有间隙的我们需要对这个间隙进行加锁防止其他事务添加数据导致产生幻读。假设我们要操作8王五二班这条数据那么当我们想要在李四和王五之间插入数据或想要在王五和赵六之间插入数据时是不能插入成功的间隙锁默认锁的是当前数据与他相邻的两侧的两条数据之间的间隙锁定开区间内的部分记录当我们操作3李四一班时无法在id 1和 id 8之间插入数据因为有间隙锁当我们操作15赵六二班时无法在 id 8和 id 20之间插入数据因为有间隙锁当我们操作20找七三班时无法在 id 15 和 正无穷之间插入数据如果操作的是最大的那条数据右侧区间就是正无穷当我们操作的是最小的那条数据左侧区间就是系统给默认的最小值虽然间隙锁也有共享锁和排它锁之分但它们起到的作用可以说是完全相同的 如果一个事务对一条数据加了间隙锁并不会影响其它事务对这条记录继续添加间隙锁。3.4.3 临键锁临键锁其实就是记录锁和间隙锁的结合体也是 InnoDB 存储引擎默认的锁它的隔离级别为可重复读除了解决不了幻读问题可以解决脏写脏读不可重复读三种并发情况。当我们操作8王五二班这条数据时其他事务不仅无法操作当前这条数据也无法在 id 3 和 id 15 之间插入数据。3.4.4 插入意向锁刚才我说到了间隙锁其实插入意向锁是一种极为特殊的间隙锁当我们想要向数据库的表中添加一条数据时会先判断要插入的位置是否已经被就添加了间隙锁。如果有插入操作就需要等待直到间隙锁的事务提交将锁释放才能进行插入在InnoDB 存储引擎中规定事务在等待的时候也需要在内存中生成一个锁结构表明有事务想要在某个位置添加数据但目前处于等待状态InnoDB 就把这种状态下的锁称为插入意向锁插入意向锁是一种极为特殊的间隙锁在执行 INSERT 操作时产生与表锁中的意向锁完全不是一个东西不要混肴。假设现在 user 表中已经有id 4和id 7的两条记录两个事务分别要插入id 5和id 6的两条记录时两个事务除了要获取5和6两行数据的排它锁之外还要获取对应的间隙锁又因为要添加的数据并不冲突所以两个事务可以同时运行不会产生阻塞。所以也就是说插入意向锁之间并不互相排斥所以就算有多个事务要同时进行插入操作他们的插入间隙锁也相同但只要它们插入的主键值不同就能同时进行插入操作而且不会互相阻塞。四. 乐观锁与悲观锁乐观锁和悲观锁从名字上就大致能看得出在面对数据并发的思维方式与态度。这里需要注意的是乐观锁和并悲观本身并不是真正的锁而是锁的设计思想。4.1 乐观锁(Optimistic Locking)乐观锁认为对同一数据的并发操作访问不会总是发生属于小概率事件对于修改数据操作成功的结果比较乐观。乐观锁在修改数据时不会上锁但是会在更新数据时判断一下在此期间是否有其他人修改过当前数据它不会去利用数据库自身的锁机制而是靠程序员写代码的方式来实现的。在程序上通常会利用版本号机制或CAS自旋的方式来实现乐观锁。乐观锁通常适用于读操作较多的业务中这样可以提高数据的吞吐量。它的优点是程序实现不存在死锁问题。下面我来简单的说一下版本号的方式实现乐观锁。通常在程序中实现乐观锁我们会在对应要操作的表中添加一个版本号字段每当一个线程去操作表中的某条数据或某张表后就让该版本号字段自增一的操作。在并发情况之下假设有一个线程来操作数据读数据的时候它会获取版本号在即将要进行操作数据的时候它会再获取一次版本号判断两次获取的版本号是否相同若相同说明在此期间没有其他线程来修改数据若不同说明在此期间有其它线程修改过数据那么它就要放弃执行到一半的本次操作重新执行此次操作一直循环直到操作执行成功。实现乐观锁的方式有很多种除了版本号的方式也可以使用时间戳的方式4.2 悲观锁(Pessimistic Locking)悲观锁是一种设计思想就是很悲观对于数据被其他事务修改持有保守态度会通过数据库自身的锁机制来实现从而保证数据操作的排他性。悲观锁总是假设最坏的情况每次去拿数据的时候都会认为别人会修改数据。为了不让别人修改所以在每次拿数据的时候都会去上锁别人再想要拿数据的时候就会被阻塞直到它已经完成了对数据的操作并将锁释放。悲观锁适合于写操作较多的业务场景下防止两个写操作事务产生冲突。悲观锁说白了有点像 Java 里面的 Synchronized 它会将共享资源每次只给一个线程进行使用其他线程会阻塞当前面的线程用完之后将锁释放才能让别的线程去操作。数据库中的读锁写锁行锁表锁都是在操作之前先进行上锁其他线程想要操作受到阻塞。五. MySQL锁的升级在数据库中虽然有这么多中锁但每个层级的锁数量都是有限制的因为锁也会占用内存空间所空间的大小是有限制的。当某个层级的锁超过了这个层级锁对应的阈值之后底层就会进行一个锁升级的过程就是用更大粒度的锁代替小粒度的锁比如在 InnoDB 存储引擎中行锁就有升级为表锁的过程升级所的好处就是锁占用的空间变小了但同时带来的影响就是数据并发能力降低了。