MySQL的事务,脏读,不可重复读,幻读

article/2025/10/2 23:09:45

一、什么是事务

         在MySQL中,事务是一种机制、一个操作序列,是访问和更新数据库的程序执行单元。事务中包含一个或多个数据库操作命令,会把所有的命令作为一个整体一起向系统提交或撤销操作请求,即这一组数据库命令要么都执行,要么都不执行。

二、操作事务的方法

 

三、执行事务的基本五步

该事例是模仿银行转钱的过程

        1.关闭自动提交:

                     SET autocommit=0;

        2.开启事务【记录状态到日志中去,不保存在表中是内存中的数据会丢失,在MySQL中有一个专门存放顺序存放日志的磁盘,所以每一次事务都会顺序存放在磁盘中不会丢失】

        3.一组SQL语句【事务】   

start  TRANSACTION;create table bank(bid int primary key auto_increment,bname varchar(20),bmoney decimal(20,2));insert into bank(bname,bmoney)values('洋',5),('南',100000000000000000);update bank set bmoney=bmoney-1000 where bname="南" ;update bank set bmoney=bmoney+1000 where bname="洋";select * FROM   bank;

   

  •  4.关闭事务

            4.1 提交       
    •           COMMIT;
    • 4.2 回滚
      •    ROLLBACK;

  • 5.开启自动提交

    •           set autocommit=1;

    • 执行结果:

    • 正常执行这五个步骤:得到的结果是正确的

    • 若是没有使用事务:当南扣了钱,而洋没有收到钱此时是不是就出现了疑问。

    • 但当把转钱的这个过程放在一个事务中,当转钱方扣钱成功,接收钱方加钱成功,此时才是一个事务执行成功,若是其中任意一个失败,则都不会执行。就不会存在一方扣钱,一方没有加钱的情况。

四、事务的提交模式【自动提交autocommit】

        MySQL中默认采用的是自动提交(autocommit)模式。在自动提交模式下,如果没有start transaction显式地开始一个事务,那么每个sql语句都会被当做一个事务执行提交操作。当然也可以关闭,通过start autocommit=0;需要注意的是,autocommit参数是针对连接的,在一个连接中修改了参数,不会对其他连接产生影响。

如果关闭了autocommit,则所有的sql语句都在一个事务中,直到执行了commit或rollback,该事务结束,同时开始了另外一个事务。

注意点:

在MySQL中,存在一些特殊的命令,如果在事务中执行了这些命令,会马上强制执行commit提交事务;如DDL语句(create table/drop table/alter/table)、lock tables语句等等。不过,常用的select、insert、update和delete命令,都不会强制提交事务。

五、事务的四大特性:ACID特性

ACID是衡量事务的四个特性:

原子性(Atomicity,或称不可分割性)
一致性(Consistency)
隔离性(Isolation)
持久性(Durability)
    按照严格的标准,只有同时满足ACID特性才是事务;但是在各大数据库厂商的实现中,真正满足ACID的事务少之又少。例如MySQL的NDB Cluster事务不满足持久性和隔离性;InnoDB默认事务隔离级别是可重复读,不满足隔离性;Oracle默认的事务隔离级别为READ COMMITTED,不满足隔离性……因此与其说ACID是事务必须满足的条件,不如说它们是衡量事务的四个维度。
下面将详细介绍ACID特性及其实现原理:
 

1.原子性


1.1 定义
    原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做;如果事务中一个sql语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态。

1.2 实现原理:undo log

    在说明原子性原理之前,首先介绍一下MySQL的事务日志。MySQL的日志有很多种,如二进制日志、错误日志、查询日志、慢查询日志等,此外InnoDB存储引擎还提供了两种事务日志:redo log(重做日志)和undo log(回滚日志)。其中redo log用于保证事务持久性;undo log则是事务原子性和隔离性实现的基础。

    下面说回undo log。实现原子性的关键,是当事务回滚时能够撤销所有已经成功执行的sql语句。InnoDB实现回滚,靠的是undo log:当事务对数据库进行修改时,InnoDB会生成对应的undo log;如果事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。

    undo log属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚时,InnoDB会根据undo log的内容做与之前相反的工作:对于每个insert,回滚时会执行delete;对于每个delete,回滚时会执行insert;对于每个update,回滚时会执行一个相反的update,把数据改回去。

    以update操作为例:当事务执行update时,其生成的undo log中会包含被修改行的主键(以便知道修改了哪些行)、修改了哪些列、这些列在修改前后的值等信息,回滚时便可以使用这些信息将数据还原到update之前的状态。
 

2、持久性

2.1定义
   持久性是指事务一旦提交,它对数据库的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

2.2. 实现原理:redo log

    redo log和undo log都属于InnoDB的事务日志。下面先聊一下redo log存在的背景。

InnoDB作为MySQL的存储引擎,数据是存放在磁盘中的,但如果每次读写数据都需要磁盘IO,效率会很低。为此,InnoDB提供了缓存(Buffer Pool),Buffer Pool中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:当从数据库读取数据时,会首先从Buffer Pool中读取,如果Buffer Pool中没有,则从磁盘读取后放入Buffer Pool;当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)。
 

Buffer Pool的使用大大提高了读写数据的效率,但是也带了新的问题:如果MySQL宕机,而此时Buffer Pool中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。

于是,redo log被引入来解决这个问题:当数据修改时,除了修改Buffer Pool中的数据,还会在redo log记录这次操作;当事务提交时,会调用fsync接口对redo log进行刷盘。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复。redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。
 

既然redo log也需要在事务提交时将日志写入磁盘,为什么它比直接将Buffer Pool中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因:

  (1)刷脏是随机IO,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO。

  (2)刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效IO大大减少。
2.3. redo log与binlog

    我们知道,在MySQL中还存在binlog(二进制日志)也可以记录写操作并用于数据的恢复,但二者是有着根本的不同的:

(1)作用不同:redo log是用于crash recovery的,保证MySQL宕机也不会影响持久性;binlog是用于point-in-time recovery的,保证服务器可以基于时间点恢复数据,此外binlog还用于主从复制。

(2)层次不同:redo log是InnoDB存储引擎实现的,而binlog是MySQL的服务器层(可以参考文章前面对MySQL逻辑架构的介绍)实现的,同时支持InnoDB和其他存储引擎。

(3)内容不同:redo log是物理日志,内容基于磁盘的Page;binlog的内容是二进制的,根据binlog_format参数的不同,可能基于sql语句、基于数据本身或者二者的混合。

(4)写入时机不同:binlog在事务提交时写入;redo log的写入时机相对多元:

前面曾提到:当事务提交时会调用fsync对redo log进行刷盘;这是默认情况下的策略,修改innodb_flush_log_at_trx_commit参数可以改变该策略,但事务的持久性将无法保证。
除了事务提交时,还有其他刷盘时机:如master thread每秒刷盘一次redo log等,这样的好处是不一定要等到commit时刷盘,commit速度大大加快。
 

3、隔离性

3.1定义

与原子性、持久性侧重于研究事务本身不同,隔离性研究的是不同事务之间的相互影响。隔离性是指,事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。严格的隔离性,对应了事务隔离级别中的Serializable (可串行化),但实际应用中出于性能方面的考虑很少会使用可串行化。

隔离性追求的是并发情形下事务之间互不干扰。简单起见,我们主要考虑最简单的读操作和写操作(加锁读等特殊读操作会特殊说明),那么隔离性的探讨,主要可以分为两个方面:

(一个事务)写操作对(另一个事务)写操作的影响:锁机制保证隔离性

(一个事务)写操作对(另一个事务)读操作的影响:MVCC保证隔离性
3.2 锁机制

首先来看两个事务的写操作之间的相互影响。隔离性要求同一时刻只能有一个事务对数据进行写操作,InnoDB通过锁机制来保证这一点。

锁机制的基本原理可以概括为:事务在修改数据之前,需要先获得相应的锁;获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。

4.一致性

4.1定义

        这个特性主要是说,一个事务在执行前后,读取数据库的数据是一致的。也就是说事务开始之前和事务结束之后,数据的完整性约束没有被破坏。

4.2实现原理        

行锁与表锁

按照粒度,锁可以分为表锁、行锁以及其他位于二者之间的锁。表锁在操作数据时会锁定整张表,并发性能较差;行锁则只锁定需要操作的数据,并发性能好。但是由于加锁本身需要消耗资源(获得锁、检查锁、释放锁等都需要消耗资源),因此在锁定数据较多情况下使用表锁可以节省大量资源。MySQL中不同的存储引擎支持的锁是不一样的,例如MyIsam只支持表锁,而InnoDB同时支持表锁和行锁,且出于性能考虑,绝大多数情况下使用的都是行锁。

show engine innodb status查看锁相关的部分:介绍完写操作之间的相互影响,下面讨论写操作对读操作的影响。

六、脏读、不可重复读和幻读

 

首先来看并发情况下,读操作可能存在的三类问题:

(1)脏读:

        当前事务(A)中可以读到其他事务(B)未提交的数据(脏数据),这种现象是脏读。

(2)不可重复读:

        在事务A中先后两次读取同一个数据,两次读取的结果不一样,这种现象称为不可重复读。脏读与不可重复读的区别在于:前者读到的是其他事务未提交的数据,后者读到的是其他事务已提交的数据。

(3)幻读:

         在事务A中按照某个条件先后两次查询数据库,两次查询结果的条数不同,这种现象称为幻读。不可重复读与幻读的区别可以通俗的理解为:前者是数据变了,后者是数据的行数变了。

七、 事务隔离级别

SQL标准中定义了四种隔离级别,并规定了每种隔离级别下上述几个问题是否存在。一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差。隔离级别与读问题的关系如下:

在实际应用中,读未提交在并发时会导致很多问题,而性能相对于其他隔离级别提高却很有限,因此使用较少。可串行化强制事务串行,并发效率很低,只有当对数据一致性要求极高且可以接受没有并发时使用,因此使用也较少。因此在大多数数据库系统中,默认的隔离级别是读已提交(如Oracle)或可重复读(后文简称RR)。
可以通过如下两个命令分别查看全局隔离级别和本次会话的隔离级别:

InnoDB默认的隔离级别是RR,后文会重点介绍RR。需要注意的是,在SQL标准中,RR是无法避免幻读问题的,但是InnoDB实现的RR避免了幻读问题。

RR解决脏读、不可重复读、幻读等问题,使用的是MVCC:MVCC全称Multi-Version Concurrency Control,即多版本的并发控制协议。下面的例子很好的体现了MVCC的特点:在同一时刻,不同的事务读取到的数据可能是不同的(即多版本)——在T5时刻,事务A和事务C可以读取到不同版本的数据。

MVCC最大的优点是读不加锁,因此读写不冲突,并发性能好。InnoDB实现MVCC,个版本的数据可以共存,主要基于以下技术及数据结构:

1)隐藏列:InnoDB中每行数据都有隐藏列,隐藏列中包含了本行数据的事务id、指向undo log的指针等。

2)基于undo log的版本链:前面说到每行数据的隐藏列中包含了指向undo log的指针,而每条undo log也会指向更早版本的undo log,从而形成一条版本链。

3)ReadView:通过隐藏列和版本链,MySQL可以将数据恢复到指定版本;但是具体要恢复到哪个版本,则需要根据ReadView来确定。所谓ReadView,是指事务(记做事务A)在某一时刻给整个事务系统(trx_sys)打快照,之后再进行读操作时,会将读取到的数据中的事务id与trx_sys快照比较,从而判断数据对该ReadView是否可见,即对事务A是否可见。

(1)脏读

当事务A在T3时刻读取zhangsan的余额前,会生成ReadView,由于此时事务B没有提交仍然活跃,因此其事务id一定在ReadView的rw_trx_ids中,因此根据前面介绍的规则,事务B的修改对ReadView不可见。接下来,事务A根据指针指向的undo log查询上一版本的数据,得到zhangsan的余额为100。这样事务A就避免了脏读。

(2)不可重复读

当事务A在T2时刻读取zhangsan的余额前,会生成ReadView。此时事务B分两种情况讨论,一种是如图中所示,事务已经开始但没有提交,此时其事务id在ReadView的rw_trx_ids中;一种是事务B还没有开始,此时其事务id大于等于ReadView的low_limit_id。无论是哪种情况,根据前面介绍的规则,事务B的修改对ReadView都不可见。

当事务A在T5时刻再次读取zhangsan的余额时,会根据T2时刻生成的ReadView对数据的可见性进行判断,从而判断出事务B的修改不可见;因此事务A根据指针指向的undo log查询上一版本的数据,得到zhangsan的余额为100,从而避免了不可重复读。

(3)幻读
MVCC避免幻读的机制与避免不可重复读非常类似。
 

当事务A在T2时刻读取0<id<5的用户余额前,会生成ReadView。此时事务B分两种情况讨论,一种是如图中所示,事务已经开始但没有提交,此时其事务id在ReadView的rw_trx_ids中;一种是事务B还没有开始,此时其事务id大于等于ReadView的low_limit_id。无论是哪种情况,根据前面介绍的规则,事务B的修改对ReadView都不可见。

当事务A在T5时刻再次读取0<id<5的用户余额时,会根据T2时刻生成的ReadView对数据的可见性进行判断,从而判断出事务B的修改不可见。因此对于新插入的数据lisi(id=2),事务A根据其指针指向的undo log查询上一版本的数据,发现该数据并不存在,从而避免了幻读。


http://chatgpt.dhexx.cn/article/OY2qKTc4.shtml

相关文章

脏读、幻读、不可重复读,傻傻分不清楚

脏读 &#xff08;针对未提交数据) 脏读又称无效数据读出&#xff08;读出了脏数据&#xff09;。一个事务读取另外一个事务还没有提交的数据叫脏读。 例如&#xff1a;事务T1修改了某个表中的一行数据&#xff0c;但是还没有提交&#xff0c;这时候事务T2读取了被事务T1修改…

【MySQL理论】脏读、不可重复读、幻读

文章目录 1. 脏读(dirty read)脏读是指事务读取到其他事务未提交的数据 2. 不可重复读(non-repeatable read)不可重复读是指在同一次事务中前后查询不一致的问题 3. 幻读(phantom read)幻读是一次事务中前后数据量发生变化&#xff0c;用户产生不可预料的问题 4. 不可重复读和幻…

脏读、不可重复读、幻读、丢失更新

根儿上来说&#xff0c;为什么需要事务和锁&#xff1f; 如果所有的操作都是依次进行的&#xff0c;或者说mysql的server是单线程的&#xff0c;就不会有并发问题&#xff0c;也就不需要事务和锁了。然而事实上&#xff0c;是多客户端&#xff0c;多线程的&#xff0c;所有必须…

一文搞懂MySQL脏读,幻读和不可重复读

目录 MySQL 中事务的隔离 1.READ UNCOMMITTED2.READ COMMITTED3.REPEATABLE READ4.SERIALIZABLE前置知识 1.事务相关的常用命令2.MySQL 8 之前查询事务的隔离级别3.MySQL 8 之后查询事务的隔离级别4.查看连接的客户端详情5.查询连接客户端的数量6.设置客户端的事务隔离级别7.新…

脏读、幻读和不可重复读

一、引言 脏读、不可重复读和幻读是数据库中由于并发访问导致的数据读取问题。当多个事务同时进行时可以通过修改数据库事务的隔离级别来处理这三个问题。 二、问题解释 1、脏读&#xff08;读取未提交的数据&#xff09; 脏读又称无效数据的读出&#xff0c;是指在数据库访…

一文详解脏读、不可重复读、幻读

MySQL 是支持多事务并发执行的。否则来一个事务处理一个请求&#xff0c;处理一个人请求的时候&#xff0c;其它事务都等着&#xff0c;那估计都没人敢用MySQL作为数据库,因为用户体验太差&#xff0c;估计都要砸键盘了。 既然事务可以并发操作,这里就有一些问题&#xff1a;一…

快速理解脏读、不可重复读和幻读

MySQL的InnoDB引擎是支持事务的&#xff0c;但是并发事务的处理又会带来以下问题&#xff1a; 脏读不可重复读幻读 一、脏读 脏读指事务A读取到了事务B更新了但是未提交的数据&#xff0c;然后事务B由于某种错误发生回滚&#xff0c;那么事务A读取到的就是脏数据。 具体的说…

引用及指针和引用的区别

一.在C语言中&#xff0c;我们要给函数传参&#xff0c;有两种方法。 1.传值 void Swap(int a,int b) {int tmpa;ab;btmp; } int main() {int a10;int b20;Swap(a,b);return 0; }优点&#xff1a;形参不影响实参&#xff08;保护实参&#xff09;&#xff0c;可读性强。 缺点…

C++ | 指针和引用的区别

01. C——指针和引用的区别 指针是一个存储变量地址的变量&#xff0c;指向内存的一个存储单元。 引用只是一个别名。 int a1; int *p&a;int a1; int &ba;使用sizeof看一个指针的大小是8&#xff0c;而引用则是被引用对象的大小。指针可以被初始化为NULL&#xff0c;…

C++—指针与引用的区别与联系

一、为什么要有引用&#xff08;C语言没有&#xff09; ▪ 因为引⽤和值有⼀样的语义&#xff0c;而指针不是 ▪ 不存在空引⽤&#xff0c;必须初始化&#xff1b;保证值不变&#xff0c;保证编译器更加安全 ▪ 加减号、赋值操作符&#xff0c;作⽤在引用上会触发对象的操作符重…

C++指针与引用的区别

指针和引用的区别 ①指针是一个变量&#xff0c;存储一个成员的地址&#xff1b;引用是一个常量&#xff08;指针常量&#xff09;&#xff0c;相当于一个成员的别名 ②指针声明和定义可以分开&#xff1b;引用声明时必须初始化 int* a;//指针声明 anew int(1);//指针定义 int…

【C++】---指针和引用的区别

指针和引用的区别 两者本质两者区别两者的相同点为什么传引用比传指针更安全两者做返回值效率的比较 两者本质 引用是别名&#xff0c;指针是地址、实体 两者区别 不同点分析1.初始化要求不同引用在创建的同时必须初始化&#xff0c;即引用到一个有效的对象&#xff0c;而指…

【C++】指针和引用的区别及指针传递和引用传递的区别

一、指针和引用的区别&#xff1a; 1.指针是一个变量&#xff0c;只不过这个变量存储的是一个地址&#xff0c;指向内存的一个存储单元&#xff1b;引用和原来的变量实质上是同一个东西&#xff0c;只不过是原变量的一个别名。 2.指针的值在初始化后可变&#xff0c;即指向其…

指针和引用的区别以及引用与指针基础

1引用&#xff1a; 引用&#xff08;reference&#xff09;为对象起了另外一个新的名字通过将声明符写成&d的形式来定义引用类型&#xff0c;其中d是声明的变量名&#xff1a; int ival 1024;int& refval ival;int& refval2;//报错&#xff1a;引用必须被初始化…

C++中指针和引用的区别(超详细)

指针和引用主要有以下区别&#xff1a; 引用必须被初始化&#xff0c;但是不分配存储空间。指针不声明时初始化&#xff0c;在初始化的时候需要分配存储空间。 引用初始化后不能被改变&#xff0c;指针可以改变所指的对象。 不存在指向空值的引用&#xff0c;但是存在指向空值的…

c++:指针和引用的区别

目录 前言&#xff1a; 1、引用概念上是定义一个变量的别名&#xff0c;而指针是存储一个变量的地址。 2、引用在定义时必须要初始化&#xff0c;但是指针没有要求。 3、引用在初始化时引用一个实体后&#xff0c;就不能再引用其他实体&#xff0c;因为其本质是一个指针常量…

C++中指针和引用的区别

1.指针和引用的定义和性质区别&#xff1a; &#xff08;1&#xff09;指针&#xff1a;指针是一个变量&#xff0c;只不过这个变量存储的是一个地址&#xff0c;指向内存的一个存储单元&#xff1b;而引用跟原来 的变量实质上是同一个东西&#xff0c;只不过是原变量的一个别…

C++中的指针与引用

写在前面 指针和引用形式上很好区别&#xff0c;但是他们似乎有相同的功能,都能够直接引用对象&#xff0c;对其进行直接的操作。但是什么时候使用指针&#xff1f;什么时候使用引用呢&#xff1f;这两者很容易混淆,在此我详细介绍一下指针和引用&#xff0c;力争将最真实的一…

引用和指针概念及区别

一、引用和指针 指针&#xff1a;指针是一个特殊的变量&#xff0c;它里面存储的的数值为内存里的一个地址&#xff0c;通过*访问内存地址所指向的值 引用&#xff1a;引用不是新定义一个变量&#xff0c;而是给已存在变量取了一个别名&#xff0c;编译器不会为引用变量开辟内…

01背包.动态规划.c语言实现

二维dp数组01背包 确定dp数组以及下标的含义 使用二维数组&#xff0c;即dp[i][j] 表示从下标为[0-i]的物品里任意取&#xff0c;放进容量为j的背包&#xff0c;价值总和最大是多少 2.递推公式&#xff1a;dp[i][j]dp[i-1][j-weight]value[i] dp[i][j]dp[i-1][j] 3.数组初始…