我的sql没问题为什么还是这么慢|MySQL加锁规则

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS PostgreSQL,集群系列 2核4GB
简介: 导致数据库访问速度变慢的原因有很多:sql语句编写不规范、数据库服务器的性能差、网络状况不佳等,但是本文所侧重的点在于探究MySQL的锁机制,在其中发挥了什么作用。

前言

前阵子参与了字节跳动后端青训营,其中大项目编写涉及到数据持久化一般选择使用MySQL。由于时间原因,数据库使用我选择了无脑三板斧:1. 建立了索引加速查询、2. 关闭自动提交事务、3. 在需要确保原子性的数据库操作之间手动创建和提交事务

这么一看,仿佛即使是实际开发也与你此前听闻的一些MySQL相关名词:读写锁间隙锁多版本并发控制redo logbin logundo log毫不相干,在讲本文的主题之前,我先引入一个真实场景。

某次不够规范的小组开发过程中,开发成员选择测试程序的方式比较原始,大家共享一个测试数据库,各自使用测试账号进行接口的测试,这就意味着数据库中的记录在某一时刻有可能被多个事务访问,甚至在其他人测试的同时,某张数据表的结构被另一位同学修改。

多事务并发访问,反映到开发者这边,就是查询接口有时速度很慢。如果你是直接使用数据库管理工具操作数据库表数据/结构,对应的就是Navicat不时的陷入较长时间的无响应状态。

当然导致数据库访问速度变慢的原因有很多:sql语句编写不规范、数据库服务器的性能差、网络状况不佳等,但是本文所侧重的点在于探究MySQL锁机制,在其中发挥了什么作用。

相信在完成本文的阅读之后,你会明白上面的场景的发生,可能是MySQL的锁机制从中作祟。

MySQL的锁有哪几种

全局锁

MySQL可以通过显式命令对整个数据库实例加全局读锁:

image-20220716162011651

此时整个数据库处于只读状态,所有数据记录的更新、数据库/表结构的改动提交都会被阻塞,这可以用于全库的数据备份。

表级锁

表锁

表锁可以通过以下显式命令实现对一个表添加读/写锁:

image-20220716162037503

如果A线程为t1表添加了读锁,为t2表添加了写锁。则其他线程将只能读t1,写t1被阻塞;读/写t2都会被阻塞。而A线程在执行unlock tables之前,也只能执行读t1、读/写t2的操作。

元数据锁(metadata lock)

MDL锁不需要显式使用,在访问一个表的时候会被自动加上,并且当事务完成提交时释放。当对一个表数据做CRUD操作的时候,自动加MDL读锁;当对该表结构作出改动的时候,自动加MDL写锁。

  • 读锁之间不互斥,因此多个线程才可以同时访问一张数据表。
  • 读写锁之间、写锁之间是互斥的(被读锁占用时,加写锁的线程被阻塞/被写锁占用时,加读锁/写锁的线程都被阻塞),这也是为了确保表结构的修改和表的数据的操作不发生冲突。

这里展示一个多线程并发操作同一个数据表的案例:

image-20220716162453691

这里线程B会因为线程A的事务还没有提交,而添加列的操作需要获取MDL写锁因此被阻塞,同时线程C申请MDL读锁的请求又被阻塞在了线程B申请MDL写锁的请求之后,此时表t在线程A事务提交之前,完全丧失了读写能力

或许此时你已经对于为什么多人调试程序时数据库访问不时出现卡顿有了一些自己的想法,当然这只是锁机制的冰山一角

行级锁

通过上面的讲解,我们明白了,所谓的读写锁并不是单指一个锁叫读锁/写锁,而是指不同粒度的锁有读锁和写锁两种状态,允许的并发程度也有所不同。行级锁也是如此(针对记录行的锁,锁粒度进一步缩小),行锁的存在也使得事务并发访问数据库的性能进一步的提高,并且依旧有读写锁之分,下面介绍。

但区别于全局锁和表级锁,MySQL行锁是由各个存储引擎自己实现的,并不是所有的存储引擎都支持行锁(MyISAM不支持),由于现在MySQL用户大多选择使用InnoDB存储引擎,所以本文将以InnoDB引擎为默认选择

两阶段锁协议

在InnoDB事务当中,行锁在需要的时候添加,并且直到事务提交才释放(锁的添加和释放分两个阶段进行),举个例子:

image-20220716164447568

事务A(线程A)在提交之前,占有id=1这条行记录的写锁,事务B(线程B)修改同一行的操作将被阻塞。

死锁与检测

死锁原本是操作系统当中的概念,意思是多个线程都在等待其他线程释放自己需要的资源,使得这些线程陷入无限制的等待。

image-20220716165858634

在这个例子当中,线程A的事务和线程B的事物分别占有id=1id=2这两条记录的写锁,使得两个线程在试图获取其他线程占用的锁资源时陷入死锁。

InnoDB存储引擎默认开启了死锁检测,每个新来的被阻塞的线程,都会主动判断是否是自己的加入导致死锁(检测逻辑就是判断自己需要的行资源是否被别的线程的事务占有),时间复杂度O(n),一旦检测到,则回滚当前线程的事务,确保其他线程可以得到执行。

这里你会发现,如果同时有多个线程修改同一条记录,一旦并发度很高,则需要消耗O(n^2)时间去完成死锁检测,就会消耗大量CPU资源在死锁检测上,而使得数据库IO的性能下降。

此时你是否又对我最初给出的小组开发时访问数据库慢的场景有了自己的思考,其实在高QPS情况下,发生死锁检测的概率是大大高于小组开发场景的

因此控制热点记录的并发访问数量,是提升数据库IO性能的重要前提。

多版本并发控制(MVCC)

上面讲述了InnoDB的update操作会占用行记录的写锁,那么你自然而然就想到,select查询操作是否就占用了行记录的读锁呢?不完全正确,这就不得不提及MySQL的InnoDB引擎的用于控制事务隔离级别的多版本并发控制机制

简言之就是每条行记录值的变化是由一个链式的结构组织的,存放在undo log文件当中,undo log在事务发生回滚的时候,用于回溯事务对行记录的修改过程。

image-20220716180521920

而InnoDB存储引擎默认的事务隔离级别是可重复读(Read Repeatable),简单来说:就是当事务A启动期间,普通的select查询将无法访问到其他事务在此期间对表记录的改动。

关于多版本并发控制(MVCC)这里我没有过多深入讲解,详情给出我的另一篇文章:https://juejin.cn/post/7085185961239248927

快照读

image-20220716175347796

对于普通的查询操作,你大致了解InnoDB引擎管理的表的行记录变更是链式组织的,那么每一条记录就相当于一个个的快照,因此普通的select查询操作被称为快照读,会读取到自己可见的最近一个版本(但不一定是最新版本),快照读并不加锁(也就是没有获取读锁)。

至于具体读到哪个版本的快照,在上面链接给出的文章中有详细讲解。

当前读

image-20220716181610257

这里给出了两种不同的当前读方式,当前读可以读取到undo log版本链上的最新记录,不同之处在于,第一条sql获取了id=1这条行记录的读锁(在其他事务已经持有id=1行记录的写锁时将被阻塞);第二条select查询虽然也是当前读,但是获取了id=1这条记录的写锁(在其他事物已经持有id=1行记录的读/写锁时将被阻塞)。

上面讲解死锁检测的时候我用更新语句获得了行记录的写锁,而这里,通过增加for update后缀,可以使得当前读操作也获取行记录的写锁

间隙锁

间隙锁的出现解决了幻读问题,那么先简述一下幻读的概念,以及幻读有什么问题。

幻读概述

  • InnoDB引擎的可重复读隔离级别下,普通查询是快照读,不会看到其他并发事务插入的数据,因此幻读在当前读情况下才会出现。
  • 幻读指当前读场景下,查询到了其他并发事务新插入的行(读到其他事务对行记录的修改,并不属于幻读,因为当前读就是会读取到行记录的最新版本)。

幻读的问题

这里用一张表t的操作来描述幻读带来的问题。

image-20220717162029042

image-20220717161618445

以下的分析建立在没有间隙锁的情况下(只是为了分析所作的假设):

  • 事务A的第一个sql查询c=1的记录,获得(1,1),此时添加了for update,从语义上就是希望锁住所有c=1的行记录。
  • 并且在RR隔离级别下,所有扫描到的行数据都会加行锁,因为c字段没有索引,比较c=1的操作需要全表扫描,因此事务A的第一条sql在当前读的情况下,为整张表的3条行记录都添加了写锁。
  • 此时事务B并发插入了一条(2,1)的记录,并且成功。
  • 事务A的第二个sql依旧查询c=1的记录,获得(1,1)、(2,1)两条记录,从语义上违背了第一条sql的目的。(原本打算锁定所有c=1的记录,但是突然又冒出一条记录)

这里的核心问题就在于:即使所有扫描到的行记录都加上了锁,依旧无法阻止新记录的插入(因为要插入的记录不可能提前锁定),要避免幻读,就需要将记录之间的间隙锁定——间隙锁

Gap Lock

间隙锁在可重复读隔离级别下才有效,所以本文的描述都是基于RR级别(InnoDB存储引擎事务默认隔离级别),这里给出间隙锁配合行锁工作的一些规则:

  • 所有的锁是添加在索引上的
  • 加间隙锁的基本单位是next-key lock(前开后闭区间)
  • 查找过程中访问到的记录和区间才会加锁
  • 索引上的等值查询,给唯一索引加锁的时候,next-key lock退化为行锁
  • 索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock退化为间隙锁
  • 唯一索引上的范围查询会访问到不满足条件的第一个值为止

小结

本文概述了MySQL锁机制的工作情况,明确了锁有读/写之分。以及给出了一些会触发表锁、行锁的案例,同时,InnoDB存储引擎为了解决幻读问题,引入了间隙锁,用于锁定索引之间的间隙,防止当前读的出错。

还记得文章开头我抛出的实际开发案例吗,相信通过这篇文章的讲解,你对于多事务并发操作数据库时数据库访问性能下降的原因,已经有了不少自己的思考。

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
30天前
|
存储 关系型数据库 MySQL
优化 MySQL 的锁机制以提高并发性能
【10月更文挑战第16天】优化 MySQL 锁机制需要综合考虑多个因素,根据具体的应用场景和需求进行针对性的调整。通过不断地优化和改进,可以提高数据库的并发性能,提升系统的整体效率。
56 1
|
1月前
|
SQL 运维 关系型数据库
MySQL 运维 SQL 备忘
MySQL 运维 SQL 备忘录
46 1
|
22天前
|
SQL 关系型数据库 MySQL
MySql5.6版本开启慢SQL功能-本次采用永久生效方式
MySql5.6版本开启慢SQL功能-本次采用永久生效方式
34 0
|
22天前
|
SQL 关系型数据库 MySQL
mysql编写sql脚本:要求表没有主键,但是想查询没有相同值的时候才进行插入
mysql编写sql脚本:要求表没有主键,但是想查询没有相同值的时候才进行插入
31 0
|
30天前
|
存储 关系型数据库 MySQL
MySQL锁,锁的到底是什么?
【10月更文挑战第16天】MySQL 锁锁定的是与数据和资源相关的对象,其目的是为了保证数据的一致性、避免冲突,并在并发环境下合理协调事务或操作的执行。理解锁的对象和意义对于优化数据库性能、处理并发问题至关重要。
55 0
|
1月前
|
关系型数据库 MySQL 数据库
mysql锁详解
通过理解并合理运用MySQL中的锁机制,开发者可以有效管理数据库并发访问,平衡性能与数据一致性需求。更多关于MySQL锁的深入探讨和最佳实践,请参考专业的数据库管理资源[[深入MySQL锁机制详解
35 0
|
1月前
|
SQL 存储 关系型数据库
mysql 数据库空间统计sql
mysql 数据库空间统计sql
46 0
|
1月前
|
SQL 存储 关系型数据库
mysql SQL必知语法
本文详细介绍了MySQLSQL的基本语法,包括SELECT、FROM、WHERE、GROUPBY、HAVING、ORDERBY等关键字的使用,以及数据库操作如创建、删除表,数据类型,插入、查询、过滤、排序、连接和汇总数据的方法。通过学习这些内容,读者将能更好地管理和操
17 0
|
11月前
|
SQL 存储 关系型数据库
MySQL下使用SQL命令进行表结构与数据复制实践
MySQL下使用SQL命令进行表结构与数据复制实践
155 0
|
SQL 关系型数据库 MySQL
MySQl数据库第八课-------SQL命令查询-------主要命脉2
MySQl数据库第八课-------SQL命令查询-------主要命脉
下一篇
无影云桌面