【MySQL】深入理解隔离性

news2025/1/11 11:17:31

文章目录

  • 多版本并发控制(MVCC)
  • 如何解决读-写并发
  • undo 日志
  • 模拟MVCC过程
  • select读取版本
  • 隔离性的实现
  • 为什么要有隔离级别
  • 快照(read view)
  • 可重复读(RR)与读提交(RC)的本质区别

多版本并发控制(MVCC)

多版本并发控制(MVCC)是一种用来解决读写冲突的无锁并发控制,

数据库并发的场景有三种:

  • 读-读:一方在读,另一方也在读,因为不对数据作修改,就不存在任何问题,也就不需要并发控制,
  • 读-写:一方在读,另一方在写,有线程安全问题,可能会造成事务隔离问题,可能遇到脏读,不可重复度,幻读问题,
  • 写-写:一方在写,另一方在写,有线程安全问题,可能会存在数据更新丢失问题,

如何解决读-写并发

1)结论:数据库在启动一个事务的时候,每一个事务都有一个id, 每一个sql都封装成事务,每一条sql都有id

Mysql会为事务分配单向增长的事务ID,为每个修改保存一个版本,版本与事务ID关联,读操作只读该事务开始前的数据库的快照, (类似于写时拷贝,读原来的表,写入到新表里面去): 所以 MVCC 可以为数据库解决以下问题 :

1)在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高数据库并发读写的性能

2)可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题


MySQL每个表中与MVCC有关的三个隐藏字段:

DB_TRX_ID : 6 字节,表示最近修改( 修改/插入 )事务ID,记录创建这条记录/最后一次修改该记录的事务ID,

DB_ROLL_PTR : 7字节,表示回滚指针,指向这条记录的上一个版本(简单理解成指向历史版本,这些历史版本数据一般在 undo log 中)

DB_ROW_ID : 6 字节,表示隐含的自增ID(隐藏主键),如果数据表没有主键, InnoDB 会自动以 DB_ROW_ID 产生一个聚簇索引,

补充:实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了(比如对应的比特位1->0表示删除了) 为1表示记录有效为0无效


例子:比如下面的表

mysql> create table student( name varchar(11) not null, age int not null ); 
mysql> insert into student (name, age) values ('张三', 28); 
mysql> select * from student; 
+--------+-----+ 
| name | age | 
+--------+-----+ 
| 张三 | 28 | 
+--------+-----+ 

我们目前并不知道创建该记录的事务ID,隐式主键,我们就默认设置成null,1,第一条记录也没有其他版本,我们设置回滚指针为null

nameageDB_TRX_ID(创建该记录的事务 ID)DB_ROW_ID(隐藏主键)DB_ROLL_PTR(回滚指 针)
张三28null1null

undo 日志

MySQL以服务进程的方式,在内存之中运行,MySQL的各种机制(索引、事务、隔离、日志等等)都在内存之中完成

  • 在MySQL内部有相对应的缓冲区保存相关的数据,完成各种操作,然后在合适的时候将数据刷新至磁盘之中

简单理解就是,undo log就是MySQL中的一段缓冲区,用来保存日志的数据, 针对每一条被修改的记录,都会改前先备份一遍放在 undo log 中,依靠字段回滚指针一条一条链接起来


日志中保存的内容通常有两种:

  • 历史数据: 回滚则重新指向,
  • 命令的反向的操作:比如insert操作会记录delete,update会记录update之前的值,

模拟MVCC过程

1)当前事务想要修改记录时,会先给该记录加锁;

2)并将改前记录拷贝一份放到 undo log 中保存起来;

3)改完后解锁,并将记录中的回滚指针字段指向 undo log 中的上一条记录,改完之后的记录就成了最新纪录,而 undo log 中的就是历史记录, 同时事务ID是递增的

4)提交事务,释放锁

image-20221028164128692
image-20221028164137277
image-20221028164302370

这样,我们就有了一个基于链表记录的历史版本链,所谓的回滚,无非就是用历史数据,覆盖当前数据, 上面的一个一个版本,我们可以称之为一个一个的快照

上面是以更新(upadte)为主的版本控制,如果是其他操作:

  • delete并不是直接删除数据,而是更改flag,(由1变0)因此是可以形成历史版本的,有版本链,update也有版本链
  • insert是新增数据,所以是没有历史版本的,但是为了回滚,一般也会将insert的数据放入undo log之中,如果当前事务commit,那么undo log之中的insert数据就会被清空
  • select不会对数据进行修改,因此没有维护多版本的意义

删改的永远都是最新纪录,而查询的可能是历史记录,对于插入操作,自身提交后,就可以清空undo log中的插入历史纪录了,因为新增数据其他并发事务是看不见的,不需要维护隔离性,但对于更新和删除操作,如果提交后清空undo log,其他并发事务还可能正在读取历史,只能等并发事务全部提交后才能清空

undo log 里面的内容什么时候会被清除

当前最新的记录没有人修改而且被提交了,且 mysql所有事物当前没有人再和undo log里面的数据有关了


select读取版本

问:select读取的数据是最新版本的还是历史版本

首先提出一个概念: 当前读:表示读取最新的记录 快照读:表示读取历史版本

  • 所有事物删改查的时候都叫当前读,因为访问的是最新的数据,需要加锁 读取最新记录都可以称为当前读
  • 如果要进行select也要读取最新版本一定是需要加锁的
  • 读取undo log中的历史版本,就称为快照读

如果是快照读,读取的是历史版本,是不受锁的限制的,因为是历史数据,历史版本不会被修改,被修改的永远是最新版本,只要读取历史版本就不需要加锁, 也就是可以并发执行,提高程序的效率,这就是MVCC的意义所在

回答: select有可能是当前读,也有可能是快照读,select读取历史的哪一个版本,由隔离级别决定


隔离性的实现

  • 隔离性的实现: 有些事务读取的是旧版本.有些读取是新版本,所以不冲突

  • 隔离级别本质:根据级别不同,让你看到历史的哪一个版本 隔离性是通过历史版本实现的

在可重复读隔离级别下,一个事务看不到其他并发事务已经提交的更改,是因为并发事务删改的是最新纪录,而当前事务只能查询到历史版本,这就是隔离性的实现,那具体看到是哪一个历史版本,就是隔离级别不同的原因

注意:隔离级别中的串行化,也就是并发事务增删改查操作都要加锁,都是当前读,不需要历史版本,而对于隔离级别中的可重复读,并发事务进行查询操作时都是读取历史版本的,就不需要加锁限制了,可以并行执行,提高了效率这就是MVCC的意义所在

事务是具有原子性的,事务能够看到的内容、哪些事务所做的修改、整个数据库数据的情况,可以认为在事务分配事务ID时就确定了,在此之后数据库发生的变化,该事务是看不见的,这就是隔离性的实现目标

为什么要有隔离级别

因为事务都是原子的,当它启动的瞬间,它的事务id就被确定好了,也就意味着它所认为的内部的mysql的数据状态也要确定下来, 它所看到的数据也必须是明确的

事务从begin开启事务->CURD->commit提交事务,是有一个阶段的,也就是事务有执行前,执行中,执行后的阶段,但是不管怎么启动多个事务,总是有先有后的, 那么多个事务在执行中,CURD操作是会交织在一起的

为了保证事务的“有先有后”,应该让不同的事务看到它该看到的内容,这就是所谓的隔离性与隔离级别要解决的问题


快照(read view)

Read View就是事务进行快照读操作的时候生产的读视图 ,在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID

  • 当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大

不同的事物读取不同的版本,这是需要判断的,判断条件就是根据事物ID,由于启动的时候会分配事务ID,ID是递增的,所以可以根据事务ID来判断,应该读取哪个历史版本

白话:当你的事务到来的时候,形成事务ID,你所能看到mysql内部的各种数据,就要被确定下来,当我们的事务到来的时候,我们能看到mysql的各种数据,类比成一张照片

总结:Read View是决定一个事务可见性的数据结构, 记录一个事务可以看到哪些历史版本的数据


Read View 在 MySQL 源码中,就是一个类,本质是用来进行可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个 Read View 读视图,把它比作条件,用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的 undo log 里面的某个版本的数据

class ReadView 
{
        //....
private:   
    m_ids; //一张列表,用来维护Read View生成时刻,系统正活跃的事务ID
    up_limit_id; //记录m_ids列表中事务最小的ID
    low_limit_id;//ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前m_ids列表中事务ID的最大值+1
    creator_trx_id //创建该ReadView的事务ID,也就是本事务的事务ID,
    //....
};

如何判断undo log中的历史版本记录是否能够被本事务读取看到呢

1)如果历史版本的记录中的隐藏字段DB_TRX_ID大于等于高水位m_low_limmit_id,说明该记录对本事务不可见

2)如果DB_TRX_ID小于低水位m_up_limmit_id或等于当前事务IDm_create_trx_id,说明该记录对本事务可见

3)如果DB_TRX_ID在高低水位之间且在m_ids中,说明该事务在创建快照时未提交,如果不在m_ids中,说明该事务在创建快照时已经提交,如果已提交当前事务就能看到其所做的修改,反之则不能

可见性判断法则:只要事务早就被提交,或者事务不在快照列表里面,就应该看到 否则不应该看到!

image-20221029102259527

准确的来说,事务能够看到的历史记录,在快照创建的一瞬间就确定好了,快照创建的时间就是事务首次进行select查询操作的时间,快照一旦建立好就不在变化,此时该事务所能看到的数据也就不会发生变化了

bool changes_visible(trx_id_t id, const table_name_t& name) const  //能否看到
{
    if (id < m_up_limmit_id || id == m_creator_trx_id) {
        return true;
    }
    
    if (id >= m_low_limmit_id) {
        return false;
    } else if (m_ids.empty()) {
        return true;
    }
    
    return !std::binary_search(m_ids.data(), m_ids.data() + m_ids.size(), id);
}
  • (creator == DB_TRX_ID || DB_TRX_ID < up_limit_id):这个版本比up_limit_id 还小(事务ID是从小往大顺序生成的),说明这个版本在SELECT之前就已经提交了,所以这个数据是可见的 ; 或者这个版本的事务本身就是当前SELECT语句所在事务的话,也是一样可见的;
  • (DB_TRX_ID >= low_limit_id):表示这个版本是由将来启动的事务来生成的,当前还未开始,那么是不可见的;
  • (up_limit_id <= DB_TRX_ID < low_limit_id):这个时候就需要再判断两种情况:
  • 如果这个版本的事务ID在ReadView的未提交事务数组中,表示这个版本是由还未提交的事务生成的,那么就是不可见的;
  • 如果这个版本的事务ID不在ReadView的未提交事务数组中,表示这个版本是已经提交了的事务生成的,那么是可见的。

假设当前有条记录:

image-20221029102951878

同时启动了4个事务,这四个事务是有先后关系的:

image-20221029103015404

事务4对数据进行了修改:修改name(张三) 变成name(李四),此时版本链是:

image-20221029103056357


当 事务2 对某行数据执行了 快照读 ,数据库为该行数据生成一个 Read View 读视图

#事务2的 Read View 
m_ids; # 1,3 
up_limit_id; # 1 
low_limit_id; # 4 + 1 = 5,原因:ReadView生成时刻,系统尚未分配的下一个事务ID 
creator_trx_id # 2

事务2在快照读该行记录的时候,就会拿版本链中的的DB_TRX_ID 去跟up_limit_id,low_limit_id和活跃事务ID列表(trx_list) 进行比较,判断当前事务2能看到该记录的版本,事务4提交的记录对应的事务ID DB_TRX_ID=4,

比较步骤:

  • DB_TRX_ID(4)< up_limit_id(1)不小于,说明事务4并不是事务2之前已经提交的事务,不能看到,
  • DB_TRX_ID(4)>= low_limit_id(5) 不大于等于,说明事务4并不是事务2创建视图之后的事务,
  • m_ids.contains(DB_TRX_ID) 事务2的Read View不包含当前DB_TRX_ID,说明事务4不在当前的活跃事务中,

所以得出结论:事务4是和事务2是同时运行的事务,如果在读提交的隔离级别下,事务2能够看到事务4修改的数据;如果是在可重复读的隔离级别下,则看不到,只能够回滚看到“张三”那条数据,而事务4提交的版本也是全局角度上最新的版本


如果事务2在快照读之后,又来了一个事务5,将“李四”改成了“王五”并提交,那版本链就如下所示:

image-20221029103426196

事务2在比较的时候DB_TRX_ID(5)>= low_limit_id(5),等式成立,说明事务5是事务2在形成快照之后来的事务,事务2无法看到这条数据,所以回滚,找“李四”那条数据


可重复读(RR)与读提交(RC)的本质区别

如果我们想要当前读怎么操作:

select * from user lock in share mode;#加共享锁方式进行读取,对应的就是当前读

可重复读隔离级别下进行测试:

以下面的表为例

image-20221029112929684

案例1:

事务A操作事务A描述事务B描述事务B操作
begin开启事务开启事务begin
select * from account快照读(无影响)查询快照读查询select * from account
update account set name=‘王五’ where id=1更新--
commit提交事务--
--快照读 ,没有读到 name=‘王五’select * from account
--当前读 , 读到 name=‘王五’select * from account lock in share mode

image-20221029113326720


案例2:

事务A操作事务A描述事务B操作事务B描述
begin开启事务begin开启事务
select * from account;快照读(无影响)查询--
update account set name=‘王五’ where id=1;更新--
commit提交事务--
--select * from account;快照读 ,读到 name=‘王五’
--select * from account lock in share mode;当前读 , 读到 name=‘王五’

image-20221029113644119


一个事务启动默认没有Read View, 当第一次select的时候,才会形成Read View,select的时候底层就是执行一次快照读,mysql要给事务形成Read View

用例1与用例2:唯一区别仅仅是 表1 的事务B在事务A修改name前 快照读 过一次age数据,而 表2 的事务B在事务A修改name前没有进行过快照读

结论: 事务中快照读的结果是非常依赖该事务首次出现快照读的地方,即某个事务中首次出现快照读,决定该事务后续快照读结果的能力, delete同样如此,


RC(读提交)与RR(可重复读)的本质区别是:二者Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同 (如果Read View 不变,快照读所能读到的东西也就不变,)

  • 在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View, 将当前系统活跃的其他事务记录起来, 此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见;
    • 即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的,而早于Read View创建的事务所做的修改均是可见,
  • 而在RC级别下的,事务中,每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因, 总之在RC隔离级别下,是每个快照读都会生成并获取最新的Read View
  • 在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View,正是RC每次快照读,都会形成Read View,所以,RC才会有不可重复读问题,

总结起来就是:

  • RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,此后Read view不在发生变化,故以后再进行快照读都读的内容是固定的,
  • RC级别下,事务每次快照读都会新生成一个快照和Read View,这就是在RC级别下的事务可以看到别的事务提交的更新的原因,

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/76546.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[附源码]计算机毕业设计教务管理系统Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

2023最新SSM计算机毕业设计选题大全(附源码+LW)之java银行管理系统275d1

要开始我们毕业设计的第一步的关键就是选好我们的课题&#xff0c;有的同学开始选题的时候想着按照传统的课题延续下去&#xff0c;在设计题目时&#xff0c;不要过于笼统广泛&#xff0c;选择题目其实并不难&#xff0c;要多从自身的角度出发&#xff0c;要结合你们当前所处的…

Briefings in Bioinformatics2021 | DLGN+:基于GAN和强化学习的分子从头双目标性质生成

论文标题&#xff1a;De novo generation of dual-target ligands using adversarial training and reinforcement learning 论文地址&#xff1a;https://academic.oup.com/bib/article/22/6/bbab333/6354720 代码&#xff1a;https://github.com/lllfq/DLGN 一、模型结构 …

数学基础从高一开始3、集合的基本运算

目录 复习内容&#xff1a; 并集的概念 你能用符号语言和图形语言表示并集这个集合吗? 例1:求并集 例2&#xff1a;求并集 符号解析&#xff1a; 例3&#xff1a; 交集的概念 例4&#xff1a; 例5&#xff1a; 例6&#xff1a; 思考题&#xff1a; 作业&#xff…

20221209在Ubuntu22.04下读取苹果分区APFS的步骤

20221209在Ubuntu22.04下读取苹果分区APFS的步骤 缘起&#xff1a;公司的新来的美工要清理MAC电脑。 由于忘记管理员密码&#xff1f;于是备份文件&#xff0c;重装系统&#xff01; 于是通过固态硬盘盒子将2TB的M2接口的固态硬盘SSD格式化为APFS&#xff0c;这样MAC电脑就可…

2022下半年软考成绩即将公布,预约查分提醒,查分快人一步

距离2022下半年考试已经过去一个多月了&#xff0c;大家都在焦急的等待软考成绩查询。根据往年情况来看&#xff0c;软考成绩查询时间并不是固定的一个时间点&#xff0c;不过可以大致预测下应该是在12月中/下旬左右开放成绩查询&#xff0c;具体情况以官方公告为准。 历年软考…

玩以太坊链上项目的必备技能(类型-引用类型-Solidity之旅三)

在前文我们讲述了值类型&#xff0c;也就说再修改值类型的时候&#xff0c;每次都有一个独立的副本&#xff0c;如&#xff1a;string 类型的状态变量&#xff0c;其值是无法修改&#xff0c;而是拷贝出一份该状态的变量&#xff0c;将新值存起来。对于处理稍微复杂地值类型时&…

2022最新性能测试面试题(带答案)

一、性能测试开展过程&#xff1a; 答&#xff1a;第一步&#xff1a;找产品沟通哪些接口需要压测&#xff0c;需要达到什么样的预期值(TPS和响应时间) 第二步&#xff1a;编写测试计划&#xff0c;人员、时间周期、工具 第三步&#xff1a;环境搭建 第四步&#xff1a;造数…

计算机操作系统

并行和并发的区别与联系&#xff1f; 【并发】 多个任务交替执行 计算机在运行过程中&#xff0c;有很多指令会涉及 I/O 操作&#xff0c;而 I/O 操作又是相当耗时的&#xff0c;速度远远低于 CPU&#xff0c;这导致 CPU 经常处于空闲状态&#xff0c;只能等待 I/O 操作完成后…

springboot项目如何启用arthas

Arthas 是Alibaba开源的Java诊断工具&#xff0c;深受开发者喜爱。当你遇到以下类似问题而束手无策时&#xff0c;Arthas可以帮助你解决&#xff1a; 这个类从哪个 jar 包加载的&#xff1f;为什么会报各种类相关的 Exception&#xff1f;我改的代码为什么没有执行到&#xff…

HTML网页设计:爱护动物题材——保护动物大象(6页) HTML网页设计结课作业 web课程设计网页规划与设计 网页设计成品DW静态网页

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

scConverter 文档转换 DLL / SDK

scConverter 转换 DLL / SDK scConverter 是一个DLL&#xff0c;可以将PDF、DWF、Gerber、CGM、TIFF、CALS、PLT、PNG和JPEG文件转换为大量输出格式。可用的输出格式列表包括Adob​​e PDF、PDF/A、DXF、DWF、CALS、TIFF、PLT和PNG。您将在下面找到所有可用输入和输出格式的完整…

R语言用向量自回归(VAR)进行经济数据脉冲响应研究分析

自从Sims&#xff08;1980&#xff09;发表开创性的论文以来&#xff0c;向量自回归模型已经成为宏观经济研究中的关键工具。最近我们被客户要求撰写关于向量自回归&#xff08;VAR&#xff09;的研究报告&#xff0c;包括一些图形和统计输出。这篇文章介绍了VAR分析的基本概念…

Java高级——后端编译与优化

后端编译与优化解释器和编译器编译器即时编译器分层编译热点代码热点探测计数器编译过程查看及分析即时编译结果提前编译器jaotc的提前编译后端编译优化总览优化演示方法内联&#xff08;最重要的优化技术之一&#xff09;逃逸分析&#xff08;最前沿的优化技术之一&#xff09…

15. 过拟合和欠拟合

1. 过拟合和欠拟合 当数据比较简单时&#xff0c;使用模型容量低的模型更好&#xff0c;否则使用高的会出现过拟合。如果是复杂的数据用到简单模型上会出现欠拟合&#xff0c;用到复杂模型上是正常的。 2. 模型容量 模型容量&#xff1a;拟合各种函数的能力 低容量的模型难以…

Springboot+Easyexcel:导出excel表格

常规导出 常规导出excel有两种&#xff0c;个人比较推荐第一种&#xff1a; 1、新建一个导出数据的实体类&#xff0c;用ExcelProperty()注解标明excel中列的中文名称&#xff1b;如果实体的类某些列不想导出&#xff0c;可以使用ExcelIgnore进行忽略就可以了。 2、使用easyexc…

彻底理解Python中浅拷贝和深拷贝的区别

目录 前言 1. 浅拷贝和深拷贝的概念 2. is和的区别 3. 赋值操作 4. copy模块里面的copy()方法 5. copy模块里面的deepcopy()方法 6.字典自带的copy方法 7.切片表达式拷贝 前言 Python 的所有变量其实都是指向内存中的对象的一个指针&#xff0c;这确实和之前学过的强类…

JDBC基本使用(第一个jdbc程序)

在web开发中&#xff0c;不可避免的地要使用数据库来存储和管理数据。为了在java语言中提供数据库访问的支持&#xff0c;Sun公司于1996年提供了一套访问数据的标准Java类库&#xff0c;即JDBC。 JDBC的全称是Java数据库连接(Java Database connect)&#xff0c;它是一套用于执…

Web3中文|AI机器人ChatGPT如何看待DeFi?

如果还没有玩过OpenAI最新的聊天机器人ChatGPT&#xff0c;那您真的应该体验一下。 从电影推介到编程查询&#xff0c;ChatGPT几乎可以对您向它提出的任何提示做出类似人类的逻辑响应。这种新奇的感觉就像乔布斯第一次滑动解锁iPhone屏幕时那样。 与加密货币一样&#xff0c;…

nacos配置在代码中如何引用

1、在代码的模块服务中安装nacos 配置依赖 <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency>2、在nacos配置中心中进行服务配置 注意不是模块名&#…