我悟了!Mysql事务隔离级别其实是这样!

news2024/11/26 22:27:37

问题描述

​ 最近几天在忙项目,有个项目是将业务收集到的数据变动,异步同步到一张数据表中。在测试的过程时,收到QA的反馈,说有订单的数据同步时好时坏。我怀着疑惑的表情打开了那段代码,它的逻辑大概是这样的:

在这里插入图片描述

如果用简单的代码实现的话,会是这样的:

    public void updateAndQuery(Example example, int diff){
        List<ProductPO> productPOS = productMapper.selectByExample(example);
        ProductPO productPO =  productPOS.get(0);
        System.out.println("一次查询内容" + JSONObject.toJSONString(productPO));

        //二次插入并查询
        Integer oldNumber = productPO.getNumber();
        productPO.setNumber(oldNumber + diff);

        //首先先更新,再进行查询
        System.out.println("更新内容:"+ JSONObject.toJSONString(productPO));
        productMapper.updateByPrimaryKey(productPO);

        //异步执行
        CompletableFuture.runAsync(() -> {
            Example example1 = new Example(ProductPO.class);
            example1.createCriteria().andEqualTo("skuId", productPO.getSkuId());
            List<ProductPO> select = productMapper.selectByExample(example1);
            System.out.println("二次查询结果:"+JSONObject.toJSONString(select));
            if (oldNumber != select.get(0).getNumber() - diff) {
                throw new NrsBusinessException("查询出错");
            }
        });
    }

起初我左看看,右看看,也没有想到这个是什么原因造成的。直到我看到了这个…

@Transactional(rollbackFor = Exception.class)

事务执行原理

​ 在了解的问题原因前,我们需要了解事务是如何实现的。首先假设现在我们要设计一个mysql事务,最简单的方案其实是这样:每执行一次SQL,写一次数据库。大致流程图如下所示:

在这里插入图片描述

​ 但是这个方案好么?显然不是。因为我们如果采用这样的方式,存在两个显著的问题:

  • 数据库写入为磁盘读写,速度很慢。
  • 数据库存在锁机制,难支持高并发。

对于问题一,了解到存储器读写速度如下所示(图源网络):

在这里插入图片描述

​ 可以看到内存的存储速度是纳秒级别(10-9次方),而硬盘的存储速度是毫秒级别(10-3次方)。由此,为加快读写速度,可以将修改的内容写入内存,而后再异步写入磁盘

​ 同时,由于内存本身并没对不同线程做锁控制机制,可以支持多个线程同时访问。对于高并发的问题也能更好支持。由此,上述的实现方案就改为了下面的流程:

在这里插入图片描述

事务隔离级别

​ 在修改为优先写内存后续再异步同步的情况后,又带来了新的问题:在一个事务尚未确认提交时,新事务从缓存中应该读取什么数据呢?

在这里插入图片描述

​ 对于这种不同事务间数据读取的策略就被称为事务隔离级别。根据读取策略的不同,事务隔离级别被划分为四种:读未提交、读已经提交、可重复读、序列化

读未提交(Read Uncommitted)

​ 读未提交的策略比较简单,即默认读取内存中的内容,而不必管这个数据是否已经写入到了数据。但是这个策略会带来一些问题:

在这里插入图片描述

存在问题:

​ 如图所示,若设置为读未提交,那么此时事务可能读到尚未提交的数据,即脏读。因此会造成数据A在前一时刻尚且可以读取到,但想二次更新的时候,mysql数据库却因为回滚导致数据A被回退了。这种错误会导致系统的无法正常运行,是不可容忍的。

读已提交(Read Committed)

​ 既然读未提交的事务带来的错误是不可容忍的,那么我只读已提交的数据就可以避免读到脏数据了呀!那么应如何实现只读已提交数据呢?对问题进行分析,要获取到最新已提交的数据,必然要将数据的版本关系体现出来。为此,InnoDB设计了一个版本链的概念。对每行记录会新增两个隐藏列:trx_id、roll_pointer

在这里插入图片描述

  1. trx_id:用于保存每次对该记录进行修改的事务id。
  2. roll_pointer:存储一个指针,指向这条数据记录上一个版本的地址,可以通过它获取到该记录上一个版本的数据信息。

由此一来,就可以通过最新记录(可能未提交)进行回溯,直到找到已提交的记录

​ 当然,仅有版本链的概念明显不够,我们还无法判断哪个数据是已提交的。为此InnoDB又新增了一个ReadView的解决方案,ReadView保存了一个写入了但未提交的事务ID列表。依据这个列表,我们就可以判断哪些事务还未写入。

在这里插入图片描述

​ 以上图为例,由于此时trx_id=20、trx_id=40的事务均未提交,InnoDB会生成一个ReadView:{20,40}。由此可能出现三种情况的事务访问:

  • 若预期访问事务ID=10的记录,由于其小于最小的事务Id20,证明事务已提交,允许访问。
  • 若预期访问事务ID=30的记录,由于其介于最大最小的事务ID之间,就需要逐一判断ReadView中是否包含事务ID=30的记录
  • 若预期访问事务ID=50的记录,由于其大于ReadView最大的事务Id,必然是在生成ReadView后生成的,也必然没有提交,不允许访问。

结合版本链和ReadView,基本就可以实现只读取已经提交的内容。

存在问题:

​ 由于ReadView是每次查询才新生成的,因此不免存在以下情况:

在这里插入图片描述

​ 在事务中首先读了一次数据A,期间事务发生了提交,导致二次查询出来的数据A同第一次出现了差异。由此难免让人发问:“两次相同的条件,查询到的结果却不一致,我是出现了幻觉了嘛?” 因此,这种情况也被形象称做:幻读

​ 幻读同脏读不同,幻读造成的问题是会破坏数据一致性。假设我们有一张表 user(id, name, age),已经有两条数据 (1, “Jack”, 20), (2, “Tom”, 18),同时我们执行以下流程:

在这里插入图片描述

​ 三个事务执行完成后,主库数据库内的数据应该是:(1, “Jack”, 10), (2, “Jack”, 18),(3, “Jack”, 18)。然而,此时binlog内的写入的SQL语句却是:

//事务二
update user set name = "Jack" where id = 2
update user set age = "40" where id = 2

//事务三
insert into user values(3, "Jack", 30) /*(3, Jack, 30)*/

//事务一
update user set name = "Tom" where name = "Jack"

​ 那么此时,从库收到了主库同步的binLog数据,并按照顺序执行。得到的结果却是:(1, “Jack”, 10), (2, “Jack”, 10),(3, “Jack”, 10)。不难发现,数据行2和3发生了主从不一致,这个是无法容忍的。

可重复读(Repeatable Read)

​ 要解决幻读,主要是解决两个问题:1、确保一次事务内看到的数据一致;2、确保生成的binLog数据顺序正确。

​ 对于问题1,其实相对比较简单。同一次事务内看到的数据不一致是由于每次ReadView都实时生成(也被称为实时读)。因此,只要确保同一次事务内只生成一次ReadView(也被称为快照读),就可以避免多次查询会出现不一致数据的情况。

​ 然而,仅保持自己看不到是不够的,如果无法解决binLog的SQL写入顺序问题,数据不一致的问题就无法得到解决。那其实对上述现象进行分析,导致SQL写入顺序混乱的原因,其实是因为违背了事务一对于"where name = “Jack” 的原子性。即事务操作期间还有别的符合条件数据能被修改

​ 那么,很朴素的一个思想就是,只要对这些都符合条件的数据都加锁不就可以了嘛?为此,mysql提出了间隙锁的概念。假设当前我们的数据对name字段配置了一个索引,那么此时事务一运行的时候,我们需要将其索引临近的一行及其间隙都锁上,不允许其余事务进行更新插入的操作。由此一来,索引被锁上,没法插入新的数据,也就不会出现SQL语句混乱的情况了。

在这里插入图片描述

​ 那么这个时候肯定有人会说:“你没索引的字段咋办啊?”,对于没有索引的字段,mysql会做全表的扫描。由此一来,相当于会把整张表的数据都给锁上。从而避免无索引的情况出现数据不一致的问题。

序列化(Serializable)

​ 对于可序列化来说,实现就相对粗暴些。本着“爷才不考虑那么多,直接将表锁了,肯定不会有问题”的思想出发进行设计:

1、首先针对每次事务读操作的时候加表级共享锁,确保多个事务可以读。

2、事务写操作的时候则加表级别的排它锁,只允许自己事务操作。

这些锁都维持到事务结束再释放,从而完美避免了上述问题的出现。然而,粗暴的方法一般性能都不太好,在高并发的情况下,常常只有一个线程可以操作数据,因此不建议使用。

总结

​ 介绍了这么多有关事务隔离的内容,我们终于可以回归到我们的问题上来了。那么其实对于开头提到的问题,原因就是在异步线程中,会新开一个事务,这两个事务是并行的。由于mysql默认的事务隔离级别是可重复读,会导致事务A异步的情况下,数据可能未提交,事务B执行较快而获取到了旧数据,造成了同步数据错误的问题。

在这里插入图片描述

​ 知道了问题,那么解决方案就比较简单了,可以不通过异步的方式发送,而是采用kafka消息的机制。这样就给事务A留足了事务提交的时间,从而确保数据的准确同步。

参考文献

深入浅出Mybatis系列(五)Mybatis事务篇

从因到果看懂事务隔离级别的实现原理

innodb存储引擎中一条sql写入的详细流程

MySQL的两阶段提交(数据一致性)

MySQL是如何实现读已提交和可重复读的——MVCC原理

幻读为什么会被 MySQL 单独拎出来解决?

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

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

相关文章

Zookeeper实现分布式锁的原理。

之前学习Redis时候&#xff0c;我们利用Redis实现了分布式锁。 黑马点评项目Redis实现分布式锁_兜兜转转m的博客-CSDN博客 为什么提出了分布式锁的概念呢&#xff1f; 因为在单体项目中&#xff0c;锁是基于JVM虚拟机实现的&#xff0c;在分布式情况下&#xff0c;JVM就不唯…

FullGC频繁,线程数持续增长排查

告警 线上应用fullgc频繁&#xff0c;收到告警 GC监控—堆内存不足 查看近12小时的监控&#xff0c;发现Survivor区一直处于 满状态、fullgc非常频繁、但没有内存溢出的现象&#xff0c;很明显是堆内存不足 GC日志分析—暂停时间并不长 因为fullgc相当频繁&#xff0c;抽…

项目管理(知识体系概述)

项目的定义:为创造独特的产品、服务或者成果进行的临时性工作。 项目的特性:1、独特的产品、服务、成果;2、临时性工作。 项目管理的目的(为了解决什么问题): 1、达成业务目标 2、满足相关方期望 3、提供项目的可预测性 4、提高项目的成果性。 5、在适当的时刻交付…

机器人运动学标定:基于考虑约束的指数积的运动学标定方法——只需要测量位置,避免冗余约束

文章目录写在前面为什么要消除归一化和正交化操作&#xff1f;只用位置而不是位姿去做标定的原因基于消除冗余约束步骤的参数辨识模型分析参考文献写在前面 基于指数积的运动学标定方法介绍&#xff1a; 机器人运动学标定&#xff1a;基于指数积的串联机构运动学标定 机器人运…

Vue表单修饰符:v-model.lazy、v-model.number、v-model.trim

表单修饰符有&#xff1a;lazy、number、trim&#xff1b;修饰符加在v-model后面&#xff1b; lazy修饰符&#xff1a; v-model的作用是双向绑定表单&#xff0c;能获取到input输入框的值&#xff0c;而且是实时获取的&#xff0c;就是当你输入框里的值发生改变就会获取到&…

【Shell 脚本速成】02、Shell 变量详解

目录 一、变量介绍 变量存取原理 二、变量定义 2.1 什么时候需要定义变量&#xff1f; 2.2 定义一个变量 定义变量举例&#xff1a; 定义变量演示&#xff1a; 2.3 取消变量 unset 2.4 有类型变量 declare declare 命令参数&#xff1a; 案例演示&#xff1a; 三…

向前迈进!走入GC世界:G1 GC原理深入解析

第零章&#xff1a;名词解释 mutator&#xff1a;应用线程 STW&#xff1a;Stop-The-World&#xff0c;指除了GC线程&#xff0c;其它所有线程全部暂停的一段时间 并发&#xff1a;指代GC线程与mutator在同一时刻执行任务 并行&#xff1a;指代多个GC线程在同一时刻执行任务…

一站式元数据治理平台——Datahub

一站式元数据治理平台——Datahub万字保姆级长文——Linkedin元数据管理平台Datahub离线安装指南 - 独孤风 - 博客园 (cnblogs.com)企业级数据治理工作怎么开展&#xff1f;Datahub这样做 - 独孤风 - 博客园 (cnblogs.com)【DataHub】 现代数据栈的元数据平台–如何与spark集成…

如何设计金融机构多场景关键应用下的存储架构

【摘要】银行、保险等金融机构存在多场景下的关键应用,如何选择适合各场景下的存储,如何设计适合业务的存储架构,显得尤为重要。本文从当前主流存储架构分析入手,提出金融机构业务场景分析与架构选型思路,以Glusterfs为例,分享如何根据业务场景的特点,有针对性的选取适合…

SQL优化

文章目录提升group by的效率分页查询优化覆盖索引子查询起始位置重定义检查 where,order by,group by后面的列尽量使用 varchar 代替 char。&#xff08;SQL 性能优化&#xff09;如果修改 / 更新数据过多&#xff0c;考虑批量进行提升group by的效率 select user_id,user_nam…

spring-security源码学习总结

由于SpringBoot 对 Security 的支持类均位于org.springframework.boot.autoconfigure.security包下&#xff0c;主要通过 SecurityAutoConfiguration 自动配置类和 SecurityProperties 属性配置来完成&#xff0c;所以需要下载springboot源码深入学习 SecurityAutoConfiguratio…

云原生边缘设备解决方案Akri on k3s初体验

作者&#xff1a; 涂家英&#xff0c;SUSE 资深架构师&#xff0c;专注 Cloud-Native 相关产品和解决方案设计&#xff0c;在企业级云原生平台建设领域拥有丰富的经验。 写在前面 k3s 是 SUSE 推出的为物联网和边缘计算构建的经过认证的 Kubernetes 发行版&#xff0c;它可以帮…

指数函数及其导函数

目录前言指数函数的导函数指数函数导函数动图绘制参考文献前言 前面我们介绍了指数函数及其基本性质以及如何在笛卡尔直角坐标系下绘制静态的指数函数图像&#xff0c;这一节&#xff0c;我们将重点讨论一下指数函数的导函数以及导函数的动态表示&#xff0c;为方便起见&#…

大数据(9f)Flink富函数RichFunction

文章目录1、概述2、示例2.1、普通函数2.2、富函数2.2.1、获取富函数的运行时上下文3、源码截取3.1、RichFunction3.2、RuntimeContext1、概述 Rich Function&#xff0c;译名富函数&#xff0c;和普通函数相比&#xff0c;多了&#xff1a;生命周期&#xff08;open和close方法…

DGL学习笔记——第二章 消息传递范式

提示&#xff1a;DGL用户指南学习中 文章目录一、内置函数和消息传递API二、编写高效的消息传递代码总结消息传递是实现GNN的一种通用框架和编程范式。它从聚合与更新的角度归纳总结了多种GNN模型的实现。 假设节点 &#x1d463; 上的的特征为 &#x1d465;&#x1d463;∈ℝ…

Java(八)----多线程

1. 线程的基本概念 1.1 进程 任何的软件存储在磁盘&#xff08;硬盘&#xff09;中,运行软件的时候,OS&#xff08;操作系统&#xff09;使用IO技术,将磁盘中的软件的文件加载到内存,程序才能运行。 &#xff08;进程是从硬盘到内存&#xff09; 进程的概念 &#xff1a; 应…

Marked.js让您的文档编辑更加轻松自如!

低代码应用平台——kintone既可以保留更改记录&#xff0c;也有流程管理的功能&#xff0c;在公司内部分享会议记录啊、wiki等文档或学习资料等时非常的便利。 kintone还有丰富的文本编辑框&#xff0c;可以对内容进行编辑提高易读性。但是还是有不少人觉得如果能够使用Markdo…

19.[Python GUI] PyQt5中的模型与视图框架-基本原理

PyQt中的模型与视图框架 一、Qt中模型与视图相关的类 二、模型与视图的基本原理 MVC把图形界面分为三个部分&#xff1a;模型&#xff08;Model&#xff09;&#xff0c;视图&#xff08;View&#xff09;和控制器&#xff08;Controller&#xff09;&#xff0c; 模型&#x…

Git大型文件存储

什么是 Git LFS&#xff1f; Git 是跟踪代码库演变和与同行高效协作的绝佳选择。但是&#xff0c;当您要跟踪的存储库非常大时会发生什么&#xff1f; 如果您考虑一下&#xff0c;存储库变得庞大的主要原因大致有两个&#xff1a; 他们积累了非常非常长的历史&#xff08;项目…

【C】文件操作fopen与fclose

目录 函数 1.fopen 2.fclose 3.freopen 函数 头文件 #include<stdio.h> 1.fopen FILE *fopen(const char *restrict dilename,const char* restrict mode); 作用&#xff1a;打开文件参数&#xff1a; 第一个是含有要打开文件名的字符串&#xff08;"文件名…