首页 > 数据库 >可重复读隔离级别真的完全解决不可重复读问题了吗?读已提交隔离级别能避免不可重复读问题吗?超超详细MySQL事务,隔离级别,并发问题讲解整理,一文带你彻底搞懂所有隔离级别和并发问题

可重复读隔离级别真的完全解决不可重复读问题了吗?读已提交隔离级别能避免不可重复读问题吗?超超详细MySQL事务,隔离级别,并发问题讲解整理,一文带你彻底搞懂所有隔离级别和并发问题

时间:2024-08-31 23:51:22浏览次数:13  
标签:事务 隔离 记录 重复 提交 级别

一文带你搞懂MySQL事务的各个疑惑,不要再在脑子里一团浆糊啦!!

事务的四大特性

在这里插入图片描述

事务的四大特性分别为 ACID,即原子性、一致性、隔离性、持久性

  • ​原子性( Atomicity ) 是指事务中的所有操作要么全部完成,要么全部不完成,不会结束在某个中间环节,原子性是由 undo log 保证的

  • 一致性( Consistensy ) 是指事务执行前后,数据库的状态必须保持一致性,一致性是通过持久性 + 原子性 + 隔离性这三个共同保证的

  • ​隔离性( Isolation ) 是指多个事务并发读写数据库,可以防止多个事务并发读写同一份数据时,导致的数据不一致问题,隔离性是由 MVCC 和 锁保证的

  • 持久性( Durability ) 是指保证事务完成后的对数据的修改就是永久的,不会因为系统故障而丢失,持久性是由 redo log 日志保证的

MySQL的三种日志

在这里插入图片描述

  • undo log(回滚日志)是 Innodb 存储引擎层生成的日志,实现了事务中的原子性,主要用于事务回滚和 MVCC。 在事务没提交之前,Innodb 会先记录更新前的数据在 undo log 中,回滚时就利用 undo log 进行回滚。

  • redo log(重做日志)是 Innodb 存储引擎层生成的日志,实现了事务中的持久性,主要用于掉电等故障恢复。比如某个事务提交了,脏页数据还没有刷盘,如果 MySQL 机器断电了,脏页的数据就丢失了,MySQL 重启后就可以通过 redo log 日志,将已提交的事务的数据恢复过来。

  • binlog (归档日志)是 Server 层生成的日志,主要用于数据备份和主从复制。在完成一条更新操作后,Server 层会生成一条 binlog,放入内存的缓冲区,等之后事务提交的时候,会将该事务执行过程中产生的所有的 binlog 统一写入 binlog 文件中。binlog 文件是记录了所有数据库表结构和表数据修改的日志,不会记录查询类的操作

详细文章讲解:MySQL 日志:undo log、redo log、binlog 有什么用?

事务的原子性是如何保证的?

事务的原子性是通过 undo log 实现的。

在事务还没有提交前,历史数据会记录在 undo log 中,如果事务执行过程中,出现了错误或用户执行了 rollback 语句,MySQL 可以利用 undo log 中的历史数据,做原来相反的操作,将数据恢复到事务开始之前的状态,从而保证事务的原子性

事务的隔离性是如何保证的?

事务的隔离性是通过 MVCC 和 锁 的机制保证的。(MVCC 在后面有讲解)

可重复读隔离级别下的快照读,也就是普通的 select,是通过 MVCC 来保证事务隔离性的,当前读,也就是 update,select … for update 等,是通过行级锁来保证事务的隔离性的

事务的持久性是如何保证的?

事务的持久性是由 redo log 保证的。

因为 MySQL 是通过 WAL(先写日志再写数据)的机制,在修改数据的时候,会将本次对数据页的修改以 redo log 的形式记录下来,这个时候更新就算完成了,Buffer Pool 的脏页会通过后台线程刷盘,即使在刷页还没刷盘的时候发生了数据库的重启,由于修改操作都记录到了 redo log 中,之前已提交的记录都不会丢失,重启后就通过 redo log,恢复脏页数据,从而保证了事务的持久性

数据库事务的隔离级别

MySQL 隔离级别有四种,分别是 读未提交、读已提交、可重复读、串行化

  • 读未提交 指的是读到其他事务没有提交的事务

  • 读已提交 指的是读到了其他事务更改的数据

  • 可重复读 指的是一个事务在执行过程中读到的数据,始终与他在开始时读到的一致

  • 串行化 指的是通过强制事务排序,使其不可能冲突

针对不同的隔离级别,并发事务时也可能出现并发问题:

可能会出现三种并发问题:脏读、不可重复读、幻读

  • 脏读 指的是一个事务读取了另一个事务还未提交的数据,如果另一个事务回滚,则读取的数据是无效的。脏读可能导致数据的不一致性

  • 不可重复读 指的是一个事务多次读取同一条记录时,在此期间另一个事务修改了该记录,导致前后读取的数据不一致。不可重复读可能导致数据的不一致性

  • 幻读 指的是一个事务多次执行同一条查询时,在此期间另一个事务插入了符合该查询条件的新数据,导致前后的结果不一致。幻读可能导致数据的不完整性

各隔离级别都各自解决了什么并发问题?

在这里插入图片描述

  • 读未提交一个问题也没有解决

  • 读已提交避免了脏读问题,但还是存在不可重复读和幻读的问题

  • 可重复读避免了脏读和不可重复读问题,并且对于幻读问题在很大程度上避免了,但还是没有完全避免

  • 串行化避免了所有的并发问题,但是事务的并发性能是最差的

什么是 MVCC?

MVCC 是多版本并发控制

通过记录历史版本数据,解决读写并发冲突问题,避免了读数据时加锁,提高了事务的并发性能

MySQL 将历史数据存储在 undo log 中,结构逻辑上类似于一个链表,MySQL 数据行上有两个隐藏列,一个是事务 ID ,一个是指向 undo log 的指针

在这里插入图片描述

事务开启后,执行第一条 select 语句时,会创建 ReadView,ReadView 会记录当前未提交的事务

在这里插入图片描述

通过与历史数据的事务 ID 进行比较,就可以根据可见性规则来进行判断,判断这条记录是否可见,如果课件就直接将这个数据返回给客户端,如果不可见就继续往 undo log 版本链中查找第一个可见的数据返回给客户端。

详情参考 小林coding:Read View 在 MVCC 里如何工作的?

读已提交和可重复读隔离级别实现 MVCC 的区别?

他们 都是由 MVCC 实现的,区别在于创建 Read View 的时机不同

  • 读已提交隔离级别 是在事务开启后,每次执行一条 select 语句时都会生成一个新的 Read View,所以每次 select 都能看到其他事务最近提交的数据

  • 可重复读隔离级别 是在事务开启后,执行第一条 select 语句时生成一个 Read View,然后整个事务期间都会复用这个 Read View,所以一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的。

可重复读隔离级别是如何解决不可重复读问题的?

快照读,也就是普通的 select,是通过 MVCC 解决不可重复读问题,在第一次查询的时候,会生成一个 Read View,之后的每次 select 查询都会复用这个 Read View不会读到其他事务的更新的操作,这样就不会发生不可重复读的问题。

当前读,也就是 select … for update 等,在第一次进行 select … for update 语句查询的时候,会对记录加上 next-key 锁,也就是记录锁 + 间隙锁

而这里的记录锁,当其他事务更新加了锁的记录时,会被阻塞住,这样就不会发生不可重复读的问题了。

可重复读隔离级别是如何很大程度上避免幻读问题的?

快照读,也就是普通的 select,是通过 MVCC 解决不可重复读问题,在第一次查询的时候,会生成一个 Read View,之后的每次 select 查询都会复用这个 Read View不会读到其他事务的插入的记录,这样就不会发生幻读的问题。

当前读,也就是 select … for update 等,在第一次进行 select … for update 语句查询的时候,会对记录加上 next-key 锁,也就是记录锁 + 间隙锁

这里的间隙锁,当其他事务往这个间隙插入新记录的时候,会被阻塞住,这样就不会发生幻读的问题了。

注意区分

在可重复读隔离级别下

  • 针对快照读,都是通过 MVCC 机制,保证了读取的数据都是与事务开启时的数据保持一致
    • 对于不可重复读问题,保证不会读取到其他事务的更新操作
    • 对于幻读问题,保证不会读取到其他事务的插入操作
  • 针对快照读,都是通过临键锁(记录锁+间隙锁)机制,保证其他事务的操作不会影响到本事务的数据
    • 对于不可重复读问题,通过记录锁保证不修改该条记录
    • 对于幻读问题,通过临键锁保证不修改符合条件查询的记录
可重复读隔离级别为什么没有完全解决幻读问题?

场景一:

数据库中不存在 id = 3 的记录

事务 A 此时执行第一次查询 id = 3 的记录,为空

事务 B 插入了一条 id = 3 的记录

事务 A 此时更新了 id = 3 的记录(没查到但更新,这确实很反直觉),因为更新语句时当前读,就会把 id = 3 这条记录的隐藏列的事务 id 变为 事务 A 的 id ,表示这条记录是事务 A 修改的

接着,事务 A 再次查询 id = 3 的记录,此时发现这条记录的事务 id 和事务 A 的是 id 是一致的,此时就能读取到这条记录,于是发生了幻读现象

场景二:

事务 A 先执行了快照读

事务 B 再插入了一条符合 A 条件的记录

事务 A 再执行了当前读

于是发生了幻读

如何避免?

在开启事务之后,马上执行 select … for update 语句,因为会对记录加临键锁,这样就可以避免其他事务插入一条新纪录,避免了幻读

(特殊)可重复读隔离级别完全解决不可重复读问题了吗?

注:这里的 “特殊” 表示一般教材上和广泛共识上默认是解决,但是我们这里死扣了细节

如果前后两次查询都是快照读,也就是普通 select 的话,就不会产生不可重复读问题。

但是如果第一次查询时快照读,第二次查询时当前读,就会产生不可重复读的问题。

举例:

表里现在有 id = 1,value = 1 的记录

  1. 事务 A ,先执行 select ,查询到 id = 1,value = 1。

  2. 事务 B ,更新该 id = 1 的记录的 value 为 2,然后提交事务。

  3. 事务 A ,执行 select for update,然后就读到了这条 id = 1,value = 2 的记录了,发生了不可重复读的问题。

眼花缭乱了吧?

俺也一样

再梳理一下

对于幻读问题:

场景一:

  1. A 查询(快照读)一条不存在的记录,结果为空
  2. B 插入了一条符合条件的记录并提交了事务
  3. A 突然更新(当前读)刚刚不存在的记录,此时 A 再去读就又能读到了,发生幻读问题

场景二:

  1. A 普通查询(快照读)出三条符合条件记录
  2. B 插入了一条符合条件的记录,并提交事务
  3. A 又select … for update(当前读)了符合条件的记录,查询出了四条,发生了幻读问题

对于不可重复读问题:

与上面场景二类似

  1. A 普通查询(快照读)出 1 条记录
  2. B 更新了该条记录,并提交事务
  3. A 又 select … for update(当前读)了记录,发现数据不一致

本质

其实本质上,就是因为 MVCC 机制的局限性,也就是快照读(普通 select )的问题,导致可重复读隔离级别会出现一些问题,

而如果你一上来就使用当前读(select … for update),直接给记录加临键锁(记录锁+间隙锁),

记录锁 锁该条记录,临键锁 锁符合条件的记录

那就不会发生不可重复读问题和幻读问题了

(特殊)读已提交隔离级别可以解决不可重复读问题吗?

读已提交跟不可重复读的区别在于

  • 快照读下生成的 readview 的时机不同

  • 当前读下读已提交只有记录锁,没有间隙锁

所以,如果使用当前读,可以通过记录锁的方式解决不可重复读的问题,当然,因为没有间隙锁,就没有办法解决幻读问题了


最后,是不是觉得可重复读隔离级别看起来是很棒的级别?能够解决很多问题,那么,他就是最广泛应用的隔离级别吗?

NONONO!

事实上,虽然可重复读隔离级别是 MySQL 的默认隔离级别,但是互联网公司大多使用的是读已提交隔离级别!

为什么互联网公司用读已提交隔离级别?

因为读已提交的并发性能更好

上一题提到,因为读已提交没有间隙锁,只有记录锁,所以发生死锁的概率比较低。

同时,在可重复读隔离级别下,条件列未命中索引会锁表!而在读已提交隔离级别下,只锁行。

然后互联网业务对于幻读和不可重复读的问题都是能接受的,所以为了降低死锁的概率,提高事务的并发性能,都会选择使用读已提交隔离级别。

那为什么 MySQL 的默认隔离级别是可重复读呢?

这是个历史遗留问题

在MySQL5.1 版本之前的 binlog 记录方式只有 statement 方式,也就是记录 sql 语句,而 binlog 是在事务提交之后才会写入的,在主从模式下,读已提交隔离级别会有 bug

举例:

在这里插入图片描述

注意区分这里 master 节点 和 slave 节点 都需要完成 事务 A 和 事务 B的内容,不要搞混淆了

  1. A 和 B 都开启事务,事务 A 先删除一条数据

  2. 此时 B 插入一条符合条件的数据 并 进行提交

  3. A 再提交

读已提交级别下:

在 master 节点,会查询到这条数据的,因为先删后插

而在 slave 节点,无法查询到这条数据,因为 B事务 先提交,所以会被先写入 binlog 中去,就会先完成 B 事务中的插入,再完成事务 A 中的删除,于是这条记录就被删除了,也就是先插后删,导致主从节点数据不一致性问题

如何解决?
  • 隔离级别设置为可重复读,在该隔离级别下引入间隙锁。当 session1 执行 delete 语句时,会锁住间隙,那么session2执行的插入语句就会堵塞住
  • 将 binlog 的格式修改为 row 格式,此时就会是基于行的复制,自然就不会出现sql不一致的情况,但是该格式是在mysql5.1版本后开始引入。

参考文章链接:

小林coding MySQL 篇

为什么Mysql 默认的隔离级别是可重复读?

Mysql默认隔离级别为什么是可重复读?

标签:事务,隔离,记录,重复,提交,级别
From: https://blog.csdn.net/m0_63653444/article/details/141611763

相关文章

  • leetcode 3 无重复字符最长串
    leetcode3无重复字符最长串思路使用滑动窗口,建两个整型变量lp和rp,分别代表左边界指针和右边界指针,整型temp储存当前字串长度,整形max储存当前最长长度,然后从左往右遍历字符串。解题过程先将字符串toCharArray转成字符数组m,建一个哈希集合,储存当前已经用过的字符,然后写一......
  • zdppy_cache缓存框架升级,支持用户级别的缓存隔离,支持超级管理员管理普通用户的缓存
    启动服务importzdppy_apiasapiimportzdppy_cachekey1="admin"key2="admin"app=api.Api(routes=[*zdppy_cache.zdppy_api.cache(key1,key2,api)])if__name__=='__main__':importzdppy_uvicornzdppy_uvico......
  • 访问者模式:如何实现对象级别的矩阵结构?
    今天我们先来看一个原理看似很简单,但是理解起来有一定难度,使用场景相对较少的行为型模式:访问者模式。一、模式原理分析访问者模式的原始定义是:允许在运行时将一个或多个操作应用于一组对象,将操作与对象结构分离。这个定义会比较抽象,但是我们依然能看出两个关键点:一个是运行时使......
  • 解决lazarus版本dbgrideh在linux粘贴重复的Bug
    dbgrideh在linux存在粘贴重复的Bug,不啰嗦,直接给解决方法:1.打开EhLib.VCL11.1xxx/Lazarus/Lib/DBAxisGridsEh.pas2.定位到functionTDBAxisGridInplaceEdit.DoPaste(varMessage:TMessage):Boolean; 添加红字部分代码,重新编译应用就可以。AAxisBar:=Grid.AxisBa......
  • 代码随想录算法训练营第四十三天 | 300.最长递增子序列 , 674. 最长连续递增序列 , 718.
    目录300.最长递增子序列 思路1.dp[i]的定义2.状态转移方程3.dp[i]的初始化4.确定遍历顺序 5.举例推导dp数组方法一:动态规划方法二:贪心心得收获 674.最长连续递增序列思路动态规划1.确定dp数组(dptable)以及下标的含义2.确定递推公式3.dp数组如何初始化4.......
  • 两种解决powerdesigner概念模型转物理模型报字段重复错误的方法
    问题使用powerdesigner概念模型转物理模型时会报一个不能重复的错误解决方法一、取消勾选Uniquecode取消勾选以后保存,再一次生成物理模型。二、取消勾选EntityAttribute,不对属性进行检查如果Uniquecode取消勾选后依旧不行,可以尝试第二种解决办法。取消勾选以后......
  • SQLserver中的事务以及数据并发的问题和事务的四种隔离级别
    SQLserver中的事务在SQLServer中,事务是一组原子性的SQL语句集合,要么全部成功执行,要么全部不执行。事务确保数据库的完整性和一致性,即使在发生错误或系统故障的情况下也是如此。SQLServer支持本地事务和分布式事务。事务的特性(ACID属性)原子性(Atomicity):事务中的所有......
  • php 生成卡密 不重复
    publicfunctionadd(){if(false===$this->request->isPost()){return$this->view->fetch();}$params=$this->request->post('row/a');if(empty($params)){$this-&......
  • 82. 删除排序链表中的重复元素 II
    传送锚点:力扣给你一个链表的头节点head和一个整数val,请你删除链表中所有满足Node.val==val的节点,并返回新的头节点。示例1:输入:head=[1,2,6,3,4,5,6],val=6输出:[1,2,3,4,5]示例2:输入:head=[],val=1输出:[]示例3:输入:head=[7,7,7,7],val=7输......
  • 代码随想录day43 || 300 最长递增子序列,674 最长连续递增子序列,718 最长重复子数组
    300最长递增子序列varpath[]intvarresintfunclengthOfLIS(nums[]int)int{ //尝试回溯思路 iflen(nums)==1{ return1 } path=[]int{} res=0 backtracking(nums) returnres}funcbacktracking(nums[]int){ iflen(nums)==0{ iflen(pat......