• MYSQL的多版本并发控制MVCC(Multi-Version Concurrency Control)


    1、概述

    MVCC是一种用于数据库管理系统的并发控制技术,允许多个事务同时访问数据库,而不会导致读写冲突。也就是说在读写的时候,线程不用去争抢读写锁。因为加锁的过程比较耗性能。
    当然很多时候还是必须的,不能避免,比如说,去ATM机取钱的时候,同时又在手机APP上进行提现,这种操作就需要加锁,不能让其同时提现,一次只能一个操作,而且只有在ATM取钱这个事务被提交之后才能做其他操作。

    处理并发的场景无外乎三种:

    读、读:这个不需要做控制,因为数据没有变化
    读、写:存在线程安全问题,可能出现脏读、幻读,不可重复读
    写、写:存在线程安全问题,可能出现更新丢失的情况

    这里介绍的MVCC是在存储引擎为InnoDB实现的,目的也是为了提高数据库的并发性能,不使用加锁的方式去处理读、写并发。

    2、MVCC特点

    这里的读操作,有两种方式:
    快照读:SELECT语句,在读写的时候不用加锁,所以效率很高,但也存在读取的时候有更新操作,可能会读到历史数据。

    当前读:读取的是最新数据,是一种悲观锁的操作。它会对当前读取的数据进行加锁,避免其他事物对其进行写操作。主要包括以下几种操作:

    select lock in share mode(共享锁)
    select for update(排他锁)
    update(排他锁)
    insert(排他锁)
    delete(排他锁)

    3、准备数据

    在介绍之前,没有安装MYSQL的,可以先进行安装,下载地址:https://dev.mysql.com/downloads/
    安装好了之后,我们就新建库与表,插入一些数据来做个测试

    1. CREATE DATABASE mydb;
    2. USE mydb;
    3. CREATE TABLE `tb1` (
    4. `id` int(12) NOT NULL AUTO_INCREMENT,
    5. `name` varchar(32) DEFAULT NULL,
    6. PRIMARY KEY (`id`)
    7. ) ENGINE=InnoDB DEFAULT CHARSET=UTF8;
    8. INSERT INTO tb1(name) VALUES ('XIAO1'),('XIAO2'),('XIAO3');
    9. CREATE TABLE `tb2` (
    10. `id` int(12) NOT NULL AUTO_INCREMENT,
    11. `name` varchar(32) DEFAULT NULL,
    12. PRIMARY KEY (`id`)
    13. ) ENGINE=myisam;
    14. INSERT INTO tb2(name) VALUES ('LAO1'),('LAO2'),('LAO3');

    这里我特地创建了两张表,分别是表tb1对应的是InnoDB引擎,表tb2对应的是myisam引擎,创建之后,也可以看到两者的数据格式也是不一样的,我们先查询下,新建的数据库mydb以及保存的数据在什么地方:

    1. mysql> show variables like '%datadir%';
    2. +---------------+---------------------------------------------+
    3. | Variable_name | Value |
    4. +---------------+---------------------------------------------+
    5. | datadir | C:\ProgramData\MySQL\MySQL Server 8.0\Data\ |
    6. +---------------+---------------------------------------------+
    7. 1 row in set, 1 warning (0.00 sec)

    然后可以看到,InnoDB引擎对应的是:tb1.ibd myisam对应的是:tb2.MYD(数据)、tb2.MYI(索引)、tb2_402.sdi(表结构)[这个在以前的版本没有出现]
    MySQL5.5之后都是默认为InnoDB引擎

    4、MVCC原理

    MYSQL存储的数据中,除了我们显式定义的字段,还隐含着两个字段。
    trx_id:事务id,每进行一次事务操作,就会自增1。
    roll_pointer:回滚指针,用于找到上一个版本的数据,结合undolog进行回滚。


    我们用SELECT读数据时,这一时刻的数据会有很多个版本【比如上图四个版本】,但我们并不知道读取哪个版本,依赖ReadView来对我们进行版本的选择,通过ReadView我们就能够知道读取哪个版本。我们来看下这个ReadView的定义:

    1. class ReadView {
    2. /* ... */
    3. private:
    4. trx_id_t m_low_limit_id; /* 大于等于这个 ID 的事务均不可见 */
    5. trx_id_t m_up_limit_id; /* 小于这个 ID 的事务均可见 */
    6. trx_id_t m_creator_trx_id; /* 创建该 Read View 的事务ID */
    7. trx_id_t m_low_limit_no; /* 事务 Number, 小于该 Number 的 Undo Logs 均可以被 Purge */
    8. ids_t m_ids; /* 创建 Read View 时的活跃事务列表 */
    9. m_closed; /* 标记 Read View 是否 close */
    10. }

    字段的解释:

    m_low_limit_id:目前出现的最大事务ID+1(下一个将被分配的事务ID)。大于等于这个ID的数据版本均不可见,也就访问不到。

    m_up_limit_id:活跃事务列表m_ids中最小的事务ID,如果为空,则m_up_limit_id为m_low_limit_id。小于这个ID的数据版本均可见。

    m_ids:ReadView创建时其他未提交的活跃事务ID列表。创建ReadView时,将当前未提交事务ID记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。m_ids 不包括当前事务自己和已提交的事务(正在内存中)

    m_creator_trx_id:创建该ReadView的事务ID

    5、实践操作

    开四个终端,新建三个事务,两个写操作,一个读操作,还有一个就是单纯的查询。大家可以根据不同的进入时间来了解这个过程,会产生未提交和已提交事务状态,对应的查询信息都是不一样的,具体代码分别如下:

    1. BEGIN;
    2. UPDATE tb1 SET name='XXX' WHERE id=1;
    3. UPDATE tb1 SET name='YYY' WHERE id=1;
    4. COMMIT;
    5. BEGIN;
    6. UPDATE tb1 SET name='ZZZ' WHERE id=1;
    7. UPDATE tb1 SET name='QQQ' WHERE id=1;
    8. DELETE FROM tb1 WHERE id=5;
    9. COMMIT;
    10. BEGIN;
    11. SELECT * FROM tb1 WHERE id=1;
    12. COMMIT;
    13. SELECT * FROM tb1;

    6、小结

    对于这种读写并发,以及对性能的要求,大家需要看实际业务情况来做决定,其中这里主要是介绍InnoDB引擎,这个要高效很多,在以前的旧版本可能大家使用MyISAM这个更多,因为性能很好,不过不支持事务操作,所以很多场景也就不适应,MYSQL5.5版本之后就是默认InnoDB引擎了。
    最后大家也可以尝试在表tb2中去尝试下,看下是什么结果,因为这张表使用的是MyISAM引擎,就起不到作用了。
    另外需要注意的是,事务的提交是默认自动的,有些时候需要关闭,将默认的1修改为0:

    1. SET AUTOCOMMIT=0;
    2. SELECT @@AUTOCOMMIT;

    比如说对于脏读的情况,我们需要当前读,也就是需要排它锁:

    1. SET AUTOCOMMIT=0;
    2. BEIGIN;
    3. DELETE FROM tb1 WHERE id=2;

    这种情况如果我们的事务在这个时候进去,对这个id=2进行读写操作,就会出现脏读的情况,这个时候就需要使用SELECT FOR UPDATE,等待事务处理完毕之后再做相应的操作。

    对于脏读这种情况,很常见,比如说事务在做删除操作,这个时候记录已被删除但是还没有提交事务,如果进行查询操作就会出现脏读,如下:

    1. SET AUTOCOMMIT=0;
    2. BEGIN;
    3. SELECT * FROM tb1 WHERE id=2 FOR UPDATE;
    4. UPDATE tb1 SET name='QQQ' WHERE id=1;
    5. COMMIT;
  • 相关阅读:
    flutter LINK : ...fatal error LNK1168: �޷���...
    什么是物联网(IoT)?
    【C#】自动升级工具
    一篇文章就足够解决大数据实时面试
    谷粒学苑_第八天
    MySQL 慢查询
    npcap开发指南
    1636. 按照频率将数组升序排序-c语言自定义二重排序
    C#可空类型
    Python FastApi 解决跨域及OPTIONS预请求处理
  • 原文地址:https://blog.csdn.net/weixin_41896770/article/details/133092273