MySQL的锁

锁的分类

根据加锁的范围,MySQL里面的锁大致可以分为全局锁,表级锁和行锁

全局锁

全局锁就是对整个数据库实例加锁。MySQL提供了一个加全局读锁的方法,命令是Flush tables with read lock (FTWRL)。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改),数据定义语句(建表,修改表结构等),和更新类事务的提交语句。
全局锁的典型使用场景是做全库逻辑备份
在备份过程中整个库完全处于只读状态,这个状态听上去是很危险的:

  1. 如果你在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆
  2. 如果你在从库上备份,那么备份期间从库不能执行主库同步过来的binlog,会导致主从延迟
    既然加全局锁不好,那我们为什么要加锁呢,假设我们不加锁:
    我们有一个卖课系统,有两张表,一个是用户的余额表,一个是用户的课程表。假设在备份期间,一个用户他购买了一门课程,业务逻辑就要扣掉他的余额,然后往这个用户对应的课程表中加一门课,如果时间上是先备份余额表,然后用户购买,然后备份课程表会发生什么呢?如图所示:(该图是极客时间的MySQL45讲里面的一张图)
    在这里插入图片描述
    可以看到,这个备份结果里,用户的数据状态是“账户余额没扣,但是用户课程表里面已经多了一门课”。如果后面用这个备份来恢复数据的话,用户就发现,自己赚了。也就是说,不加锁的话,备份系统备份的得到的库不是一个逻辑时间点,这个视图是逻辑不一致的。
    其实我们也可以通过拿到一致性视图的方式来备份(如果是InnoDB引擎的库,这种做法对应用会更加友好),但是前提是引擎要支持这个隔离级别,对于MyISAM这种不支持事务的引擎,我们就需要FTWRL命令了,这也是InnoDB代替MyISAM的原因之一。
    那么我们为什么不使用set global readonly = true的方式让全库只读呢?主要有两个原因:
    1. 在有些系统中,readonly的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库。因此,修改global变量的方式影响面更大,不建议使用。
    2. 两者在异常处理机制上有差异。执行FTWRL命令之后如果客户端发生异常端口,MySQL会自动释放这个全局锁。而将整个库设置为readonly之后,客户端发生异常,数据库会一直保持readonly状态,会导致整个库长时间处于不可写的状态。

表级锁

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

表锁

表锁的语法:lock tables … read/write。他可以用unlock tables主动释放锁,也可以在客户端断开的时候自动释放。
注意:lock tables 除了会限制别的线程的读写外,也限定了本线程接下来的操作对象。
比如我们执行了lock tables t1 read , t2 write ;其他线程写t1,读写t2的的语句都会被阻塞 , 同时本线程也只能执行读t1 , 读写t2的操作,连写t1也不行 , 也不能访问其他表

MDL锁

MDL在访问一个表的时候会被自动加上。MDL的作用是保证读写的正确性。在MySQL5.5版本引入了MDL,当对一个表做增删改查操作的时候,加MDL读锁;当要对表结构做变更操作的时候,加MDL写锁。事务中的MDL锁,在语句执行开始时申请,但是语句结束后并不会马上释放,而是等到整个事务提交后再释放
MDL锁的特点:

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

我们在给一个表加字段的时候要特别小心,尽管是该表是一个小表,操作不好也会出现问题。我们看看下面这个图:
在这里插入图片描述
我们可以知道sessionA 和 session B 都可以正常执行,因为session A的读锁还没有释放,之后session c会被阻塞,之后所有要在表t上读和写的操作都会被阻塞住了,如果这个表上的查询语句频繁,而且客户端有重试机制,这个库的线程很快就会爆满了。
解决方案:

  1. 安全的给小表加字段
    我们可以在MySQL的information_schema库的innodb_trx表中,看看有没有长事务,如果有长事务,我们要考虑先暂停DDL,或者kill掉这个长事务
  2. 给一个热点表加字段,该表数据量不大,但是上面的请求很频繁
    在alter table语句里面设定等待时间,如果在这个指定的等待时间里面能够拿到MDL写锁最好,拿不到也不要阻塞后面的业务语句,之后再通过重试命令重复这个过程

行锁

MySQL的行锁是在引擎层由各个引擎自己实现的。但并不是所有的引擎都支持行锁,比如MyISAM引擎就不支持行锁,而InnoDB支持行锁
行锁:行锁就是针对数据表中行记录的锁。比如事务A更新了一行,而这时候事务B也要更新同一行,则必须等事务A的操作完成后才能进行更新。但是我们的where没有走索引或者索引失效时,InnoDB 的行锁会变表锁,即整个表被锁住
我们看看下面这个图:
在这里插入图片描述
上面这个图的结果是:事务B的update 语句会被阻塞,直到事务A执行commit之后,事务B才能继续执行。
由此可知,事务A持有两个记录的行锁,都是在commit的时候才释放的。即:在InnoDB事务中,行锁是在需要的时候才加上,但并不是不需要了就立刻释放,而是等到事务结束时才释放。这个就是两阶段协议。所以,当事务中需要锁多个行的时候,要把最可能造成冲突,最可能影响并发度的锁尽量往后放

死锁和死锁检测

死锁:当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态。
下面这个例子就会造成死锁:
在这里插入图片描述
事务A在等待事务B释放id=2的行锁,事务B在等待事务A释放id=1的行锁,就进入了死锁状态。
解决死锁策略:

  1. 直接进入等待,直到超时自动释放锁,超时时间通过
    innodb_lock_wait_timeout 来设置
  2. 发起死锁检测,发现死锁后,主动回滚死锁链条的某一个事务,让其他事务得以执行。将参数innodb_deadlock_detect设置为on,表示开启这个逻辑。

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

假设现在有一个这样的情景,所有的事务都要更新同一行。 我们分析一下死锁检测的过程,每个新来的被堵住的线程,都要判断会不会由于自己的加入导致了死锁,一个线程这么检测一下时间复杂度就是O(N),如果有N个线程的话,每个线程都要这么搞一下,时间复杂度就是O(N平方)。尽管最终的检测的结果是没有死锁,但是这期间会消耗大量的CPU资源。
那么解决由这种热点行更新导致的性能问题呢?

  1. 如果你能确保这个业务一定不会出现死锁,可以临时把死锁检测关掉。(有一定风险性)
  2. 控制并发度,这个控制并发要做在数据库服务端。如果有中间件,可以考虑在中间件实现,如果有人能修改MySQL源码也可以做在MySQL里面。思路就是:对于相同行的更新,在进入引擎之前排队。
  3. 可以考虑通过将一行改成逻辑上的多行来减少锁冲突。以银行账户为例子,我们可以考虑把账户余额放在多条记录上,比如把他分为10条记录,银行账户总额等于这10个记录的值的总和,每次给账户加钱的时候,随机选择其中一条记录来加,这样冲突就减少了十分之一。减少锁等待个数,也就减少了死锁检测的CPU消耗。但是账户余额可能会减少,那么我们需要考虑在某一个记录变成0的时候,代码要特殊处理
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值