mysql——锁

MySQL里面的锁大致可以分成全局锁、 表级锁和行锁三类

1.全局锁

一个库被全局锁上以后, 你要对里面任何一个表做加字段操作, 都是会被锁住的。

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

2.全局锁 FTWRL 会出现什么问题?

FTWRL确保不会有其他线程对数据库做更新, 然后对整个库做备份,但是让整库都只读, 有风险:

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

全局锁不太好。 但是细想一下, 备份为什么要加锁呢? 不加锁会有什么问题。

3.不加锁会出现什么问题?

​ 假设备份期间, 有一个用户, 他购买了一门课程, 业务逻辑里就要扣掉他的余额, 然后往已购课程里面加上一门课
在这里插入图片描述

  • 时间顺序上是先备份账户余额表, 然后用户购买, 然后备份用户课程表会怎么样呢?

​ 结果就是:用户数据A的数据状态是“账户余额没扣,但是课程表里多了一门课,如果用这个备份来恢复数据的话,用户A就赚了。

  • 如果备份表的顺序反过来, 先备份用户课程表再备份账户余额表, 又可能会出现什么结果?
    结果就是:用户数据A的数据状态是“账户余额扣了,但是课程表里还是空的。

从上面实验结果来看,不加锁备份的话,备份得到的库结果不是一个逻辑时间点,这视图的逻辑不一致的

4怎么解决不加锁备份会出现库数据不一致的问题?

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

single-transaction方法只适用于所有的表使用事务引擎的库

5.既然备份工具是mysqldump可以解决一致性视图问题,为什么还需要 FTWRL ?

使用 逻辑备份工具是mysqldump前提是 引擎要支持这个隔离级别 , 比如, 对于MyISAM这种不支持事务的引擎, 如果备份过程中有更新, 总是只能取到最新的数据, 那么就破坏了备份的一致性。 这时, 我们就需要使用FTWRL命令了。 single-transaction方法只适用于所有的表使用事务引擎的库

6.还有一种方式支持全库只读 readonly

使用set global readonly=true的方式可以实现全库只读, eadonly方式也可以让全库进入只读状态, 但是会建议你用FTWRL方式, 主要有两个原因 :

  1. 在有些系统中, readonly的值会被用来做其他逻辑, 比如用来判断一个库是主库还是备库。 因此, 修改global变量的方式影响面更大, 我不建议你使用
  2. 在异常处理机制上有差异。 如果执行FTWRL命令之后由于客户端发生异常断开, 那么MySQL会自动释放这个全局锁, 整个库回到可以正常更新的状态。 而将整个库设置为readonly之后, 如果客户端发生异常, 则数据库就会一直保持readonly状态, 这样会导致整个库长时间处于不可写状态, 风险较高
7. 表级锁

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

表锁的语法是 lock tables …read/write,可以用unlock tables主动释放锁,也可以在客户端断开的时候自动释放

lock tables语法除了会限制别的线程的读写外, 也限定了本线程接下来的操作对象, 举个例子, 如果在某个线程A中执行lock tables t1 read, t2 write; 这个语句, 则其他线程写t1、 读写t2的语句都会被阻塞。 同时, 线程A在执行unlock tables之前, 也只能执行读t1、 读写t2的操作。 连写t1都不允许, 自然也不能访问其他表。

8. 表级的锁是MDL

MDL不需要显式使用, 在访问一个表的时候会被自动加上。 MDL的作用是, 保证读写的正确性。

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

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

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

​ 给一个表加字段, 或者修改字段, 或者加索引, 需要扫描全表的数据。 在对大表操作的时候, 你肯定会特别小心, 以免对线上服务造成影响。 而实际上, 即使是小表, 操作不慎也会出问题。 我们来看一下下面的操作序列, 假设表t是一个小表。
在这里插入图片描述
我们可以看到session A先启动, 这时候会对表t加一个MDL读锁。 由于session B需要的也是MDL读锁, 因此可以正常执行。之后session C会被blocked, 是因为session A的MDL读锁还没有释放, 而session C需要MDL写
锁, 因此只能被阻塞。如果只有session C自己被阻塞还没什么关系, 但是之后所有要在表t上新申请MDL读锁的请求也会被session C阻塞。 所有对表的增删改查操作都需要先申请MDL读锁, 就都被锁住, 等于这个表现在完全不可读写了 . 如果某个表上的查询语句频繁, 而且客户端有重试机制, 也就是说超时后会再起一个新session
再请求的话, 这个库的线程很快就会爆满

10 如何安全地给小表加字段?
  1. 解决长事务, 事务不提交, 就会一直占着MDL锁。 在MySQL的information_schema
    库的 innodb_trx表中, 你可以查到当前执行中的事务。 如果你要做DDL变更的表刚好有长事务
    在执行, 要考虑先暂停DDL, 或者kill掉这个长事务。

  2. 如果变更的表是一个热点表, 虽然数据量不大,上面的请求很频繁, 而你不得不加个字段, 你该怎么做呢?

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

11 MySQL的行锁
行锁就是针对数据表中行记录的锁。 这很好理解, 比如事务A更新了一行, 而这时候事务B也要更新同一行, 则必须等事务A的操作完成后才能进行更新。  
加锁的方式:自动加锁。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁;对于普通SELECT语句,InnoDB不会加任何锁;当然我们也可以显示的加锁:   
12 MySQL的行锁是两阶段锁

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

两阶段锁协议容易出现死锁。例子 ,

  1. 线程A开启事务,执行UPDATE t set k=k+1 where id=1
  2. 线程B开启事务,执行UPDATE t set k=k+1 where id=2
  3. 线程A开启事务,执行UPDATE t set k=k+1 where id=2
  4. 线程B开启事务,执行UPDATE t set k=k+1 where id=1
  5. 进入了死锁状态

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

13 死锁和死锁检测

当并发系统中不同线程出现循环资源依赖, 涉及的线程都在等待别的线程释放资源时, 就会导致这几个线程都进入无限等待的状态, 称为死锁 , 调整语句顺序并不能完全避免死锁

14 出现死锁以后, 有两种策略 是怎么解决地?
  1. 一种策略是, 直接进入等待, 直到超时。 这个超时时间可以通过参数innodb_lock_wait_timeout来设置
  2. 另一种策略是, 发起死锁检测, 发现死锁后, 主动回滚死锁链条中的某一个事务, 让其他事
    务得以继续执行。 将参数innodb_deadlock_detect设置为on, 表示开启这个逻辑。主动死锁检测在发生死锁的时候, 是能够快速发现并进行处理的

innodb_lock_wait_timeout的默认值是50s,意味着如果采用第一个策略,当出现死锁以后,第一个被锁住的线程要过50s才会超时退出, 然后其他线程才有可能继续执行,对于在线服务来说,这个等待时间往往是无法接受的。 直接把这个时间设置成一个很小的值, 比如1s。 这样当出现死锁的时候, 确
实很快就可以解开, 但如果不是死锁, 而是简单的锁等待呢? 所以, 超时时间设置太短的话, 会
出现很多误伤

正常情况下我们还是要采用第二种策略, 即: 主动死锁检测, 而且innodb_deadlock_detect的默认值本身就是on。 主动死锁检测在发生死锁的时候, 是能够快速发现并进行处理的, 但是它也是有额外负担的

15 热点行更新导致的会导致什么问题?

​ 每个新来的被堵住的线程, 都要判断会不会由于自己的加入导致了死锁, 这是一个时间复杂度是O(n)的操作。 假设有1000个并发线程要同时更新同一行, 那么死锁检测操作就是100万这个量级的。 虽然最终检测的结果是没有死锁, 但是这期间要消耗大量的CPU资源。 因此, 你就会看到CPU利用率很高, 但是每秒却执行不了几个事务。

16 怎么解决由这种热点行更新导致的性能问题呢?
  1. 就是如果你能确保这个业务一定不会出现死锁, 可以临时把死锁检测关掉

  2. 控制并发度

  3. 从设计上优化这个问题

    考虑通过将一行改成逻辑上的多行来减少锁冲突。 还是以影院账户为例, 可以考虑放在多条记录上, 比如10个记录, 影院的账户总额等于这10个记录的值的总和。 这样每次要给影院账户加金额的时候, 随机选其中一条记录来加。 这样每次冲突概率变成原来的1/10, 可以减少锁等待个数, 也就减少了死锁检测的CPU消耗

17.MySQL语句加锁分析

假设表数据有ID,NUMBE为(1,1),(5,5),(10,10),(15,15)(20,20),
RC隔离级别下锁行

1.REPEATABLE READ隔离级别下

REPEATABLE READ隔离级别与READ UNCOMMITTEDREAD COMMITTED这两个隔离级别相比,最主要的就是要解决幻读问题,幻读问题的解决还得靠我们之前讲过的gap锁

  1. 对于使用主键进行等值查询的情况

    SELECT * FROM t WHERE id = 10 for update;--结果id=10这行数据被锁
    --如果我们要查询主键值不存在的记录
    SELECT * FROM t WHERE id = 8 for update;--结果(5,10)这个区间数据被锁
    
    1. 对于使用主键进行范围查询的情况
    SELECT * FROM t WHERE id >= 10 for update;
    --为id值大于10的所有聚簇索引记录都加一个S型next-key锁(包括Supremum伪记录)[10,+正无穷)。
    SELECT * FROM t WHERE id <= 10 for update;
    --为id值小于于10的所有聚簇索引记录都加一个S型next-key锁(包括Supremum伪记录)(-负无穷,15)。
    --REPEATABLE READ隔离级别下,在判断id值为15的记录不满足边界条件 id <= 10 后,并不会去释放加在该记录上的锁!!! 
    
    1. 对于使用唯一二级索引进行等值查询的情况

    唯一二级索引具有唯一性

     ---对唯一二级索引列进行等值查询的记录存在
     select * from t where c=10 for update;---先对二级索引记录加X锁,再对聚簇索引加X锁 
    ---对唯一二级索引列进行等值查询的记录并不存在
     select * from t where c=8 for update;
    -- 这条二级索引记录上加一个gap锁
    
    1. 对于使用普通二级索引进行等值查询的情况
    --对普通二级索引等值查询的值存在
    select * from t where c=10 for update;---(5,15)被next-lock锁
    --对普通二级索引等值查询的值并不存在
    select * from t where c=8 for update;---(5,10)被next-lock锁
    
    
    1. 对于使用普通二级索引进行范围查询的情况
    --对普通二级索引进行范围查询
    select * from t where c>=10 for update;---(5,,+正无穷)被next-lock锁
    
    1. 全表扫描的情况

    全部记录都被加了next-key锁

18 间隙锁

产生幻读的原因是, 行锁只能锁住行, 但是新插入记录这个动作, 要更新的是记录之间的“间隙”。 因此, 为了解决幻读问题, InnoDB只好引入新的锁, 也就是间隙锁(GapLock)

即使给所有的行都加上行锁, 仍然无法解决幻读问题, 因此引入了间隙锁的概念。 把隔离级别设置为读提交的话,就没有间隙锁了 ,只有行锁, 间隙锁的引入, 可能会导致同样的语句锁住更大的范围, 这其实是影响了并
发度的。

19 for update锁表还是锁行

如果where条件是有索引的就是行锁+间隙锁, 间隙锁和行锁合称next-keylock

不是索引就是表锁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值