MySQL_事务(MVCC机制详解)

MySQL 事务

引言

事务是数据库确保数据并发安全的机制,或者说,事务设计初衷是让用户无需手动确保数据安全,将该问题交给数据库自身解决即可,而MySQL内部具备多种事务级别,用以实现各种情况下的事务隔离机制

事务的性质

  1. 如同索引机制的实现与引擎相关般,在MySQL中,事务的具体实现也根据不同引擎有所区分,而 InnoDB 是 MySQL 默认的、主流的、完整支持事务的存储引擎,接下来介绍的事务机制均为 InnoDB 的行为
  2. 事务是原子的
  3. 若事务未被提交(如mysqld异常挂掉,客户端异常结束等),数据会自动回滚至事务的开始位置

事务的提交形式

事务具备两种提交方式,分别为手动提交和自动提交,一般情况下,手动提交常用于需要确保多条SQL语句为原子性的情况,而自动提交则用于确保单SQL原子使用

  • 手动提交:

    • 使用begin; / start transaction;命令均可启动一个事务,之后在commit前的所有SQL语句都处于该事务中
    • commit; 命令可提交事务
    • savepoint save1; 命令可创建一个保存点(save1为参数,自定义命名即可)
    • rollback to save1; 命令可回滚事务至保存点save1,若直接使用rollback; 命令可回滚事务至事务的开始位置
  • 自动提交:

    • 默认情况下自动提交是开启的,此时,单SQL就属于一个事务,会在单SQL开始前自动进行类似begin操作启动新事务,单SQL执行后自动使用"commit;"进行该语句的事务提交;若关闭自动提交,则会在所有SQL前启动新事务,用户手动commit后关闭该事务,即,从效果上来看,关闭自动提交 + 多SQL + commit 与 begin + 多SQL + commit的行为是一致的,均可保证commit前的所有SQL共同的原子性
    • 在MySQL中使用 show variables like 'autocommit';命令即可查询是否处于自动提交状态(OFF:关闭自动提交,ON:开启自动提交)
    • 使用set autocommit = 0; 命令可关闭自动提交
    • 使用set autocommit = 1; 命令可开启自动提交

因此,无论是否使用begin; / start transaction; 启动事务,所有的SQL都可确保在事务中进行

事务的隔离级别

事务的隔离级别指的是对于并发事务的操作行为,由于上面提到过"MySQL的InnoDB引擎中的所有SQL都是在事务中的",因此,事务的隔离级别可以简化理解为,多线程并发访问数据库的保护级别

隔离级别有4种,分别为:

  • 读未提交(RU):"读没有commit的内容",即并发事务访问同一数据时,一个事务在commit之前做的修改可以立即被另一事务看到
  • 读提交(RC):"读已提交内容",即并发事务访问同一数据时,一个事务中的修改在commit前不能被另一事务看到
  • 可重复读(RR):"在同一事务中,不同时刻使用相同select语句读取到的结果一定是相同的",即并发事务访问同一数据时,一个事务中的修改不能被另一事务看到,必须等待观测的事务commit后才能观测到另一事务的修改,注意,RR隔离级别为MySQL的默认隔离级别
    • 关于可重复读的使用场景,比如说,按对公司的贡献发工资,观测事务要执行两条select查询各个贡献的成员,有一个目标初始时符合第一条select,其贡献为B,然后在观测者事务执行第一条select后,出现新事务增加了目标的贡献,导致目标又符合第二条select,贡献为A,此时,若不可重复读就会导致为目标发放两份工资
  • 串行化:简单理解就是加互斥锁,确保事务串行操作,实际效果是,并发两个事务,两事务内部可同时使用select,效果为可重复读,但更改数据的操作只有一个事务可以执行,其他事务必须等待直至可更改事务commit

无锁实现前三种隔离级别的机制------------MVCC(多版本并发控制)

  • MVCC实际是为无锁解决并发读写冲突而诞生的,其原理为:启动一个事务后,若尝试对某数据进行修改,则保存一份元数据快照,存储至MySQL内部一个内存块(undo log)中,然后创建一份更改后的新数据,并将其链接到元数据快照,若同一事务内多次更改同一条数据,则会形成链表结构......,而所谓的无锁解决读写冲突,其实就是对不同快照进行操作,这点会在下面详细讲解

  • 实现MVCC的前置条件其一:其实,每次插入数据,MySQL都会在该条数据中添加一些隐藏字段

    • DB_TRX_ID:表示该条数据最新被插入/修改的事务ID
    • DB_ROW_ID:隐式主键,在数据中不具备主键时自动添加,用于数据库快速索引
    • DB_ROLL_PTR:回滚指针,也就是上述MVCC原理中提到的"链接"用指针
  • 实现MVCC的前置条件其二:Read View

    cpp 复制代码
    class ReadView {
    // 省略...
    private:
    /** 高水位,大于等于这个ID的事务均不可见*/
    trx_id_t m_low_limit_id
    比特就业课
    /** 低水位:小于这个ID的事务均可见 */
    trx_id_t m_up_limit_id;
    /** 创建该 Read View 的事务ID*/
    trx_id_t m_creator_trx_id;
    /** 创建视图时的活跃事务id列表*/
    ids_t m_ids;
    /** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,
    * 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/
    trx_id_t m_low_limit_no;
    /** 标记视图是否被关闭*/
    bool m_closed;
    // 省略...
    };
    // 重点字段:
    // m_ids;  //一张列表,用来维护Read View生成时刻,系统正活跃的事务ID
    // up_limit_id;  //记录m_ids列表中事务ID最小的ID(没有写错) 
    // low_limit_id; //ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1(也没有写错)
    // creator_trx_id   //创建该ReadView的事务ID,事务ID一般递增分配,也就是事务越新越值越大

    解释:在单个事务中,首次进行快照读的时候,会创建一个ReadView对象,其中会记录当前事务ID,当前并发的事务ID列表,以及其中最小的事务ID和下一个将被分配的事务IO(首次快照读创建,也可以说,事务的创建与Read View对象创建之间存在时间窗口,ReadView对象创建后,当前事务中,其记录的这四个值不会被变更

    注:快照读,就是读快照(上述提到的链表中的非头节点)时,可以理解为在RC/RR隔离级别下的正常select语句都是快照读(SELECT * FROM t WHERE id = 1 FOR UPDATE;/SELECT * FROM user WHERE id = 1 FOR SHARE;这种是指定读取最新的,这算是非正常的)

  • 无锁并发的具体实现规则:

    每次读数据时,都会尝试从头遍历链表,针对链表节点中DB_TRX_ID分为下列情况:

    • 小于up_limit_id:说明该事务在Read View创建前已经结束,则该节点的数据可见
    • 大于low_limit_id:说明该事务在Read View创建后开始,则该节点的数据不可见
    • 等于creator_trx_id:说明该操作就是本事务进行的,自然当前事务可见(即,在可重复读下,观测事务修改数据,会造成select结果不同的情况,这是符合预期的,因为可重复读指的是不同事务间)
    • 当DB_TRX_ID位于up_limit_id和low_limit_id之间时,则需要判断该ID是否存在于m_ids列表中,若存在,则表明该事务在Read View前创建且创建后仍在执行,则该节点数据不可见;若不存在,则表明该事务在Read View创建前结束,则该节点数据可见(注:m_ids列表中的ID可能不连续,因为会存在先开始的事务后结束以及后开始的事务先结束的情况

MVCC实现RC/RR隔离级别

简而言之,上面描述的Read View对象仅在第一次快照读时创建之后不做修改就是RR(可重复读)隔离级别的实现原理,而RC的实现原理则为,每次快照读都创建新Read View对象,以此做到可以看到提交后的数据

补充知识点------幻读

在可重复读(RR)情况下,事务 A 新增的数据,在事务 B 的快照读 中不会被看到,因此 B 多次执行相同的 SELECT,不会因为事务 A 的 INSERT 而突然多出新的记录。

但在一般数据库的 RR 实现中,如果仅依靠普通行锁,很难解决其他事务 INSERT 导致的幻读问题。因为待插入的记录原本并不存在,对一条不存在的记录本身无法直接加行锁。

例如:

sql 复制代码
-- 事务 B
BEGIN;
SELECT * FROM user WHERE age = 20;
-- 第一次查询得到 3 条记录

-- 事务 A
BEGIN;
INSERT INTO user VALUES (..., 20);
COMMIT;

-- 事务 B
SELECT * FROM user WHERE age = 20;
-- 如果能够看到事务 A 新插入的记录,则变成 4 条

事务 B 两次查询的结果中,出现了之前不存在的新记录,就好像产生了"幻觉",因此这种现象称为幻读(Phantom Read)

不过,MySQL InnoDB 在 RR 隔离级别下,通过 MVCC + 锁机制 可以解决典型的幻读问题:

  • 对于普通 SELECT 快照读 :通过 ReadView 保证同一事务中的一致性视图,新提交的 INSERT 不会出现在当前事务的快照中。
  • 对于 SELECT ... FOR UPDATESELECT ... FOR SHARE当前读 :InnoDB 会通过 Next-Key Lock(记录锁 + 间隙锁) 锁住记录以及相应的间隙,阻止其他事务在范围内插入新的记录。

因此,可以简单理解为:

MySQL InnoDB 的 RR 级别能够解决典型的幻读问题。

oDB 会通过 Next-Key Lock(记录锁 + 间隙锁) 锁住记录以及相应的间隙,阻止其他事务在范围内插入新的记录。

因此,可以简单理解为:

MySQL InnoDB 的 RR 级别能够解决典型的幻读问题。

相关推荐
geovindu1 小时前
sql: Data Modeling Patterns using mysql
sql·mysql·设计模式·数据库开发
cspttty1 小时前
银行数据分析校招能力模型:SQL、指标体系、可视化与AI辅助分析
数据库
今年下半年2 小时前
mysql数据库备份详细操作流程
学习·mysql
Kamille Bidan2 小时前
平台核心业务表结构设计
mysql·heidisql
这个DBA有点耶2 小时前
AI Agent操作数据库的安全边界:只读沙箱、操作预演与自动回滚如何落地?
数据库·sql·程序人生·aigc·数据库架构·dba
梦帮科技3 小时前
从提示词到可复现音乐工作流:RNOISE 2.0 的 Prompt Engineering 架构
人工智能·python·mysql·ci/cd·架构·node.js·numpy
ss2733 小时前
Java全栈实战 | 1.3-03 索引原理:联合索引 (a,b,c) 只查 b 走不了索引?B+ 树一图讲透所有索引玄学
java·数据库
聚美智数3 小时前
车辆合格证OCR识别-机动车合格证识别-车辆合格证OCR‑机动车出厂证解析‑整车参数提取 API 接口介绍
数据库·经验分享
DBA_G3 小时前
南大通用技术分享:GBase 8a数据库执行计划架构原理解析
数据库