MySQL并发控制下的|事务|MVCC|锁机制|解读

在这里插入图片描述

一、前言

随着业务发展,对数据库的并发性能要求也越来越高,不仅要做到高并发还需要在保障数据安全,那么今天我们聊一聊 MySQL 在高并发下事务、MVCC、锁机制是如何在高并发情况下维护数据的安全。

二、事务 ACID

  • 为什么需要事务:事务是为了保障用户的数据操作对数据是安全的。比如我们的银行卡余额,我们希望对它的操作是稳定准确的,而且绝对安全
  • ACID:事务的四大特性原子性一致性隔离性持久性
  • 原子性:原子性是指一个事务要么全部执行,要么完全不执行。主要是由 innodb 引擎中的 undo 回滚日志来维护
  • 隔离性:事务在操作过程中不会受到其它事务操作的影响。主要由事务的隔离级别锁机制共同维护。
  • 持久性:事务操作的结果是具体持久性的,通俗来讲就是提交事务后会持久化存储(落盘)。主要是由 redo log 来维护。
  • 一致性:事务在开始和结束时,数据始终保持一致。由原子性、隔离性、持久性共同维护。

三、多版本并发控制 MVCC

  • 介绍:数据库的核心方向就是高并发,MySQL 通过并发控制技术维护高并发的环境下数据的一致性和数据安全。MySQL 并发控制有两种技术方案锁机制(Locking)多版本并发控制 (MVCC)

  • 锁机制:通过锁机制可以维护数据的一致性,但是整体业务场景大多是读-读读-写写-写,三类并发场景,看似容易融合到业务场景后也比较复杂。通过锁机制主要可以帮助我们解决写-写读-读 场景下的并发安全问题 则 MVCC 主要帮助解决 读-写 问题。

  • MVCC:多版本并发控制,侧重优化读-写业务的高并发环境。可以解决写操作时堵塞读操作并发问题。

  • 一致性非锁定读:指 innodb 引擎通过多版本并发控制的方式来读取,当前执行时间数据库中的行数据。读取正在进行 update 或 delete 操作的行,不会等待锁释放,而是会读取该行的快照数据。快照就是指该行之前的版本数据,主要靠 undo 日志来实现,而 undo 是用于数据库回滚,因此快照读本身是没有开销的。后台 Purge 线程也会自动清理一些不需要的 undo 数据。

  • MVCC 两类读操作:分为两类读情况 快照读(Snapshot Read)当前读(Current Read) 快照读是读取数据的可见版本而当前读则是读取当前数据的最新版本需要加锁从而保障其它事务不会修改当前数据。

  • MVCC 实现策略:我们在设计表过程中通过不会直接删除数据而是设定一个字段来标记,从实现逻辑意义上的删除。MVCC 的实现方式也与此类似。这种数据管理方式叫数据生命周期管理,其中两个指标就是标记数据的变化标记数据可用状态

  • MVCC 下的 DML过程演示:

    Insert进行 insert 操作,事务 id 假设为 1

    id name create version delete version
    1 test 1

    UpdateMVCC 会先将当前记录标记为已删除在 delete version 字段下设置版本号(原来为空),然后新增一行数据,写入相应的版本号,此时为新版本号为 2 和上一条数据的 delete version 一致,比如将 name 修改为 fantasy,如下表:

    id name create version delete version
    1 test 1 2
    1 fantasy 2

    Delete直接将当前数据的 delete version 打上版本号标记为删除

    id name create version delete version
    1 test 1 2
    1 fantasy 2 3
  • MVCC 解析:刚才只是在逻辑层面上介绍 MVCC 的运作方式 create version 和 delete version 维护的是数据的版本信息和数据可用状态,而实际上还有一个字段是用户 undo回滚的指针,接下来我们介绍源码中 MVCC 的实现方式。默认会给每张表加入三个隐藏字段(内部属性)

    DB_TRX_ID:占 6 个字节,记录每一行最近一次修改它的事务 ID
    DB_ROLL_OIR:占 7 个字节,记录指向回滚段 undo 日志的指针
    DB_ROW_ID:占 6 个字节,当写入数据时,自动维护自增列

    将三个字段结合就可以标记数据的周期性和,并定位到对应的事务。这就引出 innodb 中实现 MVCC 两个重要模块 undo 日志用来存储数据的变化 Read View 用来做可见性判断的, 里面保存了对本事务不可见的其他活跃事务

  • 注意:对于 innodb 来讲,无论是更新还是删除,都只是设置行记录上的 deldte BIT 来标记,而并不是真正的删除记录,后续这些记录的清理就需要 Purge 线程来做。还需要注意的是 MVCC 只能在 RCRR 隔离级别下使用,RU读未提交状态,所以不存在版本问题,而串行化则会对读取的数据行加锁。

四、隔离级别

  • 为什么需要隔离级别?
    事务之间如果不互相隔离,那么就会出现脏读不可重复读幻读

  • 简单概括脏读、不可重复读和幻读:
    在前,在后:脏读;
    在前,在后:不可重复读;
    在前,在后,再:幻读。

  • 隔离级别与并发问题的关系如下:

    级别 脏读 不可重复读 幻读
    读未提交(READ-UNCOMMITTED)
    不可重复读(READ-COMMITTED)
    可重复读(REPEATABLE-READ) 🆚
    串行化(SERIALIZABLE)

    其中串行化隔离级别虽然解决了所有数据问题,但是却带来了并发的性能问题,而读未提交的隔离级别违反了基础事务的安全处理要求,所以我们在选择隔离级别时都会在 RC 和 RR 中选择,MySQL 默认隔离级别为 RR 级别。

    -- 查询数据库中的隔离级别
    select @@transaction_isolation;
    -- 临时设置MySQL数据库中的隔离级别
    set global transaction_isolation='READ-COMMITTED';
    
  • RC 和 RR 的区别:RC 在事务可以读取到其它事务提交的事务数据,而对于 RR 级别来讲,它会保证在一个事务中数据多次的查询结果是不变的,尽管其它事务已经提交了改动。从锁的角度来讲 RC 的性能会优于 RR。

  • RC 和 RR 级别下的快照读:RC 级别下的快照读总是会读取被锁定行的最新版本的一份快照数据,而 RR 级别下的快照读总是会读取事务开始时行版本的数据。这个怎么理解呢?请看如下案例:


    首先打开 MySQL 会话 Session 1 开启一个事务然后查询一条数据


    在这里插入图片描述


    开启另一个 MySQL 会话 Session 2 模拟并发场景,开启事务修改 id1 = 3 中的 id2 为 8023


    在这里插入图片描述


    Session 2 中我们修改了 id2 20170831 为 8023 但是还未提交,此时 id1 = 3 的行已经加上了一个 X 排它锁,此时再读取 Session 1 会话中的 id1 = 3 的记录根据 innodb 引擎的特性,即在 RR 和 RC 事务隔离级别下会使用 “非锁定一致性读” 也就是快照读。接者 Session 1 未提交的事务再此运行查询 id1 = 3 的 SQL 语句,此时无论此时隔离级别是 RR 和 RC 结果都如下图:


    在这里插入图片描述


    接者我们提交 Session 2 中的事务。


    在这里插入图片描述


    Session 2 中的事务提交后,在 Session 1 中再次执行查询 id1 = 3 的 SQL 语句,此时在 RRRC 级别下运行的结果就不同了,RC 事务的隔离级别,总是读取最新版本快照数据,因为 Session2 提交了事务更新快照版本,所以 Session 1 在事务中可以读取 Session 2 中已经提交的改动,结果如下:


    在这里插入图片描述


    因为 RR 级别读取数据快照总是读取开始事务前的行版本的快照数据,所以尽管 Session 2 更新了快照版本,RR 级别下事务未提交之前不会受到影响,所以 RR 级别下两次读取数据的结果都相同


    在这里插入图片描述

五、锁机制

  • 什么是锁?
    锁是计算机协调多个进程或线程并发访问某一资源的机制。

  • innodb 两种锁:
    共享锁 S:允许一个事务去一行,阻止其它事务获得相同数据集的排它锁。通俗来讲就是可以重复读,没读完时不允许写。
    排它锁 X:允许获得排它锁的事务更新数据,阻止其它事务获得相同数据集的排他锁共享锁。通俗来讲就是写的时候不允许其它事务写和读。

  • 发现问题:有两个事务 A 和 B,事务 A 锁住了表中的一行数据,加上行锁 S,即这一行只能读不能写。之后事务 B 又申请整张的写锁 (mysql 中可以使用 lock table xxx write 锁表),正常逻辑来将,事务 B 就可以修改表中任意行数据,包括事务 A 锁住的那一行数据,实际情况则会发生锁冲突,现在就需要一种机制来判断是否有行锁,比如锁表前先判断每一行数据是否有行锁,但是这种方案在随着数据量增大代价会无限放大,肯定是不取的,而意向锁就是来解决冲突的协调者

  • 意向锁工作流程:
    事务 A 首先需要申请表的意向锁,成功后申请一行的行锁。
    事务 B 申请排它锁,但是发现表中已经有意向共享锁,说明表中的某行数据已经被锁定,此时申请的写锁会被堵塞

  • 意向共享锁 IS:事务给某行数据加入共享锁前,需要先申请意向共享锁。通俗来讲,就是一个数据行加共享锁前必须要先取得该表的意向共享锁。

  • 意向排它锁 IX:与上类似,加入排它锁前需要先获得意向排它锁

  • innodb 行锁:行锁是通过给索引加锁来实现的,不用担心表中是否创建了索引,如果有主键 MySQL 会在主键上创建聚簇索引用于回表查询,如果没有主键则考虑 unique 约束的字段,如果前面两种都不满足,则会创建隐藏列 RowID 作为聚簇索引。如果不通过索引检索数据,那么 innodb 引擎会对表中所有的数据加锁,实际效果与表锁相同,所以要尽可能让所有数据都通过索引来完成,避免行锁升级为表锁。

    innodb 下的三种行锁:
    行锁(Record Lock):对索引加锁,即锁定一行记录。
    间隙锁(Gap Lock):对索引项之间的间隙、对第一条记录前的间隙或最后一条记录后的间隙加锁,即锁定一个范围的记录,不包含记录本身。
    Next-Key Lock:锁定一个范围的记录幷包含记录本身。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章