《MySQL必懂系列》全局锁、表级锁、行锁

《MySQL必懂系列》全局锁、表级锁、行锁

大家好,又见面了,我是全栈君。

MySQL提供了不同等级的锁,按限制能力的划分,分为全局锁、表锁、行锁。本文会描述不同锁的应用场景与实现原理。

全局锁

全局锁就是对整个MySQL数据库加锁,MySQL中的命令是 Flush tables with read lock (FTWRL)。在执行这个命令之后,MySQL进入全局锁的状态,整个数据库会拒绝掉增删改这些请求。

为什么需要全局锁

全局锁的目标是为我们维护一个数据库的逻辑一致性。如下场景中:在进行逻辑备份(即备份的数据是SQL语句)的时候,没有开启全局锁,那么很可能会导致出现数据库的逻辑一致性错误,例如两个表,一个余额表、一个订单表,在购物时(减余额、生成订单)如果逻辑备份在这两个操作之间,也就是说减完余额之后,逻辑备份,拒绝生成订单,那么这个时候,我们进行的逻辑备份就是一个错误的逻辑一致性状态。以后使用这个逻辑备份进行数据恢复的时候,就会出现用户余额已经减少,但并没有订单这种问题。

全局锁的缺点

  • 对主库使用全局锁进行逻辑备份时,会造成业务的停摆
  • 对从库使用全局锁进行逻辑备份时,会造成主从延迟的问题

FTWRL的替代方式

全局锁解决的就是上面的问题,我们可以结合数据库中事务的隔离级别,使用可重复读(各个事务之间没有相互影响,基于mvcc)的隔离级别,获取数据库的逻辑一致性视图。MySQL官方自带的逻辑备份工具mysqldump,在备份数据之前,会启动一个事务,以此来获得一个逻辑一致性视图。

但需要注意的是,虽然事务的可重复能解决FTWRL影响性能的问题,但事务并不是万能的,因为并不是所有的引擎都支持这个隔离级别,MyISAM这种不支持事务的引擎,如果备份过程中有更新,总是只能取到最新的数据,那么就破坏了备份的逻辑一致性。

为什么不设置为全库只读?

我们的目的是实现数据库的逻辑一致性,那么为什么不建议直接把数据库设置成只读状态呢? (set global readonly=true)
主要有一下原因:

  1. FTWRL与readonly的异常机制不太一样。客户端(相对于MySQL)发生异常,FTWRL命令下会自动释放MySQL的全局锁。而readonly会一直停留在readonly状态,数据库长期处于不可写状态。
  2. readonly会被一些逻辑判断使用,例如使用readonly判断是主库或者备库。

表级锁

表级锁也分为两类: 表锁元数据锁(meta data lock,MDL)
业务的更新不只是增删改数据(DML),还有可能是加字段等修改表结构的操作(DDL)。

表锁

使用场景

在还没有更细粒度的行锁的时候,表锁是最长用的处理并发的解决方式。但是对于当前支持行锁的引擎例如innodb,都优先使用行锁来控制并发,以此来避免因为锁住整个表的影响。

表锁的语法

加锁 lock tables … read/write、主动释放锁unlock tables 。同时表锁也可以在客户端断开连接的时候自动释放。

读锁(共享锁)

事务A对数据d加上共享锁S,那么事务A只能对d进行读操作,并且后面的事务B、C、D都可以加锁S进行只读操作。在释放完S锁之前不能对数据d进行修改。

写锁(排它锁)

事务A对数据d加上排它锁X,那么事务A可以对数据d进行访问、修改,并且拒绝其他事务对数据d的读、写。

表锁需要注意的地方

lock tables语法不仅会限制别的线程(事务)读写操作,也限定了本线程(事务)的操作对象以及操作方式。即本线程只能按照加锁语句中规定的方式(读或者写)访问特定的资源(table1、table2)。例如:线程 Thread1 中执行 lock tables table1 write, table2 read;其他线程读、写 table1写 table2 的语句都会被 阻塞。同时,线程 Thread1 在执行 unlock tables 之前,也只能执行读、写 table1、读 table2 的操作。连写 table2 都不允许,并且也不能访问其他表。

元数据锁 metadata lock MDL

元数据在这里其实指的就是表结构,MDL锁定的也就是我们表结构。防止出现一个线程A在执行表查询操作时候,线程B删除了一个字段,导致查询的结果与表结构不符合这种情况的出现。
所以为了解决上述问题,MDL分为了读锁写锁

  • 在进行表的增删改查时候,会对表自动加上读锁,读锁之间不会互斥,所以多个线程可以对同一个表进行增删改查。
  • 在进行表结构更改时候,会对表自动加上写锁,写锁是互斥,多个线程能依次对表结构进行修改,然后再加上读锁进行增删改查。

表锁并不是现在优先考虑使用的锁,应该尽量的使用行锁,如果在项目中遇到lock table1这样的SQL语句时,应该思考一下:

  • 是否使用了过老的引擎,例如MYISAM就不支持行锁,可以考虑升级一下引擎,然后把业务代码中的lock tables unlock tables替换为begin commit就OK啦。

行锁

行锁顾名思义就是对每一行的数据加锁,这是MySQL数据库中最细粒度的锁,右innodb引擎支持。对于不能支持行锁的引擎,对于并发操作的处理只能使用表锁锁定整个表,这也是MyISAM被innoDB所替代的重要原因之一。

行锁的使用过程

使用行锁过程中,若一个事务A正在更新某一行数据d,这时候如果事务B也想对d进行更新操作,那么只能等A更新完毕然后再加自己的行锁对d进行更新操作。这其中就涉及到一个两阶段锁这个概念。

行锁的两阶段锁协议

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

其实就是规定了加锁与解锁的时机,两阶段锁协议不仅局限在行锁中。

事务A 事务B
begin
update t1 set k=k+1 where id=1;
update t1 set k=k+1 where id=2;
 
  begin
update t1 set k=k+1 where id=1;
commit  

上面的两个事务AB执行时候就会使用到两段锁协议:事务A先开始执行,id=1时加锁这一行,id=2时加锁这一行,事务A的两条语句执行完了但是还没有commit,事务B开始执行,但是这个时候事务B的update id=1会被阻塞,因为id=1还被事务A加着行锁,虽然事务A的update执行完了,但是事务A还没有commit,意味着事务A所占据着的行锁都没有释放,只有等A执行commit之后,事务B才能继续获得id=1的行锁进行update。

所以我们应该记住两段锁的特点:

  1. 在行锁的引擎中,行锁是执行到具体某一行才加上的。
  2. 行锁在本本事务commit之后才会被释放。

所以根据两段锁协议的特点,我们在开发过程中,应该在事务中把并发大的表放到后面执行,让它被行锁锁定的时间最短。

例如在减库存,生成订单这样的场景中,我们应该先在事务中生成订单,在减库存。因为库存的update并发量会大于订单insert的并发量,update需要使用行锁,如果先update库存,会使库存中的这一行一直被行锁锁定,在事务提交时候才能被释放,增加了许多无用的库存行锁锁定时间。

行锁中的死锁

数据库中死锁的概念很清晰,和我们操作系统中的一致:

  1. 资源必须互斥访问
  2. 请求并保持
  3. 不可抢占资源
  4. 形成一个环

如果一个项目要新上线一个新功能,如果新功能刚开始的时候MySQL 就挂了。登上服务器一看,CPU 消耗接近 100%,但整个数据库每秒就执行不到 100 个事务。原因很可能就是死锁。

解决MySQL死锁策略

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

  1. 设置等待的超时时间。innodb_lock_wait_timeout
  2. 主动发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。innodb_deadlock_detect = on,表示开启死锁检测。

innodb_lock_wait_timeout在innoDB引擎中的的默认值是50s,意味着如果发生死锁的情况,第一个被锁住的线程等待50s才会超时退出,然后其他线程才有可能继续执行。对于在线服务来说,这个等待时间往往是无法接受的。但是,我们又不可能直接把这个时间设置成一个很小的值,比如1s。这样当出现死锁的时候,确实很快就可以解开,但如果不是死锁,而是简单的锁等待呢?所以,通过设置超时时间通常不是一个好办法,这个更依赖经验值,也依赖不同项目的环境(请求并不均匀)。

所以通常情况下会采用主动死锁检测的策略,innodb_deadlock_detect默认值就是on的状态。主动死锁检测能及时发现并解决死锁,但主动死锁检测会消耗硬件资源。

主动死锁检测 流程:每当一个事务被锁的时候,就要看看它所依赖的线程有没有被 别人锁住,如此循环,最后判断是否出现了循环等待,也就是死锁。

主动死锁检测在热点行更新时产生的问题

上面我们提到更推荐使用主动死锁检测去解决死锁问题,但在这样的场景中:所有的事务都需要更新同一行的数据。使用主动死锁检测肯定能得出未死锁,但是这期间要消耗大量的cpu,导致虽然占用了大量cpu却实际没能执行几个事务。

这种由这种热点行更新导致的性能问题的原因在于:主动死锁检测要耗费大量的 CPU 资源。

热点行更新导致的性能问题的解决思路:

  1. 如果能保证某个业务不会出现死锁,可以临时关闭死锁检测,但本身可能存在风险,如果发生死锁,会发生事务等待超时时间。
  2. 控制并发度。例如一行数据只能允许20个事务进行同时更新,那么可以极大的减缓死锁检测的压力。如何去控制并发度,大体也有两个思路一是通过业务代码在客户端进行访问MySQL的控制,但是MySQL不一定只有这一个客户端,所以这个思路优缺点;二是考虑使用中间间或者是修改MySQL源码,对于相同行的update,在进入引擎之前排队,里面只允许存在20个事务进行update,这样update时候就不会有太大的死锁检测压力。(死锁检测时间复杂度为O(n平方))。 但是这个需要数据库方面的专家。。。
  3. 可以考虑在业务层面减少对某一行的并发度。例如在收款这个场景中,我们把热点的某一行拆分出来,保证拆分出来的几行最后在收款的总数一致就可以了。如果分为20个,那么死锁的肯能性就变为了原来的20粉之一,与此同时由于不是同一行也减少了主动死锁检测cpu的消耗。这种方式需要在代码里做详细、严谨的逻辑分析。

综上:减少死锁的主要方向,就是控制访问相同资源的并发事务量。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/111655.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • python程序的热部署实现[通俗易懂]

    python程序的热部署实现[通俗易懂]pytho程序的热部署知乎上面的回答真正意义上的代码热部署应该是类似erlang那样的,将代码更新到节点后不停服务,不断连接的自动应用新代码。autoreload(代表django的autoreload)什么的还是会造成业务瞬间中断。我感觉是可以从wsgi容器级别上实现,比如更新代码后检测到文件变更,然后通知容器创建新的wsgiapplication的实例,之后所有新的请求都发送到新的wdgi…

  • 列举出linux文件和目录常用的命令_cat -n file1file2 命令的意思是

    列举出linux文件和目录常用的命令_cat -n file1file2 命令的意思是目录命令总览ls(英文全拼:listfiles):列出目录及文件名cd(英文全拼:changedirectory):切换目录pwd(英文全拼:printworkdirectory):显

  • android的toast提示_android studio unknown host

    android的toast提示_android studio unknown host相信很多人遇到过这关问题编码的设置问题但是我要说的并不是这个问题 而是系统自动弹出的toast 醉了这特么谁看得懂 后来经过观察发现是权限的问题如果需要获取权限但是没有处理的话默认是会弹出这个提示 因此首先要检查是否拥有该权限如果拥有再搞事情,如果没有就申请权限/*********获取设备id的权限检查*********/if(islacksO

  • SecureCRT下中文乱码怎么解决?「建议收藏」

    SecureCRT下中文乱码怎么解决?「建议收藏」在SecureCRT窗口中敲击命令后中文出现乱码,比如输入df如何设置编程中文?第一步第二步:说明一下因为视频中的图片不是很清晰,所以我用我自己的中文版SecureCRT,每个位置是一样的.第

  • win10关闭445端口方法_服务器关闭445端口

    win10关闭445端口方法_服务器关闭445端口445端口是一种TCP端口,有了它我们可以在局域网中轻松访问各种共享文件夹或共享打印机,但也正是因为有了它,黑客们才有了可乘之机,他们能通过该端口偷偷共享你的硬盘,甚至会在悄无声息中将你的硬盘格式化掉。今天小编将为大家分享Win11关闭445端口的方法,一起来看看吧!更多重装系统教程尽在小白系统重装官网  1、首先,按Win+S组合键,或点击底部任务栏上的搜索图标,打开的Windows搜索窗口,顶部输入Windows防火墙,然后点击系统给出的最佳匹配WindowsDefender防

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号