工作面试老大难 - 锁

一、概述

为保证数据的一致性和完整性,需要对 事务间并发操作进行控制 ,因此产生了 锁冲突是影响数据库 并发访问性能 的一个重要因素。所以锁对数据库而言显得尤其重要,也更加复杂。

二、并发问题

  1. MySQL并发事务访问相同记录
    (1)读读情况
    允许这种情况的发生
    (2)写-写情况在这里插入图片描述
    该图描述:当一个事务想对这条记录做改动时,首先会看看内存中有没有与这条记录关联的 锁结构 ,当没有的时候就会在内存中生成一个 锁结构 与之关联等其它事务再次访问该条记录时候,若已经有锁与之关联,那么就需要等待,自身的锁结构为true。
    (3)读写情况
    即一个事务进行读取操作,另一个进行改动操作。这种情况下可能发生 脏读不可重复读幻读 的问题。注意: MySQL在 REPEATABLE READ 隔离级别上就已经解决了 幻读 问题。
  2. 并发问题的解决方案
    (1)方案一:读操作利用多版本并发控制( MVCC ,下章讲解),写操作进行 加锁 。
    (2)方案二:读、写操作都采用 加锁 的方式。
    两个方案对比:
    (1)采用 MVCC 方式的话, 读-写 操作彼此并不冲突性能更高
    (2)采用 加锁 方式的话, 读-写 操作彼此需要 排队执行影响性能

三、锁的分类(不同角度)

在这里插入图片描述

从数据操作的类型划分:读锁、写锁

  1. 读锁 :也称为 共享锁 、英文用 S 表示。针对同一份数据多个事务的读操作可以同时进行而不会
    互相影响
    相互不阻塞的。
  2. 写锁 :也称为 排他锁 、英文用 X 表示。当前写操作没有完成前,它会阻断其他写锁和读锁。
    注意:对于 InnoDB 引擎来说,读锁和写锁可以加在表上也可以加在行上
    对读取的记录加X锁:SELECT … LOCK IN SHARE MODE; SELECT … FOR SHARE;
    对读取的记录加S锁:SELECT … FOR UPDATE;

从锁粒角度划分

表级别

  1. 表级别的 X锁、S锁:普通的读锁、写锁。注意InnoDB 存储引擎提供的表级 S锁 或者 X锁 是相当鸡肋,只会在一些特殊情况下,比方说崩溃恢复过程中用到
  2. 表级别的 意向锁:判断表中是否有记录被上锁。注意:也就是说其实IS锁和IX锁是兼容的(IX、IX也兼容)。表级别的各种锁的兼容性:
    在这里插入图片描述
    在给记录加锁时候,会先给表加一个意向锁(Is Ix),然后加再加行记录锁(s x)
  3. 表级别的元数据锁:当一个事务进行读取,另一个事务对该表进行DDL表定义语句时候,且目前在RR(可重复读隔离级别下),比如另一个事务进行删除了某一个列,那么就会造成第一个事务读取到的数据不一样,也就是事务失去了隔离性。元数据锁会在每执行一条DML、DDL语句时都会获取元数据锁,所以当另一个事务进行DDL语句时候会阻塞。
  4. 表级别的 AUTO-INC锁
    InnoDB 存储引擎提供了个 innodb_autoinc_lock_mode 的系统变量。
    (1)当 innodb_autoinc_lock_mode = 0,就采用 AUTO-INC 锁,语句执行结束后才释放锁;
    (2)当 innodb_autoinc_lock_mode = 2,就采用轻量级锁,申请自增主键后就释放锁,并不需要等语句执行后才释放。
    (3)当 innodb_autoinc_lock_mode = 1:对于普通 insert 语句,自增锁在申请之后就马上释放;而对于 类似 insert … select 这样的批量插入数据的语句,自增锁还是要等语句结束后才被释放;

行级别

  1. 行级别的 记录锁
    普通的读锁(S)锁、写(X)锁。 对一行记录锁定

  2. 行级别的 间隙锁
    引出为了解决幻读问题而生
    概念锁定两条记录之间间隙(左开右开),使其中不能插入数据,也就防止了幻读问题产生
    注意如果对一条记录加了 gap锁 (不论是 共享gap锁 还是 独占gap锁 ),并不会限制其他事务对这条记录加 正经记录锁 或者继续加 gap锁这也说明了间隙锁只为解决防止插入幻影记录而生。
    特例给最后一条记录或者给Supremum加gap锁之后,可以阻止其他事务插入 number 值在 (20, +∞) 这个区间的新记录在这里插入图片描述

  3. 行级别的 临键锁
    概念:一句话,记录锁与间隙锁的合体,左闭右开在这里插入图片描述
    例如这个,在( 3,8 ] 这个区间中在锁还没有释放之前(拥有 gap锁 的该事务提交之前)不能插入记录。也就是,它既能保护该条记录,又能阻止别的事务
    将新记录插入被保护记录前边的 间隙 。

  4. 行级别的 插入意向锁(想要在间隙锁中保护的间隙中插入记录,等待时,会生成一个插入意向锁)
    概念:设计 InnoDB 的大叔规定事务在等待的时候也需要在内存中生成一个 锁结构 ,表明有事务想在某个 间隙 中插入新记录,但是现在在等待。
    注意插入意向锁并不会阻止别的事务继续获取该记录上任何类型的锁

页级别

  1. 页锁
    页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。
    每个层级的锁数量是有限制的,因为锁会占用内存空间, 锁空间的大小是有限的 。当某个层级的锁数量
    超过了这个层级的阈值时,就会进行 锁升级 。锁升级就是用更大粒度的锁替代多个更小粒度的锁,比如
    InnoDB 中行锁升级为表锁,这样做的好处是占用的锁空间降低了,但同时数据的并发度也下降了。

从锁的态度划分

悲观锁

概念对数据被其他事务的修改持保守态度,会通过数据库自身的锁机制来实现,从而保证数据操作的排它性每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据会 阻塞 直到它拿到锁

乐观锁

概念乐观锁认为同一数据的并发操作不会总发生,属于小概率事件不用每次都对数据上锁,但是在更新的时候判断一下在此期间别人有没有去更新这个数据不采用数据库自身的锁机制,而是通过程序来实现。
思路:一条记录,事务A读一次数据version是1然后进行修改判断version是否为1如果读的时候version是1,改的时候还是1,那么这就说明,在两次操作之间没有其它事务对该条记录操作。(以版本号机制为例子)
实现方式:
(1)乐观锁的版本号机制
(2)乐观锁的时间戳机制(原理相同

加锁方式

显示锁

通过特定的语句进行加锁,我们一般称之为显示加锁,例如:
(1)显示加共享锁: select … lock in share mode
(2)显示加排它锁: select … for update

隐式锁

概念一个事务对新插入的记录可以不显式的加锁(生成一个锁结构),但是由于事务id 这个牛逼的东东的存在相当于加了一个 隐式锁 。(必须加一个锁的原因,在一个事务中新插入的记录,并发的事务会对该记录进行读、或者写操作,这就造成了脏读、脏写)

事务id起的作用
(1)对于聚簇索引记录来说,有一个 trx_id 隐藏列,该隐藏列记录着最后改动该记录的 事务id 。那么如果在当前事务中新插入一条聚簇索引记录后,该记录的 trx_id 隐藏列代表的的就是当前事务的事务id ,如果其他事务此时想对该记录添加 S锁 或者 X锁 时,首先会看一下该记录的 trx_id 隐藏列代表的事务是否是当前的活跃事务如果是的话,那么就帮助当前事务创建一个 X锁 (也就是为当前事务创建一个锁结构, is_waiting 属性是 false ),然后自己进入等待状态(也就是为自己也创建一个锁结构, is_waiting 属性是 true )。

(2)对于二级索引记录来说本身并没有 trx_id 隐藏列,在二级索引页面的 Page Header 部分有一个 PAGE_MAX_TRX_ID 属性,该属性代表对该页面做改动的最大的 事务id ,如果 PAGE_MAX_TRX_ID 属性值小于当前最小的活跃 事务id ,那么说明对该页面做修改的事务都已经提交了则此时其他事务直接可以对该记录添加 S锁 或者 X锁, 否则就需要在页面中定位到对应的二级索引记录,然后回表找到它对应的聚簇索引记录,然后再根据该条聚簇索引的trx_id找到这条记录目前所在事务,则会为该事务创建一个 X锁结构,并且自己也创建一个进入等待状态。

其他

全局锁

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

死锁

概念:在MySQL中,当多个事务同时请求相同的资源时,可能会发生死锁。死锁是指两个或多个事务互相等待对方释放资源,导致所有事务都无法继续执行的情况。

实际处理
(1)第一种策略,直接进入等待,直到超时。这个超时时间可以通过参数innodb_lock_wait_timeout 来设置。
(2)第二种策略,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务(将持有最少行级排他锁的事务进行回滚),让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为on ,表示开启这个逻辑。

第二种策略成本分析
(1)如果你能确保这个业务一定不会出现死锁,可以临时把死锁检测关掉。但是这种操作本身带有一定的风险,因为业务设计的时候一般不会把死锁当做一个严重错误,毕竟出现死锁了,就回滚,然后通过业务重试一般就没问题了,这是 业务无损 的。而关掉死锁检测意味着可能会出现大量的超时,这是业务有损 的。
(2)控制并发度。如果并发能够控制住,比如同一行同时最多只有10个线程在更新,那么死锁检测的成本很低。基本思路就是,对于相同行的更新,在进入引擎之前排队,这样在InnoDB内部就不会有大量的死锁检测工作了

避免死锁

  1. 尽量减少事务的持有时间,尽快释放锁。
  2. 尽量减少事务中需要锁定的资源数量,避免同时请求相同的资源。
  3. 尽量按照相同的顺序请求资源,避免交叉锁定。
  4. 使用合适的隔离级别,例如使用READ COMMITTED隔离级别可以减少死锁的发生。
  5. 对于复杂的事务,可以使用分布式事务管理器来协调多个事务的操作,避免死锁的发生。

四、锁内存结构

在这里插入图片描述

五、锁监控

在这里插入图片描述

至此,《MySQL是怎样运行的:从根儿上理解MySQL》,复习结束!

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值