IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 大数据 -> MySQL 8:行锁 -> 正文阅读

[大数据]MySQL 8:行锁

行锁

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

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

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

行锁优点

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

表锁和行锁比较:

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

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

两阶段锁

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

事务 A

事务 B

begin;

update t set k=k+1 where id=1;

update t set k=k+1 where id=2;

begin;

update t set k=k+2 where id=1;

commit;

实际上事务 B 的 update 语句会被阻塞,直到事务 A 执行 commit 之后,事务 B 才能继续执行。

我们来分析下:

  • 事务 A 持有的两个记录的行锁,都是在 commit 的时候才释放的。
  • 也就是说,在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议

知道了这个设定,对我们使用事务有什么帮助呢?

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

来举个栗子:

假设你负责实现一个直播间在线交易业务,顾客 A 要在电商直播间 B 购买一件衣服。我们简化一点,这个业务需要涉及到以下操作:

  1. 从顾客 A 账户余额中扣除衣服价;
  2. 给电商直播间 B 的商家账户余额增加这件衣服的价;
  3. 记录一条交易日志。

也就是说,要完成这个交易,我们需要 update 两条记录,并 insert 一条记录。当然,为了保证交易的原子性,我们要把这三个操作放在一个事务中。那么,你会怎样安排这三个语句在事务中的顺序呢?

试想如果同时有另外一个顾客 C 要在电商直播间 B 买衣服,那么这两个事务冲突的部分就是语句 2 了。因为它们要更新同一个直播间商家的余额,需要修改同一行数据。

根据两阶段锁协议,不论我们怎样安排语句顺序,所有的操作需要的行锁都是在事务提交的时候才释放的。所以如果把语句 2 的顺序放在最后,那么电商直播间 B 的商家账户余额这一行锁的时间就最少,这样就大幅度减少了事务之间的等待,提升了并发度。

如果这个电商直播间 B 做促销活动,以特别低价清仓处理一年内所有的衣服,而且这个活动只做一天。于是在活动时间开始的时候,你的 MySQL 就挂了。你登上服务器一看,CPU 消耗接近 100%,但整个数据库每秒就执行不到 100 个事务。这是什么原因呢?

这里,我就要说到死锁和死锁检测了。

死锁和死锁检测

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

我们来看个栗子:

事务 A

事务 B

begin;

update t set k=k+1 where id=1;

begin;

update t set k=k+1 where id=2;

update t set k=k+1 where id=2;

update t set k=k+1 where id=1;

事务 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 之后才会超时退出,然后其他线程才有可能继续执行下去,对于在线的业务系统来说,这个等待时间着实有点长,客户是无法接受的。

但是,我们又不可能直接把这个时间设置成一个很小的值,比如 1s。这样当出现死锁的时候,确实很快就可以解开,但如果不是死锁,而是简单的锁等待呢?所以,超时时间设置太短的话,会出现很多误伤。

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

你可以想象一下这个过程:每当一个事务被锁的时候,就要看看它所依赖的线程有没有被别人锁住,如此循环,最后判断是否出现了循环等待,也就是死锁。那如果是我们上面说到的所有事务都要更新同一行的场景呢?

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

线上踩坑

我们线上就遇到了这种情况,做了分表处理,自增 id 要去查数据,然后更新这个自增 id,就造成所有的并发线程都要更新同一行数据让 id 去增值,于是就有很多超时等待的线程。 消耗了大量的 CPU 资源,但是每秒执行的事务却很少。

根据上面的分析,我们来讨论一下,怎么解决由这种热点行更新导致的性能问题呢?问题的症结在于,死锁检测要耗费大量的 CPU 资源。

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

    根据上面的分析,你会发现如果并发能够控制住,比如同一行同时最多只有 10 个线程在更新,那么死锁检测的成本很低,就不会出现这个问题。

    一个直接的想法就是,在客户端做并发控制。但是,你会很快发现这个方法不太可行,因为客户端很多。如果一个应用,有 600 个客户端,这样即使每个客户端控制到只有 5 个并发线程,汇总到数据库服务端以后,峰值并发数也可能要达到 3000。

    因此,这个并发控制要做在数据库服务端。如果有合适的中间件,可以考虑在中间件实现;如果能修改 MySQL 源码,也可以做在 MySQL 里面。基本思路就是,对于相同行的更新,在进入引擎之前排队。这样在 InnoDB 内部就不会有大量的死锁检测工作了。

    如果不能修改源码,可以考虑通过将一行改成逻辑上的多行来减少锁冲突。以我遇到的 id 增更新同一行数据为例,可以一下查出来 1000 个 id 存起来,用完再去查询和更新 id 的值,这样每次冲突概率变成原来的 1/1000,可以减少锁等待个数,也就减少了死锁检测的 CPU 消耗。

行锁的衍生锁

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

记录锁(Record Lock)

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

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

间隙锁(Gap Lock)

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

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

临键锁(Next-Key Lock)

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

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

注意的点

索引上的等值查询,给唯一索引加锁的时候,Next-Key Lock 退化为记录锁。

索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。

唯一索引上的范围查询会访问到不满足条件的第一个值为止,MySQL 8.0.22之后已修复。

示例

一张表 t, id (主键)、k (普通索引)、d 字段 插入数据 (0,0,0),(5,5,5),(10,10,10),(15,15,15)

id

k

d

0

0

0

5

5

5

10

10

10

15

15

15

  • update t set d=1 where id = 7 主键索引上的 (5,10) 间隙锁,id = 7 在主键 5 和 10 之间,所以是 (5,10) 间隙锁
  • update t set d=1 where id = 5 主键索引上的 5 记录锁,命中主键 id = 5 的记录锁
  • update t set d=1 where k = 7 普通索引上的 (5,10) 间隙锁
  • update t set d=1 where k = 5 普通索引上的 (0,5] 临键锁 (5,10) 间隙锁
  • update t set d=1 where k
  • update t set d=1 where k >=10 普通索引上的 (5,10] 临键锁 (10,~] 的临键锁
  • update t set d=1 where k >=10 and k
  • update t set d=1 where id >=10 and id 8.0.22 之前的主键索引上的范围查找,会访问到第一个不满足条件的值 15 为止。

  大数据 最新文章
实现Kafka至少消费一次
亚马逊云科技:还在苦于ETL?Zero ETL的时代
初探MapReduce
【SpringBoot框架篇】32.基于注解+redis实现
Elasticsearch:如何减少 Elasticsearch 集
Go redis操作
Redis面试题
专题五 Redis高并发场景
基于GBase8s和Calcite的多数据源查询
Redis——底层数据结构原理
上一篇文章      下一篇文章      查看所有文章
加:2022-04-26 11:47:11  更:2022-04-26 11:49:03 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 4:09:11-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码