【MySQL篇】事务管理,事务的特性及深入理解隔离级别

目录

一,什么是事务

二,事务的版本支持

三,事务的提交方式

四,事务常见操作方式

五,隔离级别

1,理解隔离性

2,查看与设置隔离级别

[3,读未提交(read uncommitted)](#3,读未提交(read uncommitted))

[4,读提交(read committed)](#4,读提交(read committed))

[5,可重复读(repeatable read)](#5,可重复读(repeatable read))

​编辑

6,串行化(serializable)

7,总结

六,深入理解隔离级别

MVCC的实现原理

3个隐藏记录字段

undo日志

模拟MVCC

[Read View](#Read View)

总结

RR与RC的本质区别


一,什么是事务

先思考一个场景,在一个火车票购票系统中,西安到郑州的火车票只剩下一张,两个用户进行买票,可以想象成这两个用户对表中数据进行CURD,而如果这些CURD不加控制,会不会出现问题?

当客户端A检查到还有一张票时,将票卖掉,还没有 执行更新数据库的语句时,客户端B检查了票数,发现大于0,于是又买了一次票。然后 A将票数更新回数据库。这就出现了同一张票被买了2次的问题。

为了解决上述的问题,就需要CURD操作满足 如下的属性:

1,买票的过程需要是原子的

2,买票之间不能互相影响

3,买完票,对数据进行修改后,数据应该永久有效

4,买前和买后的状态都是确定的


事务的概念:事务就是一组DML语句组成,这些语句在逻辑上 存在相关性,这一组DML语句要么全部成功 ,要么全部失败,是一个整体。

通俗的讲,事务就是一些SQL语句的组合。在业务层面上,为了完成某个业务,比如完成一次转账,要将一个用户的数据减100,另一个用户的数据加100,为了完成这个业务,在MySQL层面上,就需要多条SQL语句来完成。

单独的一条SQL语句之间是没有关系的,但是站在使用者的角度,这些SQL语句是 存在逻辑关系的,共同完成某个任务。这些SQL语句要么全部成功,要么全部失败。就比如转账的例子,不能使一个用户减100,后面的操作失败,导致另一个 用户没有加100.


事务就是要做或者所做的事情 ,主要用于处理操纵量大,复杂度高的数据。一个MySQL数据库,可不止一个事务在运行。在同一时刻,可能有大量的请求被包装成事务,向MySQL服务器发起事务处理请求。而每条事务至少一条SQL语句,也可能有很多条SQL语句,这样如果大家都访问同样的表数据,在不加保护的情况下,一定会存在问题。也会存在,一个事务中,一些SQL语句执行完成了,另一些SQL执行出错或者不再执行的情况,那么这种情况下该怎么办?

所以,一个完整的事务,绝对不是简单的SQL语句的集合,还需要满足如下属性:

  • 原子性:一个事务(transaction)中的所有操作,要么全部完成 ,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像 这个事务从来没有执行过。
  • 一致性:在事务开始之前和事务结束之后,数据库的完整性没有被破坏。这表示事务按照我们的预期执行的。
  • 隔离性:数据库允许多个并发事务同时对其数据进行读和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据不一致的问题。事务隔离分为不同的级别:读未提交(read uncommited),读提交(read commited),可重复读(repeatable read )和串行化(serializable)。
  • 持久性:事务结束后,对数据的修改是永久的,即便系统 故障也不会丢失。

上面的四个属性,可以简称为ACID:

原子性(Atomicity)

一致性(Consistency)

隔离性(Isolation)

持久性(Durability)

二,事务的版本支持

在MySQL中只有使用了Innodb存储引擎的数据库或表才支持事务,MyISAM不支持。

查看数据库引擎

show engines; --表格显示

show engines\G --行显示

三,事务的提交方式

事务的提交方式有两种:自动提交和手动提交

查看事务提交方式

show variables like 'autocommit'

用set来改变MySQL的自动提交模式:

四,事务常见操作方式

创建测试表

mysql> create table account(

mysql> id int primary key,

mysql> name varchar(20) not null default '',

mysql> blance decimal(10,2) not null default 0.0);

演示:事务的开始与回滚

begin 开始一个事务,commit结束这个事务。

如果事务执行到一半,未commit,客户端崩掉,MySQL会自动回滚到开始。

如果事务执行完了,commit了,客户端崩掉,MySQL数据不受影响,已经持久化。

begin开始一个事务,需要手动commit,和MySQL是否开启自动提交无关。

单条SQL与事务的关系:单条SQL也是一个事务,被MySQL封装成一个事务。


总结:

  • 只要输入begn或者start transaction,事务便必须要通过commit提交,才会持久化,与是否支持自动提交无关(autocommit)。
  • 事务可以手动回滚,同时,当操作异常,MySQL会自动回滚。
  • 对于Innodb,每一条SQL语句,都会封装成事务 ,自动提交。(select有特殊情况 ,因为MySQL有MVCC)。

五,隔离级别

1,理解隔离性

mysql可能会被多个客户端进程(线程)进行访问,访问的方式以事务的方式进行

一个事务 可能由多条SQL语句构成,也就意味着,任何一个事务,都有执行前,执行中,执行后的阶段。而所谓的原子性,就是让用户层,要么看到执行前,要么看到执行后。执行中出现问题,可以随时回滚。所以单个事务,对用户表现出来的就是原子性的。

那么在多个事务执行自己的SQL时,就有可能出现相互影响的情况。比如:多个事务访问同一张表,甚至同一行数据。

数据库中,为了保证事务执行过程中尽量不受干扰,就有了隔离性的概念。

数据库中,允许事务受不同程度的干扰,就有了隔离级别的概念。


隔离级别:

**读未提交(read uncommitted):**在该隔离级别下,所有事务都可以看到其他事务还没有提交的执行结果,(实际生产中不可能使用这种级别),相当于没有隔离性,会有很多并发问题,如脏读,幻读,不可重复读等问题。

**读提交(read committed):**该隔离级别是大多数数据库的默认隔离级别(不是MySQL的)。它满足了隔离的简单定义:一个事务只能看到其他已提交的事务所作的改变。这种隔离级别会引起不可重复读的问题,即一个事务执行时,如果多次select,会得到不同的结果。

**可重复读(repeatable read):**这是MySQL的默认级别,它确保同一个事务,在执行中,多次读取数据时,读到的是同一份数据。但是会有幻读的问题。

**串行化(serializable):**这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决了幻读的问题,它在每个读的数据行上面加上共享锁。但是可能会导致超时和锁竞争。(这种隔离级别太极端,实际生产一般不使用)

接下来是对这4中隔离级别的现象演示。

2,查看与设置隔离级别

查看

mysql> select @@global.transaction_isolation; --查看全局隔离级别

mysql> select @@transaction_isolation; --查看当前会话的隔离级别

会话启动时,当前会话的隔离级别会从全局隔离级别中拷贝一份。

设置

语法:
set [session | global] transatcion isolation level [隔离级别];

设置当前会话的隔离级别

mysql> set session transaction isolation level serializable; -- 串行化

设置全局隔离级别:

mysql> set global transaction isolation level read uncommitted;

//transaction 事务

//isolation 隔离

//level 级别

//read uncommitted 读未提交

全局隔离级别设置好以后,需要重启终端才可以生效。

mysql> select @@transaction_isolation;

3,读未提交(read uncommitted)

现象演示:(先将当前隔离级别设置为read uncommitted)

一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但是未commit的数据,这种现象叫做脏读。

4,读提交(read committed)

终端Acommit之后 :

但是,终端B事务还未结束,那么就造成了,同一个事务内,同样的读取,在不同的时间段,读到了不同的数据,这种现象,叫做不可重复读。

5,可重复读(repeatable read)

两个终端A和B,如果终端A在对应事务中insert数据,在终端B的事务周期中 ,没什么影响,也符合可重复的特点。但是,一般的数据库在可重复读的时候,无法屏蔽其他事务的insert操作(为什么?因为隔离性是对数据加锁完成的,而insert待插入的数据并不存在,那么一般加锁无法屏蔽insert数据)。会造成虽然大部分内容是可重复读的,但是insert的数据在可重复读的情况下被读出来,导致多次查找时,会多查找除出的记录,就如同产生了幻觉,这种现象叫做幻读。但是,MySQL在repeatable read隔离级别下解决了该问题。

6,串行化(serializable)

对于所有操作全部加锁,进行串行化,不会出现问题。但是效率很低,几乎不会被采用。

7,总结

隔离级别越高,安全性越高,但数据库并发性越低,往往需要在两者之间找一个平衡点。

不可重复读的重点是修改和删除:同样的条件,不同的时间,发现读取到的数据不一致。

幻读的终点在于新增:同样的条件,第一次和第二次读出来的数据记录不同。

六,深入理解隔离级别

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

读-读:不存在任何问题,也不需要并发控制

写-写:有线程安全问题,可能会存在更新丢失问题。

读-写:有线程安全问题,可能会造成事务隔离性问题,如脏读,幻读,不可重复读等问题。

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

首先抛出概念:

MVCC的全称是Multi-Version Concurrency Control,即多版本并发控制。它的主要目的是在读取数据时不加锁,从而提高并发性能。传统的锁机制可能会导致读阻塞写或者写阻塞读,而MVCC通过保存数据的历史版本来让读写操作可以同时进行,避免阻塞。

首先,每个事务都有一个唯一的事务ID,这个ID是在事务开始时分配的,按照时间顺序递增,就像每一个进程都有唯一一个PID。


MVCC的实现原理

理解MVCC的三个前置知识:

  • 3个隐藏记录字段
  • undo日志
  • Read View

3个隐藏记录字段

  1. DB_TRX_ID:6byte,最近修改(修改/插入)的事务ID,记录创建这条记录或者修改这条记录的的事务ID。
  2. DB_ROLL_PTR:7byte,回滚指针,这想这条记录的上一个版本。(这些数据一般在undo log中)。
  3. DB_ROW_ID:6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,Innodb会自动以DB_ROW_ID产生一个聚簇索引。
  4. 补充:实际还有一个删除flag隐藏字段,既记录被删除并不代表真的被删除,而是flag由0置1,或由1置0。

索引特性传送门: 【MySQL篇】索引特性,索引的工作原理以及索引的创建与管理_mysql索引b+树特性-CSDN博客

undo日志

首先,要理解一个事情,MySQL将来是以服务进程的方式,在内存中运行。索引,事务,隔离性等机制,都是在内存中完成的。即在MySQL内部的相关缓冲区中,保存相关数据,完成各种判断操作,然后再合适的时候数据刷新到磁盘上。

所以,undo日志可见简单的理解成,是一段MySQL内部的缓冲区,用来保存日志数据的。

重点会在下面模拟的时候讲解。

模拟MVCC

假设现在有一个事务,事务ID为11,对上面表的数据进行修改,将name(张三)改成name(李四)。大致过程如下:

  • 事务11,因为要修改,先对这条记录加行锁。
  • 修改前,先将该行记录拷贝到undo log中,所以undo log中就有了一行数据。
  • 现在修改MySQL中的记录,将原始记录name(张三)修改为name(李四)。并且修改原始记录的隐藏字段,DB_TRX_ID为当前事务的ID11,回滚指针列DB_ROLL_PTR,里面写入放入undo log中修改前数据的地址。从而指向上一条记录,即指向上一个版本。
  • 事务11提交,释放锁。

假设又有一个事务,事务ID为12,要将age改为30,同样按照上述过程。

总结:当一行数据被修改时,Innodb会将当前行的当前版本复制到undo log日志中,并通过DB_ROLL_PTR形成一个版本链。这样,每次修改都会都会形成一个新版本,旧版本保留在undo log中,直到不再需要为止。
上面是以更新(update)为例,如果是删除(delete)操作呢?

其实是一样的,因为上面提到过,还有一个隐藏字段flag,删除一段记录,设置flag即可。因此也可以生成版本链。


如果是insert插入数据呢?

因为insert是插入数据,之前没有数据,那么insert也就没有历史版本。但是,一般为了回滚操作,insert后的数据也是要被放到undo log日志中的。

如果当前事务commit了,那么这个undo log的历史insert记录就可以被清空了。因为一个事务跑起来了,插入数据,这个数据之前是没有的,就注定了和该事务并发运行的事务是不会用这个数据的,所以insert形成的版本链就可以清空了。

而update和delete就不一定要被清空了,因为这条事务提交了,可能会有其他事务也要访问这条记录。


如果是select操作呢?

select是不会对任何数据进行修改的。所以,维护select多版本是没有意义的。


select读取,应该读取到的是最新版本,还是历史版本?

当前读:读取最新的记录。增删改,都是当前读。

快照读:读取历史版本。

在多个事务进行当前读的时候,同时多个select,是需要加锁的,这就是串行化。

而对于读提交和可重复读的隔离级别,是在进行快照读,读取历史版本。

但如果是快照读,读取历史版本的话,是不受加锁限制的。也就是可以并发执行!换言之,调高了效率,这就是MVCC的意义。


但是,什么决定了select是当前读,还是快照读呢?

隔离级别。

事务是有执行前,执行中和执行后三个阶段的。begin-CURD-commit。

那么多个事务执行中,CURD是会交织子在一起的。那么,为了保证事务有先有后,就该让不同的事务看到该看到的内容,这就是隔离级别要完成的任务。这个工作就由接下来的read view实现。

Read View

概念:

Read View 就是事务进行快照读操作时产生的读视图,在该事务执行快照读的那一刻,记录并维护当前活跃事务的ID,这个ID是自增的,所以最新的事务,ID值越大。

Read View作用:事务进行读操作的时候,确定那些数据版本对该事务可见。


事务和Read View的关系,可以理解成进程PCB和进程地址空间的关系。让该事务看到应该看到的数据版本。

Read View在MySQL源码中,就是一个类,本质是用来进行可见性判断的。即当我们某个事务进行快照读的时候,读该记录创建一个Read View读视图,用它来判断当前哪个事务可以看见哪个版本的数据,可能是当前最新的数据,也就能是该记录的undo log日志里面的某个版本数据。


Read View类中几个重要的字段:

m_ids; //一张列表,用来维护Read View生成时刻,系统中活跃的事务ID

up_limit_id; //记录m_ids列表中事务ID最小的ID

low_limit_id; //Read View生成时刻,系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1

creator_trx_id; //创建该事务的事务ID

对于undo log中的数据版本链,我们在读取的时候,是能读取到每一个版本的事务ID的,即DB_TRX_ID。

所以,现在我们手里有两样东西:当前事务的Read View和数据版本链中的某一条记录的DB_TRX_ID。

所以,现在的问题就是当前事务应不应该看到当前版本记录。

总结

MVCC实现原理:

|-----------|-------------------------|
| 核心组件 | 作用 |
| 事务ID | 每个事务启动时,分配一个事务ID,ID是自增的 |
| undo 日志 | 存储数据的历史版本,形成版本链。 |
| Read View | 事务进行读操作时生成,确定那些版本对该事务可见 |
| 隐藏字段 | 每个记录包含3个隐藏字段 |

MVCC的优缺点:

|---------------|-------------------|
| 优点 | 缺点 |
| 非阻塞读,提高并发性能 | 需要维护undo日志,增加存储开销 |
| 减少锁冲突,降低死锁概率 | 旧版本数据不及时清理可能影响性能 |
| 支持快照图,实现一致性视图 | 复杂事务可能导致版本链过长 |

RR与RC的本质区别

有了MVCC的认识,接下来看看mysql是如何实现RC和RR两种不同的隔离级别的。

注意:read view是事务可见性的一个类,不是事务创建出来,就会有 read view,而是当事务第一次进行快照读的时候,mysql才会生成read view。

|----------|-------------------------------|----------------------------|
| 隔离级别 | read view 生成时机 | 效果 |
| 读已提交(RC) | 每次查询时生成新的read view, 可能 一直在变化。 | 能看到其他事务提交后的修改。 |
| 可重复读(RR) | 事务首次查询生成read view, 之后就不会变化了。 | 始终看到事务开始时的数据快照,避免不可重复读的问题。 |

RR 级别下,快照读生成 Read View 时, Read View 会记录此时所有其他活动事务的快照,这些事
务的修改对于当前事务都是不可见的。而早于 Read View 创建的事务所做的修改均是可见的。
而在 RC 级别下的,事务中,每次快照读都会新生成一个快照读 Read View, 这就是我们在 RC 级别下 的事务中可以看到别的事务提交的更新的原因。

正式因为RC每次快照读,都会形成新的read view,所以,RC才会有不可重复读的问题。

相关推荐
周Echo周39 分钟前
5、vim编辑和shell编程【超详细】
java·linux·c++·后端·编辑器·vim
小Tomkk41 分钟前
mysql 最长连续登录天数解析
数据库·mysql
榆榆欸41 分钟前
6.实现 Reactor 模式的 EventLoop 和 Server 类
linux·服务器·网络·c++·tcp/ip
快来卷java1 小时前
深入剖析雪花算法:分布式ID生成的核心方案
java·数据库·redis·分布式·算法·缓存·dreamweaver
tpoog1 小时前
[MySQL]数据类型
android·开发语言·数据库·mysql·算法·adb·贪心算法
云上艺旅1 小时前
K8S学习之基础六十四:helm常用命令
学习·云原生·容器·kubernetes
时光追逐者1 小时前
学习如何设计大规模系统,为系统设计面试做准备!
学习·面试·职场和发展·系统设计
爱吃喵的鲤鱼2 小时前
MySQL增删改查(CRUD)操作详解与实战指南
数据库·mysql
淬渊阁2 小时前
汇编学习之《指针寄存器&大小端学习》
汇编·学习
淬渊阁2 小时前
汇编学习之《段寄存器》
汇编·学习