4

一文带你了解MySql并发事务中的数据库锁、隔离级别、MVCC

 3 years ago
source link: https://leishen6.github.io/2021/03/05/mysql_mvcc_lock/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

最近同事开发时遇到了一个事务阻塞的问题,通过网上查询发现关于MySQL事务、锁这一块的资料都比较絮乱,让人看的云里雾里,所以借着这个机会,刚好也对这一块内容做一个总结梳理,希望能比较全面去写一下MySQL的并发事务处理。

本文主线:

  • 简述事务的特性与隔离级别

  • 聊聊MySql中各种类型的锁

  • 然后再聊聊MVCC是个什么东东

  • 接着再聊聊数据库锁的触发及升级,以及死锁

  • 最后说下出现锁问题时的常见排查命令

简述事务的特性与隔离级别:

在讲锁之前,必须要先聊聊 「事务的特性与隔离级别」 ,因为锁机制的存在是为了保证事务对应隔离级别下的特性。

「事务具有以下几个特性:」

「说完特性,再聊聊MySql中的几种事务隔离级别:」

RU 读未提交:

顾名思义,在这种隔离级别下,当多个事务并行对同一数据进行操作时,会读取未提交的数据,也被称之为 「脏读」

这种隔离级别因为会出现脏读现象,所以在实际场景中很少用。

RC 读提交:

一个事务只能看见已经提交事务所做的改变。

但这种隔离级别会出现 「不可重复读」 现象,即在一个事务内,多次读同一数据,在这个事务还没有结束时,如果另一个事务恰好修改了这个数据,那么,在第一个事务中,两次读取的数据就可能不一致。

RR 可重复读:

这是MySQL的 「默认事务隔离级别」 ,在这种隔离级别下,解决了RC存在的不可重复读问题,确保在同一事务中,会看到同样的数据行。

但可能会出现 「幻读」 ,即当一个事务在执行读取操作,第一次查询数据总量后,另一个事务执行了新增数据的操作并提交后,这个时候第一个事务读取的数据总量和之前统计的不一样,就像产生幻觉一样。

SERIALIZABLE 串行化:

此隔离级别是四种隔离级别中最高的级别,解决了 「脏读、可重复读、幻读」 的问题。

但是性能最差,它将事务的执行变为顺序执行,与其他三个隔离级别相比,在并行事务执行过程中,后一个事务的执行必须等待前一个事务结束。

MySql中各种类型的锁:

在MySQL中,按锁类型划分,有以下种类:

提到锁到种类,需要提一下MySQL到存储引擎,MySQL常用引擎有 「MyISAM和InnoDB」 ,而InnoDB是mysql默认的引擎。MyISAM是不支持行锁的,而InnoDB支持行锁和表锁。

MyISAM 存储引擎下表锁:

MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁;

读锁会阻塞对同一张表的写操作,而写锁既会阻塞对同一张表的写操作,也会阻塞此表的读操作。

排他锁、共享锁、意向锁 是什么东东?

「排他锁:」

通常我们在InnoDB存储引擎中对表执行一个更新操作,针对这一行数据会持有排他锁;

持有排他锁时,不允许再在数据行上添加写锁与读锁,其他事务对此行数据的读、写操作都会被阻塞,只有当前事务提交了,锁释放了才允许其他事务进行读写,达到避免 脏读 的效果。

「共享锁:」

主要是为了支持并发的读取数据而出现的,当一个事务持有某一数据行的共享锁时,允许其它事务可以再获取共享锁,但不允许其它事务在此基础上获取排他锁;

也就是说,在持有共享锁时,多个事务可以同时读取当前数据,但是不允许任何事务同时对当前数据进行修改操作,阻塞添加排它锁。

「意向锁:」

首先需要明白一点,意向锁的作用是在 表上 的,当一个事务需要获取共享锁或排他锁时,首先要获取对应的意向锁;

为什么要这样做呢?举个例子,假设在事务A中,对某一行数据添加共享锁,这一行只能读,不能写;此时事务B申请获得表的写锁,假如加锁成功,那么事务B将能够对整个表的数据进行读写,与事务A冲突,这种操作肯定是不允许的呢;

所以MySQL会在申请共享锁或者排他锁的时候,先获取对应的意向锁,也就是说,你要操作表中的某一行锁数据,先要看看整个表能不能被操作;意向锁的申请是由数据库完成的,不需要人为申请。

Innodb 存储引擎下的行锁:

上文对几种锁类型进行了简要分析,其实平时工作开发中接触到最多的还是行锁,行锁的实现有以下几种:

注意:在Innodb 存储引擎中,行锁的实现是基于索引的

「Record Lock(记录锁):」

它是会锁住索引记录,比如 update table where id = 1, id 是主键,然后在聚簇索引上对 id=1 的个索引记录进行加锁;

「Gap Lock(间隙锁):」

实质上是对索引前后的间隙上锁,不对索引本身上锁,目的是为了防止幻读。

当使用范围条件查询而不是等值条件检索数据,并请求排他锁、或共享锁时,对于该范围内不存在的记录,不允许其修改插入。

举个例子:当表中只有一条id=101的记录,一个事务执行select * from user where user_id > 100 for update;此时另一个事务执行插入一条id=102的数据是会阻塞的,必须等待第一个事务提交后才能完成。

间隙锁是针对事务隔离级别为可重复读或以上级别的。

「Next-Key Lock:」

Next-Key Lock 是 记录锁和间隙锁 的结合,会同时锁住记录与间隙。

在innodb存储引擎中,如果没有通过 索引项 进行查询时:

①、在RR隔离级别下,会以Next-Key Lock的方式对数据行进行加锁,通过 行锁+间隙锁 实现了 "锁表" 的效果,但请记住这不是添加的表锁;

②、而在 RU、RC 隔离级别下还是只会锁行记录,为什么呢?因为在innodb存储引擎下的四种事务隔离级别中都支持行锁,但是间隙锁只存在于RR、Serializable 两种隔离级别下。

可以通过下面这篇文章了解为什么在RR隔离级别下会实现"锁表"的效果,而在RC隔离级别下只会锁行记录: 互联网项目中mysql应该选什么事务隔离级别

MVCC 是什么:

锁机制可以控制并发操作,来保证一致性,但是系统开销会很大;在RC、RR的隔离级别下,MySQL的InnoDB存储引擎通过 MVCC (多版本并发控制) 机制来解决幻读。

「使用MVCC时具体的体现是什么呢?」

使事务在并发过程中,SELECT 操作不用加锁,读写不冲突从而提高性能。

「那么实现MVCC机制的原理是什么呢?」

其原理是通过保存数据在某个时间点的快照来实现的;通过在每行记录后面保存隐藏列来存放事务ID,这样每一个事务,都会对应一个递增的事务ID。

假设三个事务同时更新来同一行数据,那么就会对应三个数据版本,但实际上版本1、版本2并不是物理存在的,而是通过关联记录在undo log 回滚日志中,这样就可以通过undo log找回数据的历史版本,比如回滚的操作,会使用上一个版本的数据覆盖数据页上的数据。

「举例一个RR隔离级别下快照读的例子:」

开启事务A按条件A查询到两条数据,此时事务B再插入1条数据且满足条件A的数据,并提交事务;

此时事务A再按条件A进行查询,查询到的依然是两条数据,也就是说,事务A查询到的并不是当前最新的数据版本(三条数据),而是通过MVCC实现的历史快照版本;这也是可重复读的实现。

「介绍了完读操作,再举例一个RR隔离级别下 更新 写操作的例子:」

注意:对数据修改的操作(update、insert、delete)都会读到已提交事务的最新数据,因为这就是 当前读。

假设事务A执行一个更新语句,满足更新条件A(条件A字段无索引或者存在非唯一索引)的数据是2条,更新成功后不提交事务;

此时事务B插入一条新的满足条件A的数据时会被阻塞的,为什么呢?

因为这里在事务A更新时使用到了Next-Key Lock锁,它会使用行锁+间隙锁实现了"锁表",所以后面事务B再进行插入数据时会被阻塞的;这也防止了幻读的出现。

这里如果看的不是很明白的话,可以同时再参考下此文章,此文章详细分析了加锁情况:惊!史上最全的select加锁分析(Mysql)

MVCC只在RC和RR两个隔离级别下支持;其他两个隔离级别和MVCC不兼容,因为 RU总是读取最新的数据行,而不是符合当前事务版本的数据行;而S ERIALIZABLE 则会对所有读取的行都加锁,是当前读,也是读取的最新数据。

数据库锁的触发及升级,以及死锁:

数据库锁的触发及升级:

什么时候会出现DeadLock:

「什么是死锁呢?」

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

「举例说明:」

事务A获取 id=20的锁,事务B获取id=30的锁,然后,事务A试图获取id=30的锁,而该锁已经被事务B持有,所以事务A等待事务 B释放该锁,然后事务B又试图获取id =20 的锁这个锁被事务 A 占有,于是两个事务之间相互等待,这就会导致死锁。

死锁的场景还有许多,归根结底,都是因为多个事务想要获取的锁互斥且获取的顺序不一致所造成。

「如何避免死锁呢?」

通常Record Lock引起的死锁问题开发时都会比较小心,但Gap Lock可能导致死锁的问题通常会被忽略,所以这一点要多加注意,另外就是建立合适的索引,如果没有索引,那么在操作数据时会锁住每一行,会增大死锁的概率。

出现锁问题时的常见排查命令:

「show open tabbles:」

SHOW OPEN TABLES where In_use > 0:查看哪些表被锁了

「show status like 'table%':」

  • table_locks_waited:出现表级锁定争用发生等待的次数,此值高说明存在验证的表记锁争用情况

  • table_locks_immediate:表示立即释放表锁的次数

「show status like 'innodb_row_lock%':」

  • Innodb_row_lock_current_waits:当前正在等待锁定的数量

  • Innodb_row_lock_time:系统启动到现在锁定总时间

  • Innodb_row_lock_time_avg:每次等待话费的平均时间

  • Innodb_row_lock_time_max:系统启动到现在等待最长一次所花时间

  • Innodb_row_lock_waits:系统启动后到现在共等待次数

「information_schema:」

information_schema是MySQL专门记录性能信息的库,在5.7版本后默认打开。

  • SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS: 查看当前InnoDB的锁的信息,会显示是什么锁类型,属于那个事务ID

  • SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX: 查看InnoDB事务ID,会显示是什么操作和一些常规信息,例如是否在运行running,还是等待锁

  • SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS:查看InnoDB锁的等待时间,和等待的是哪个事务ID的锁

❤ 点赞 + 评论 + 转发 哟

如果本文对您有帮助的话,请挥动下您爱发财的小手点下赞呀,您的支持就是我不断创作的动力,谢谢啦!

您可以微信搜索 【木子雷】 公众号,大量Java学习干货文章,您可以来瞧一瞧哟!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK