• 面试官:小伙子你给我说说MySql并发事务处理细节


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

    本文主线:

    • 简述事务的特性与隔离级别
    • 聊聊MySql中各种类型的锁
    • 然后再聊聊MVCC是个什么东东
    • 接着再聊聊数据库锁的触发及升级,以及死锁
    • 最后说下出现锁问题时的常见排查命令

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

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

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

    0e60b050ec5cc777aba20f9c1c2c7b11.png

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

    7e73b10fd7f1b6618a18bd80effcfde1.png

    RU 读未提交:

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

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

    RC 读提交:

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

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

    RR 可重复读:

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

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

    SERIALIZABLE 串行化:

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

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

    MySql中各种类型的锁:

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

    42451b74176bb0c142a704252c85c79f.png

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

    MyISAM 存储引擎下表锁:

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

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

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

    「 排他锁: 」

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

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

    「 共享锁: 」

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

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

    「 意向锁: 」

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

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

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

    Innodb 存储引擎下的行锁:

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

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

    6761f5c27a8310ccde9f7b17b0dde1e0.png

    「 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 则会对所有读取的行都加锁,是当前读,也是读取的最新数据。

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

    数据库锁的触发及升级:

    c6b3a3102e806bd6ddb8101bf4b476ec.png fbc4f23723f9621023b0c339d1539cc5.png

    什么时候会出现DeadLock:

    「 什么是死锁呢? 」

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

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

    「 举例说明: 」

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

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

    「 如何避免死锁呢? 」

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

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

    8538a93b34c98fbe9508394b47fa1d6e.png

    「 show open tabbles: 」

    c2ba74c74b7e61e48c50b376064eb668.png

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

    「 show status like 'table%': 」

    4e61d8b624d06aba5c5910e8bb1dd5b7.png
    • table_locks_waited:出现表级锁定争用发生等待的次数,此值高说明存在验证的表记锁争用情况
    • table_locks_immediate:表示立即释放表锁的次数

    「 show status like 'innodb_row_lock%': 」

    9e29e1f6069c8d5e1acd510d68474ea2.png
    • 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的锁

    原文链接:https://leishen6.github.io/2021/03/05/mysql_mvcc_lock/

    如果觉得本文对你有帮助,可以转发关注支持一下

  • 相关阅读:
    【Spring Security 系列】(一)理论篇
    python Clickhouse 分布式表介绍和创建、插入和查询数据,以及解决遇到的问题
    Python教程:Pandas删除数据的4种情况
    Android:DMB-T/H,开发记录
    【python语法】@staticmethod、@classmethod、@property使用方法
    C 语言 math.h 库介绍
    Aztec的隐私抽象:在尊重EVM合约开发习惯的情况下实现智能合约隐私
    使用 Web HID API 在浏览器中进行HID设备交互(纯前端)
    联合搜索:搜索中的所有需求
    【复旦邱锡鹏教授《神经网络与深度学习公开课》笔记】卷积
  • 原文地址:https://blog.csdn.net/m0_64435718/article/details/124950869