mysql锁(全局锁、表锁、行锁、页锁、排他锁、共享锁)

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: mysql锁(全局锁、表锁、行锁、页锁、排他锁、共享锁)

mysql锁

简介


数据库锁定机制简单来说,就是数据库为了保证数据的一致性,而使各种共享资源在被并发访问变得有序所设计的一种规则。


MySQL数据库由于其自身架构的特点,存在多种数据存储引擎,每种存储引擎的锁定机制都是为各自所面对的特定场景而优化设计,所以各存储引擎的锁定机制也有较大区别。


根据加锁的范围,MySQL里面的锁大致可以分成全局锁表级锁 页锁行锁四类。

微信图片_20230511001502.png

全局锁

概念


全局锁就是对整个数据库实例加锁。当我们对数据库加了读锁之后,其他任何的请求都不能对数据库加写锁了,当我们对数据库加了写锁之后,后续其他任何的请求都不能对数据库加读锁和写锁了。


全局读锁(FTWRL)

MySQL提供了一个加全局读锁的方法,命令是 Flush tables with read lock 。当需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句


应用场景


全局锁的典型使用场景是,做全库逻辑备份。也就是把整库每个表都select出来存成文本.也就是全局锁只有在进行主从备份数据或者导入导出数据的时候才会使用到。


那么为什么需要全局锁呢?


因为我们在做数据备份或者导入导出数据的时候,如果在这个期间还可以同时进行数据的增删改,那么就会出现数据不一致的问题。


以前有一种做法是通过上面提到的 FTWRL 确保在备份的时候不会有其他线程对数据库做更新,注意:这里备份过程中整个库都是完全处于只读状态。


但是让整个库都只读,可能出现以下问题:


  • 如果在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆
  • 如果在从库上备份,那么在备份期间从库不能执行主库同步过来的binlog,会导致主从延迟


如何避免加锁


既然加全局锁影响这么大,我们能不能避免加锁呢?


通过上面的介绍,我们知道加锁是为了解决数据不一致问题。那么是不是只要我们能解决数据不一致的问题,就可以不用加全局锁了。有这样一个思路:如果我们在开始进行数据备份的时候,记录一个操作日志,备份过程中不加锁允许对数据库的增删改查,而在备份过程中,增删改查的操作记录都记到一个日志文件里,等我们备份完成后,再把这段时间日志文件里的操作都执行一遍。这样就能保证备份前后数据的一致性了。


总结,不加锁的话,备份得到数据和主数据不是一个逻辑时间点,这个视图是逻辑不一致的。如果保证逻辑时间点一致即逻辑视图一致就能保证数据一致,由此我们就想到了事务隔离级别,可重复复读的隔离级别下开启一个事务就是一个一致性视图。


在 MySQL 的默认引擎 InnoDB 里有一个机制可以保证数据一致性。InnoDB 引擎中有数据快照版本的功能,这个功能叫 MVCC,因为 MVCC 保留了历史版本的快照,每个快照都对应一个事务版本号,而在我们备份数据的时候会申请一个事务版本号,在读取数据的时候,只需要读取比自己事务版本号小的数据即可。


–single-transaction 命令加锁


官方自带的逻辑备份工具是 mysqldump。当 mysqldump 使用参数 –single-transaction 的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于 MVCC 的支持,这个过程中数据是可以正常更新的。


–single-transaction 参数的作用,设置事务的隔离级别为可重复读,即 REPEATABLE READ,这样能保证在一个事务中所有相同的查询读取到同样的数据,也就大概保证了在 dump 期间,如果其他 InnoDB 引擎的线程修改了表的数据并提交,对该 dump 线程的数据并无影响。


并且设置 WITH CONSISTENT SNAPSHOT 为快照级别。设想一下,如果只是可重复读,那么在事务开始时还没 dump 数据时,这时其他线程修改并提交了数据,那么这时第一次查询得到的结果是其他线程提交后的结果,而 WITH CONSISTENT SNAPSHOT 能够保证在事务开启的时候,第一次查询的结果就是事务开始时的数据 A,即使这时其他线程将其数据修改为 B,查的结果依然是 A。


single-transaction方法只适用于所有的表使用事务引擎的库。在 mysqldump 过程中,加了–single-transaction 就能保证 InnoDB 的数据是完全一致的,对于MyISAM这种不支持事务的引擎,如果备份过程中有更新,总是只能取到最新的数据,那么就破坏了备份的一致性。这时候就还是需要全局锁的,所以我们就还是需要使用 FTWRL 命令的。


只读设置


我们可能还会有这样一个疑问,既然要全库只读,我们为什么不适用 set global readonly = true 的方式呢?


确实 readonly 方式也可以让全库进入只读状态,但还是会建议用 FTWRL 方式,主要有两个原因:

在有些系统,readonly 的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库。因此,修改global变量的方式影响面更大。


在异常处理机制上有差异。如果执行 FTWRL 命令之后由于客户端发生异常断开,那么 MySQL 会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为 readonly 之后,如果客户端发生异常,则数据库就会一直保持 readonly 状态,这样会导致整个库长时间处于不可写状态,风险较高。


表级锁


MySQL里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)


表锁

概念

表锁就是锁一整张表,在表被锁定期间,其他事务不能对该表进行操作,必须等当前表的锁被释放后才能进行操作。表锁响应的是非索引字段,即全表扫描,全表扫描时锁定整张表。索引字段对行锁才起作用。


语法


语法结构:lock table 表名字1 read(write),表名字2 read(write);

比如有俩表 t1、t2,给 t1 加读锁,给 t2 加写锁


示例: lock table t1 read,t2 write;


手动释放表锁:unlock tables;

也可以在客户端断开的时候自动释放。另外,lock tables 语法除了会限制别的线程读写外,还会限制本线程接下来的操作。


如果在某个线程A中执行lock tables t1 read,t2 wirte;这个语句,则其他线程写t1、读写t2的语句都会被阻塞。同时,线程A在执行unlock tables之前,也只能执行读t1、读写t2的操作。连写t1都不允许。


表级锁优点


  • 开销小,加锁快
  • 不会出现死锁
  • 锁定力度大,发生锁冲突的概率高,并发度小


不同的存储引擎支持的锁粒度不一样:


  • InnoDB 行锁和表锁都支持,MyISAM 只支持表锁。
  • InnoDB 只有通过索引条件检索数据才使用行级锁,否则,InnoDB 将使用表锁。InnoDB 的行锁是基于索引的。


元数据锁(meta data lock ,MDL)

简介


元数据锁不需要显式的使用,在访问一个表的时候会自动加上。


MDL 的作用是保证读写的正确性。


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


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


  • 读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。
  • 读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性;因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。


即在表的读锁和写锁的模式下:读读不阻塞,读写阻塞,写写阻塞。


  • 读读不阻塞:当前线程在读数据时,其他线程也可读数据,不会加锁,不会发生阻塞。
  • 读写阻塞:当前线程在读数据时,其他线程不能修改当前线程读的数据,会加锁,发生阻塞。
  • 写写阻塞:当前线程在修改数据时,其他线程不能修改当前线程正在修改的数据,会加锁,发生阻塞。


我们知道,给一个表加字段,或者修改字段,或者加索引,需要扫描全表的数据。在对大表操作的时候,我们都会特别小心,以免对线上服务造成影响。而实际上,即使是小表,操作不慎也会出问题。


例子:

24.png

1.我们可以看到 session 1 先启动,这时候会对表 t 加一个 MDL 读锁。由于 session 2 需要的也是 MDL 读锁,因此可以正常执行。


2.之后 session 3 会被 blocked,是因为 session 1 的 MDL 读锁还没有释放,而 session 3 需要 MDL 写锁,因此只能被阻塞。


3.如果只有 session 3 自己被阻塞还没什么关系,但是之后所有要在表 t 上新申请 MDL 读锁的请求也会被 session 3 阻塞。前面我们说了,所有对表的增删改查操作都需要先申请 MDL 读锁,就都被锁住,等于这个表现在完全不可读写了。


4.如果某个表上的查询语句频繁,而且客户端有重试机制,也就是说超时后会再起一个新的 session 请求进来的话,这个库的线程很快就会爆满。


事务中的 MDL 锁,在语句执行开始时申请,但是语句结束后并不会马上释放,而会等到整个事务提交后再释放。


如何安全的给小表加字段?


首先要解决长事务,事务不提交,就会一直占着DML锁。在MySQL的information_schema库的innodb_trx表中,可以查到当前执行的事务。如果要做DDL变更的表刚好有长事务在执行,要考虑先暂停DDL,或者kill掉这个长事务


检测长事务

select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60

如果你要变更的表是一个热点表,虽然数据量不大,但是过来的请求很频繁,而你的这个表又不得不加字段,这时候我们能怎么做呢?


这时候通过 kill 事务未必管用了,因为新的请求马上就进来了。比较理想的机制是,在 alter table 语句里面设定等待时间,如果在这个指定的等待时间里,能够拿到 MDL 写锁最好,若拿不到也不能阻塞后面的业务,应该先放弃。之后再通过重试命令重复这个过程。


比如:

ALTER TABLE tbl_name NOWAIT add column ...
ALTER TABLE tbl_name WAIT N add column ...

页级锁


页级锁是MySQL中比较独特的一种锁定级别。页级锁定的特点是锁定颗粒度介于行级锁定与表级锁之 间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力也介于表级锁和行级锁中间。 使用页级锁定的主要是 BerkeleyDB 存储引擎。


行锁


MySQL的行锁是在引擎层由各个引擎自己实现的。但不是所有的引擎都支持行锁,比如MyISAM引擎就不支持行锁


行锁就是针对数据表中行记录的锁。比如事务A更新了一行,而这时候事务B也要更新同一行,则必须等事务A的操作完成后才能进行更新


行锁是针对数据库表中行记录的锁,是锁一行或者多行。MySQL 的行锁是基于索引加载的,所以行锁是要加在索引响应的行上。加行锁的目的是为了减少锁冲突,提升业务的并发度。


示例 1 :比如事务 A 更新了一行数据,而此时事务 B 也要更新同一行数据,则必须等待事务 A 操作完成之后才能进行更新操作。


示例 2 :数据库表中有一个主键索引和一个普通索引,SQL 语句基于索引查询,命中两条记录。此时行锁就锁定两条记录,当其他事务访问数据库同一张表时,被锁定的记录不能被访问,其他的记录都可以访问到。


行锁优点

  • 开销大,加锁慢
  • 会出现死锁
  • 锁定粒度小,发生锁冲突的概率低,并发度高。

表锁和行锁比较:

  • 表锁:开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度低。
  • 行锁:开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高。


两阶段锁

我们先来看一个例子:在下面的操作序列中,事务 B 的 update 语句执行时会是什么现象呢?假设字段 id 是表 t 的主键。

00001.png

事务A持有的两个记录的行锁都是在commit的时候才释放的,事务B的update语句会被阻塞,直到事务A执行commit之后,事务B才能继续执行


在InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议


如果事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发的锁尽量往后放


假设要实现一个电影票在线交易业务,顾客A要在影院B购买电影票。业务需要涉及到以下操作:

1.从顾客A账户余额中扣除电影票价

2.给影院B的账户余额增加这张电影票价

3.记录一条交易日志


为了保证交易的原子性,要把这三个操作放在一个事务中。如何安排这三个语句在事务中的顺序呢?


如果同时有另外一个顾客C要在影院B买票,那么这两个事务冲突的部分就是语句2了。因为它们要更新同一个影院账户的余额,需要修改同一行数据。根据两阶段锁协议,所有的操作需要的行锁都是在事务提交的时候才释放的。所以,如果把语句2安排在最后,比如按照3、1、2这样的顺序,那么影院账户余额这一行的锁时间就最少。这就最大程度地减少了事务之间的锁等待,提升了并发度


死锁和死锁检测


在并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁


我们来看个栗子:00002.png

事务 A 在等待事务 B 释放行 id=2 的行锁,而事务 B 在等待事务 A 释放行 id=1 的行锁,于是就进入一种事务 A 和事务 B 在互相等待对方资源的状态,此时也即使进入了死锁的状态。


当出现死锁以后,有两种策略:


1.直接进入等待,直到超时。这个超时时间可以通过参数innodb_lock_wait_timeout来设置


2.发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数innodb_deadlock_detect设置为on,表示开启这个逻辑


在InnoDB中,innodb_lock_wait_timeout的默认值是50s,意味着如果采用第一个策略,当出现死锁以后,第一个被锁住的线程要过50s才会超时退出,然后其他线程才有可能继续执行。对于在线服务来说,这个等待时间往往是无法接受的。


正常情况下还是要采用主动死锁检查策略,而且innodb_deadlock_detect的默认值本身就是on。主动死锁监测在发生死锁的时候,是能够快速发现并进行处理的,但是它有额外负担的。每当一个事务被锁的时候,就要看看它所依赖的线程有没有被别人锁住,如此循环,最后判断是否出现了循环等待,也就是死锁。


热点行更新


如果所有事务都要更新同一行的场景,每个新来的被堵住的线程都要判断会不会由于自己的加入导致死锁,这是一个时间复杂度是O(n)的操作怎么解决由这种热点行更新导致的性能问题?


1.如果确保这个业务一定不会出现死锁,可以临时把死锁检测关掉。


这种操作本身带有一定的风险,因为业务设计的时候一般不会把死锁当做一个严重错误,毕竟出现死锁了,就回滚,然后通过业务重试一般就没问题了,这是业务无损的。而关掉死锁检测意味着可能会出现大量的超时,这是业务有损的。


2.控制并发度


比如同一行同时最多只有 10 个线程在更新,那么死锁检测的成本很低,就不会出现这个问题。可以在Nginx中控制并发,在redis中控制并发以及采用队列的形式都可以。


行锁的衍生锁


行锁还衍生了其他几种算法锁,分别是 记录锁、间隙锁、临键锁,下面我们依次来看看这三种锁。


记录锁(Record Lock)


记录锁,锁的是表中的某一条数据。


触发条件:必须是精准命中索引并且索引是唯一索引,比如主键 id。


**例如:**update user_info set name=’张三’ where id=1 ,这里的id是唯一索引。


**记录锁的作用:**加了记录锁之后数据可以避免数据在查询的时候被修改的重复读问题,也避免了在修改的事务未提交前被其他事务读取的脏读问题。


间隙锁(Gap Lock)


间隙锁又称为区间锁,每次锁定都是锁定一个区间,也是属于行锁的一种。锁定的是记录与记录之间的空隙,间隙锁只阻塞插入操作,是 Innodb 为了解决幻读问题时引入的锁机制,所以只有在 Read Repeatable 、Serializable 隔离级别才有。


触发条件:也是命中索引,当我们查询数据用范围查询而不是相等查询时,查询条件命中索引,即便是没有查到符合条件的记录,此时也会将查询条件中的范围进行锁定,即使是范围中不存在的数据也会被锁定。


比如下面的表里面的数据ID 为 1,4,5,7,10 ,那么会形成以下几个间隙区间,-n-1区间,1-4区间,7-10区间,10-n区间 (-n代表负无穷大,n代表正无穷大)

00003.png


例如:对应上图的表执行select * from user_info where id>1 and id<4 for update(这里的id是唯一索引) ,这个SQL查询不到对应的记录,那么此时会使用间隙锁。


间隙锁作用:防止幻读问题,事务并发的时候,如果没有间隙锁,就会发生以下的问题,在同一个事务里,A事务的两次查询出的结果会不一样。

00004.png

临键锁(Next-Key Lock)

Record Lock + Gap Lock,是记录锁与间隙锁的并集,锁定一个范围并且锁定记录本身,是前开后闭区间,是 MySQL 加锁的基本单位。


查找过程中访问到的对象才会加锁。


**临键锁的作用:**结合记录锁和间隙锁的特性,临键锁避免了在范围查询时出现脏读、重复读、幻读问题。加了临键锁之后,在范围区间内数据不允许被修改和插入。


属性锁


排他锁(eXclusive Lock)


排他锁又称写锁,简称X锁;当一个事务为数据加上写锁时,其他请求将不能再为数据加任何锁,直到该锁释放之后,其他事务才能对数据进行加锁。


排他锁的目的是在数据修改时候,不允许其他人同时修改,也不允许其他人读取。避免了出现脏数据和脏读的问题


语法:

select * from t1 where ... for update; 

意向锁


针对于innodb的多粒度锁的情况,我们来假设一个场景,如果我需要给一张表加表锁,我们需要去判断表 中的每一行是否已经持有了锁,这个复杂度特别的高。我们可以在加行锁的时候,直接在表级别加上意向锁,这样加表锁时,只需要去判断是否存在意向锁即可。


比如我们需要给一行记录加上排他锁,首先会在表级别加入一个意向排他锁,然后在行级别加上一条排他锁。


意向锁是InnoDB自动加的,不需要用户干预。


意向锁的解释:当一个事务试图对整个表进行加锁(共享锁或排它锁)之前,首先需要获得对应类型的意向锁(意向共享锁或意向共享锁)


意向共享锁


当一个事务试图对整个表进行加共享锁之前,首先需要获得这个表的意向共享锁。

意向排他锁


当一个事务试图对整个表进行加排它锁之前,首先需要获得这个表的意向排它锁。

00005.png

相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
7天前
|
SQL 关系型数据库 MySQL
MySQL数据库进阶第五篇(锁)
MySQL数据库进阶第五篇(锁)
|
12天前
|
存储 SQL 关系型数据库
【MySQL技术内幕】6.3-InnoDB中的锁
【MySQL技术内幕】6.3-InnoDB中的锁
146 57
|
1天前
|
存储 关系型数据库 MySQL
深入浅出MySQL事务管理与锁机制
MySQL事务确保数据一致性,ACID特性包括原子性、一致性、隔离性和持久性。InnoDB引擎支持行锁、间隙锁和临键锁,提供四种隔离级别。通过示例展示了如何开启事务、设置隔离级别以及避免死锁。理解这些机制对优化并发性能和避免数据异常至关重要。【6月更文挑战第22天】
77 3
|
12天前
|
存储 SQL 关系型数据库
【MySQL技术内幕】6.5-锁问题、阻塞、死锁、锁升级
【MySQL技术内幕】6.5-锁问题、阻塞、死锁、锁升级
37 2
|
12天前
|
SQL 存储 算法
【MySQL技术内幕】6.4-锁的算法
【MySQL技术内幕】6.4-锁的算法
23 1
|
4天前
|
SQL NoSQL 关系型数据库
Mysql锁及适用场景
Mysql锁及适用场景
11 0
|
12天前
|
存储 SQL 关系型数据库
【MySQL技术内幕】6.1-锁、lock和latch
【MySQL技术内幕】6.1-锁、lock和latch
22 0
|
2天前
|
存储 关系型数据库 MySQL
关系型数据库MySQL的MyISAM
【6月更文挑战第17天】
19 11
|
3天前
|
存储 关系型数据库 MySQL
|
3天前
|
SQL 关系型数据库 MySQL