当前位置:   article > 正文

数据库(4):索引 与 事务_改成rc可以看到其他线程已提交的结果

改成rc可以看到其他线程已提交的结果

 f6530b671e504029a0f415a3818ebfb2.jpeg

"那些只言片语戳中我的心" 


一、认识磁盘 

我们都知道,mysql存在的目的,就是为了解决,用户直接与存储介质打交道效率低的问题。

磁盘是计算机中的一个机械设备,相比于计算机其他电子元件,磁盘效率是比较低的,在加上IO本身的特征,可以知道,如何提交效率,是 MySQL 的一个重要话题。

1b3c39fe05274eb287b010dcaaf1f1d7.png

①扇区 

数据库文件,本质其实就是保存在磁盘的盘片当中。也就是上面的一个个小格子中,就是我们经常所说的扇区。因此扇区 是 储存的基本单位。 

 e2b870cb266e43c7856d83c1b680c456.png

②定位扇区 

柱面(磁道): 多盘磁盘,每盘都是双面,大小完全相等。那么同半径的磁道,整体上便构成了一个柱面


每个盘面都有一个磁头,那么磁头和盘面的对应关系便是1对1的.

32dd062635744350b251903c0b5fc678.png

那么磁盘是如何读取数据的呢?只需要给已经确定好了的磁头、柱面、扇区。就可以访问扇区上的数据了(这种方式叫CHS) 但是实际的方案 采用的是另外一种 LAB线性地址转换。当然并不重要。

磁盘 与 系统、mysql数据交互大小是多少?

在设计交互数据大小上,是否需要按照 磁盘扇区大小的基本单位 进行数据的IO? 不行!

1.如果是操作系统在 与磁盘进行数据交互时,采取扇面单位大小的数据量,那么系统IO的代码会和磁盘(硬件)强相关。一旦扇区单位大小的数据量变更,系统IO代码也得跟着改变!

2.同样,如果单纯依靠 扇区的512字节 进行数据交互,肯定交互的量很少,也就意味着 当要获取大量数据时,不得不多次、反复 与磁盘进行交互,产生大量IO,降低数据交互效率。

3. 之前学习文件系统,就是在磁盘的基本结构下建立的,文件系统读取基本单位,就不是扇区,而是数据块。

 故,系统读取磁盘,是以块为单位的,基本单位是 4KB

磁盘随机访问(Random Access)与连续访问(Sequential Access)

随机访问:本次IO所给出的扇区地址和上次IO给出扇区地址不连续,这样的话磁头在两次IO操作之间需要作比较大的移动动作才能重新开始读/写数据。
 

连续访问:如果当次IO给出的扇区地址与上次IO结束的扇区地址是连续的,那磁头就能很快的开始这次IO操作,这样的多个IO操作称为连续访问。

磁盘是通过机械运动进行寻址的,随机访问不需要过多的定位,故效率比较高。

③mysql 与 磁盘 

b48224cabd694ca1ab125abe63d20ace.png

我们知道mysql 仅仅是 做了一层 用户 与磁盘交互的一个封装。本质上,mysql的文件系统仍然建立在linux文件系统之上。 

它有着更高的IO场景,所以,为了提高基本的IO效率, MySQL 进行IO的基本单位是 16KB (后面统一使用 InnoDB 存储引擎讲解)

而这个16KB的 数据单元, 在mysql这里 被称为 Page

 74178b165ace4c41bd71b462ffd2d490.png


一、索引

提高数据库的性能,索引是物美价廉的东西了。不用加内存,不用改程序,不用调sql,只要执行正确的create index ,查询速度就可能提高成百上千倍。但是天下没有免费的午餐,查询速度的提高是以插入、更新、删除的速度为代价的,这些写操作,增加了大量的IO。

所以它的价值,在于提高一个海量数据的检索速度。

索引分类:

主键索引(primary key)
唯一索引(unique)
普通索引(index)
全文索引(fulltext)

(1)索引原理

先创建一个表看看什么反应。

①索引排序

我先随机插入一些数据

2d7e1e4a91424f39b1ed1d39d6c6f3eb.png

f1b896f1804d44e4badddd2e546bc2a8.png 这样"自动"排序我完后 有什么好处?

在讲这个问题前,我们需要再回到mysql数据交互的基本单位 Page上

②为什么交互的Page大小是16KB?

往往IO效率低下的最主要矛盾不是IO单次数据量的大小,而是IO的次数。 

我们举例上述例子,如果 用户想查找数据 id=3 ,那么需要加载 id=1 id=2 一次一次这样的记录。显然,加载的越多 就会产生越多的IO交互。

但是如果我们将这一堆数据 放在一个 Page中,如果要读取数据,只需要把Page加载进 pool buffer里读取就行了,也就只需要进行一次IO。大大减少了IO次数。

用户又怎么知道 自己要查找的数据在这样的page中呢? 答案是肯定不知道!

③理解单个Page

一定时间,系统一定存在大量的IO请求。那么这些请求需要管理吗? 答案是肯定需要!

675c21e9fc2f4fb680e786113e99ac57.png

那么mysql要管理多个page,多个用户同时管理多个page。同一时间mysql内部是不是也会出现大量的page 需要被管理呢? 答案 是肯定的!

58beebd343004ab5868b9a26f0faf333.png

不同的 Page ,在 MySQL 中,都是 16KB ,使用 prev 和 next 构成双向链表 

因为有主键的问题, MySQL 会默认按照主键给我们的数据进行排序,从上面的Page内数据记录可以看出,数据是有序且彼此关联的

这样按照的排序有什么好处呢?

开头也介绍了 索引的出现。本深会对inset \ delete \update .....数据修改的操作有影响。

其目的就是 为了 提高查询的效率!

页内部存放数据的模块,实质上也是一个链表的结构,链表的特点也就是增删快,查询修改慢,所以优化查询的效率是必须的。


正式因为有序,在查找的时候,从头到后都是有效查找,没有任何一个查找是浪费的,而且,如果运气好,是可以提前结束查找过程的

④如何理解多个Page

你说的我都懂,为什么存在Page呢,答案是为了更好提高IO交互效率,为什么Page是一个结构体呢?

答案是 便于mysql进行队列 管理,为什么Page内部会进行排序呢?答案是,优化了查找效率、避免查询无效数据。 但是mysql中一定存在大量的page 那么 多个Page的情况又是怎样的呢??

现在的页模式内部,采用了链表的结构,前一条数据指向后一条数据,本质上还是通过数据的逐条比较来取出特定的数据。

如果有1千万条数据,一定需要多个Page来保存1千万条数据,多个Page彼此使用双链表链接起来,而且每个Page内部的数据也是基于链表的。

那么,查找特定一条记录,也一定是线性查找。这效率也太低了。也就得不偿失了

页目录:

我们可以举一举身边例子

338dcce2a99849be90f9a48da269b928.png“我觉得我C语言的 算术、逻辑操作符不会了,我该怎么办呢?”  是需要我们从0~118一页一页的去翻查吗?  答案是肯定的! 如果没有这个页目录!

页目录与 一本书的内容毫无关系,但为什么很多书 会选择多浪费一些纸张去 专门印上书的目录?

本质上就是为了,方便对书内容的查询! 提高读取效率。

因此,在page的认识上  这就是一种 “以空间换取时间的做法”! 

那么具体是怎么做呢?

34c6d1f836e54b82b9a0975c0e40b94c.png

如果在还没有引入页目录之前,id=3我们需要进行三次线性遍历(数据之间遍历)。 但引入目录后 只需要2[0],就可以拿到要查找的数据。大大提高了查询效率。

在单表数据不断被插入的情况下, MySQL 会在容量不足的时候,自动开辟新的Page来保存新的数据,然后通过指针的方式,将所有的Page组织起来。(下图仅仅作为演示)。

但此时,我们仍发现当数据量增大、page增多时,page与page之间就存在 线性遍历!也就存在了效率问题。那对于根本不需要加载到内存读取的page,也被迫会占用系统资源,被内存读取影响IO效率!

980f751db04246e4af18a3349fcd26e0.png

那么如何解决呢?

8a8ce22824234539a9e91a35eef6627f.png

我们也用同样的思路,这时候给Page 带上自己的“页目录”。 

用目录项来指向某Page,而这个目录项存放的就是该Page页中存放的最小数据的键值。
 

和页内目录不同的地方在于,这种目录管理的级别是页,而页内目录管理的级别是行。
其中,每个目录项的构成是:键值+指针。图中没有画全。

存在一个目录页来管理页目录,目录页中的数据存放的就是指向的那一页中最小的数据。有数据,就可通过比较,找到该访问那个Page,进而通过指针,找到下一个Page。很好地避免加载无效的Page页! 这样我们明显可以发现,查找数据id的时候 遍历的次数变小了!!! ---->IO效率提高了
 2f00e6615d7642ea82efa0b759c03972.png

但是这样是不是很浪费空间呢? 但是肯定不是! 一个Page页 的大小为16kb 光是管理下层page目录 就可以管理  4,096 个。

目录页就是页,普通页中存的数据是用户数据,而目录页中存的数据是普通页的地址。

这也就从底层上回答了,为什么主键插入 的时候会进行排序。是因为底层 page内部结构目录需要对数据id进行 排序,从而优化查询结构。

可能心细的朋友就可以看出,这货完全就是一个树形结构啊! 这难道就是传说中的B+树? 是的! 

那么这颗B+树在哪里呢?

53a6d3f0b74a43fc82ed35644b6b038c.png

80dda809b5574ed9b1428971eec29075.png

索引原理小结 

Page分为目录页和数据页。目录页只放各个下级Page的最小键值。


查找的时候,自定向下找,只需要加载部分目录页到内存,即可完成算法的整个查找过程,大大减少了IO次数

6c589c03b1a94782a2eefe620c64fb91.png

⑤InnoDB 在建立索引结构来管理数据的时候,其他数据结构为何不行?

线性、链表不用多说肯定不行。线性遍历的IO交互次数代价大

二叉树? 退化问题,成为线性遍历

平衡二叉+红黑树? 

虽然红黑+AVL树可以保证高度次的搜索效率,但唯一不足在于数据量过大,由于树的深度过大而造成磁盘IO读写过于频繁,进而导致效率低下的情况。

Hash?

官方的索引实现方式中, MySQL 是支持HASH的,不过 InnoDB 和 MyISAM 并不支持。
虽然查询的效率保证O(1),但如果是为了进行范围查找,不像B+树 Page的链表结构是相连的.

为什么不是B树? 这个和自己特别特别相近的数据结构?

 95b632a22d9d4ce98c62e8c66f32000f.png

b480d7f9e5e542aabfd1f8bbaf4b8d57.png

对我们最有意义的区别是:
B树节点,既有数据,又有Page指针

B+,只有叶子节点有数据,其他目录页,只有键值和Page指针

B+叶子节点,全部相连,而B没有

单从区别上就可以 取舍B+树 的优点。

1.B+只有叶子结点才放数据 意味着 其他节点保存的key值可以更多.

2.B+叶子结点全部相连 更便于范围查找。


(2)索引的基本操作

 ①聚簇索引 \ 非聚簇索引

这两者最大的区别就在于:

聚簇索引的数据+索引是在一起的。

费聚簇索引的数据+索引是分开的。

非聚簇索引: MyISam

聚簇索引:InnoDB

非聚簇索引:

b351b91027b54cc097afe8ef23eed6ce.png

他的叶子结点不再保存数据,而是保存的是另外一个记录数据处的 地址! 

分别创建种不同引起的表

5d4165957d074602b50bd2e87e980286.png

 MyIsam;

9d4cf3bdf8664592ac06dad18cac412a.png

Innodb;

793e28e402814bcf92740cebd34414bf.png

Mysql中,除了主键索引,也会有 用户根据场景建立其他索引。 叫做辅助(普通)索引。

对MyIsam而言,有无普通索引都没有任何影响。

但对innodb而言,因为聚簇索引的缘故:

首先检索辅助索引获得主键,然后用主键到主索引中检索获得数据,

这个过程回表查询。 为什么普通索引不在叶子结点附上数据呢? 太浪费空间了。

②创建主键索引

-- 在创建表的时候,直接在字段名后指定 primary key
create table user1(id int primary key, name varchar(30));

-- 在创建表的最后,指定某列或某几列为主键索引
create table user2(id int, name varchar(30), primary key(id));

create table user3(id int, name varchar(30));
-- 创建表以后再添加主键
alter table user3 add primary key(id); 

主键索引的特点

一个表中,最多有一个主键索引,当然可以使符合主键。
主键索引的效率高(主键不可重复)。
创建主键索引的列,它的值不能为null,且不能重复。
主键索引的列基本上是int。 

③唯一索引

-- 在表定义时,在某列后直接指定unique唯一属性。
create table user4(id int primary key, name varchar(30) unique);
 

-- 创建表时,在表的后面指定某列或某几列为unique
create table user5(id int primary key, name varchar(30), unique(name));

create table user6(id int primary key, name varchar(30));
alter table user6 add unique(name);

唯一索引的特点:

一个表中,可以有多个唯一索引
查询效率高
如果在某一列建立唯一索引,必须保证这列不能有重复数据
如果一个唯一索引上指定not null,等价于主键索引

④普通索引的创建

--在表的定义最后,指定某列为索引

create table user8(id int primary key,
name varchar(20),
email varchar(30),
index(name) );

--创建完表以后指定某列为普通索引

create table user9(id int primary key, name varchar(20), email varchar(30));
alter table+ user9 add index(name); 

-- 创建一个索引名为 idx_name 的索引

create table user10(id int primary key, name varchar(20), email varchar(30));
create index idx_name on user10(name);

普通索引的特点:

一个表中可以有多个普通索引,普通索引在实际开发中用的比较多。
如果某列需要创建索引,但是该列有重复的值,那么我们就应该使用普通索引。

⑤删除索引

第一种方法-删除主键索引:

alter table 表名 drop primary key; //主键只有一个 可以直接删除

第二种方法-其他索引的删除:
alter table 表名 drop index 索引名;   //索引名就是show keys from 表名中的Key_name 字段

alter table user10 drop index idx_name;

第三种方法方法:

drop index 索引名 on 表名;
drop index name on user8;

⑥查询索引 

第一种方法:
show keys from 表名


第二种方法:

show index from 表名;


第三种方法(信息比较简略)

desc 表名;

7ab53355db1e48749408989fa56f55e8.png

也就应证了那一句话:

一旦建立索引mysql就会默认为该索引 以Page为单位,BTree+ 形式进行组织管理。 

下面也就顺便提一下、索引创建原则:

比较频繁作为查询条件的字段应该创建索引.
唯一性太差的字段不适合单独创建索引,即使频繁作为查询条件.
更新非常频繁的字段不适合作创建索引.
不会出现在where子句中的字段不该创建索引.


三、事务

在实际使用中,不仅仅只有一个用户 使用mysql。一定会有多个用户正在登录mysql,并且可能还在使用同一个库,甚至同一张表!

下面举个例子:
8b694aa28935466bab1374f22be35a39.png

因此,对于mysql而言,无非就是CURD,因此为解决上述问题,CURD应当具备什么属性呢??

1. 买票的过程得是原子的吧。 (买到、没买到)
2. 买票互相应该不能影响吧。 (我在买、你也再买)

3. 买完票应该要永久有效吧。 (买到了、但没刷新进磁盘。数据删除了)
4. 买前,和买后都要是确定的状态吧。 (要么没有票 要么买到票)

(1)为什么会出现事务? 

我们在学习通信时,父子进程、管道、多线程、socket网络套接字......难免会遇到突发情况,导致通信无法正常进行下去、或者通信失败,例如:进程待机太久被OS发送信号干掉;为了保证网络传输的可靠性,采用TCP协议保障……

数据库同样如此,只不过它的数据交互时 应用程序 与 磁盘(等)。Mysql编写者为了 简化编程模型,不去考虑各种各样的潜在问题 或是 并发问题。当我们使用事务时,要么提交,要么回滚,我们不会再去虑网络异常了,服务器宕机了,同时更改一个数据怎么办...... 所以务本质上是为了应用层服务的.而不是伴随着数据库系统天生就有的。
 

 (2)什么是事务?

事务就是一组DML语句组成,这些语句在逻辑上存在相关性。

这一组DML语句要么全部成功,要么全部失败(原子性),是一个整体。

针对Mysql,不免会提出以下几个问题:

1.因为多条sql语句组成,如果执行到一半,服务挂掉了,那么已经执行、没执行的应该怎么办?

2.事务sql语句不加保护会怎样(问题是什么)?

3.一个 MySQL 数据库,可不止你一个人在处理数据,用户与用户 之间怎么保证互不干扰?

①事务特点(ACID) 

原子性(Atomicity):个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

一致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。

隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。

持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

事务特点的概念一大把,但是事实上没必要去记住。下述事务特点 就是回答上述问题的标准答案。

WHO!保障Mysql数据库的安全性?事务!

②事务的版本

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

这也是为什么虽然没MyISam在索引技术上 不需要“回表查询”,效率 胜过Innodb.但innodb使用的范围更广。

4a289ed21e774508a031c1519d75a304.png

(3)事务的提交方式

事务的提交方式有两种:

手动提交

自动提交

默认情况下,mysql都为我们打开了自动提交。 SQL语句都会被打包成事务 提交!

为什么这么说呢?

在前面的练习中,可能你觉得没有事务的出现,但事实上却处处都是单句"事务"。

所以任意一条sql语句 都是一个事务。

show variables like 'autocommit'

9193f61b95974ee68ceab02f849ef8a2.png

 set autocommit;

a83c9ff68dde4e26b2f60172b4228378.png

(4) 事务常见的基本操作

启动事务的方法:

1.start transaction || begin;

2.正常操作(autocommit);

①插入 与 回滚 

41819005d52a40cb917c1bd964344aa0.png

b08f205215c74aeba22186c1308daabe.png

4c5478880f76426b83eb3ad065328807.png

savepoint + 点名;  记录当前保存点

rollback to +点名;   退回哪个保存点

rollback ; 直接回到beign处 

②如果客户端在没有commit时 发送崩溃?

我们在事务特性一栏看到它具有原子性,那这个原子性又是怎么体现的呢?

4b9e9d15e8e2485eaeee524be1b6d7c8.png

681035293e9d485dbcb6ddba2528fea0.png

 ③如果客户端已经commit 发生崩溃了

803a9a612267468dbd2f0970fee000fe.png

commit后,客户端崩溃,MySQL数据不会在受影响,已经持久化!

④小结:

1.只要输入begin或者start transaction,事务便必须要通过commit提交,才会持久化,与是否设置set autocommit无关。

2.事务可以手动回滚,同时,当操作异常,MySQL会自动回滚

3.对于 InnoDB 每一条 SQL 语言都默认封装成事务,自动提交.(select有特殊情况,因为 MySQL有"MVCC")  ; MACC之后会细讲。

 我们能看到事务本身的原子性(回滚),持久性(commit) , 那么什么是隔离性? 什么是一致性呢?

(5)事务隔离性1 

 ①如何理解隔离性

1.MySQL服务可能会同时被多个客户端进程(线程)访问,访问的方式以事务方式进行。


 2.一个事务可能由多条SQL构成,也就意味着,任何一个事务,都有执行前,执行中,执行后的阶段(这跟原子性没有冲突!!!)。而所谓的原子性,其实就是让用户层,要么看到执行前(结果),要么看到执行后(结果)。而执行中出现问题,可以随时回滚!但对单个事务,给用户表现出来的特性,就是原子性。

因此如何保证 多进程(线程)访问的mysql条件下,如何保证各个事务在执行过程中互不干扰。

        这就叫 "隔离性"。

而允许事务受不同程度的干扰(本质是适用场景决定),就有了一种重要特征:"隔离级别"

②隔离级别 

读未提交[Read Uncommitted]:

在该隔离级别,所有的事务都可以看到其他事务没有提交的执行结果。

读提交[Read Committed]:

该隔离级别是大多数数据库的默认的隔离级别(不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变。

可重复读[Repeatable Read]:

这是 MySQL 默认的隔离级别,它确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行。

串行化[Serializable]: (类似于生产消费之模型)

这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突。

隔离级别如何实现:隔离,基本都是通过锁实现的,不同的隔离级别,锁的使用是不同的。常见有,表锁,行锁,读锁,写锁,间隙锁(GAP),Next-Key锁(GAP+行锁)等。不过,我们目前现有这个认识就行,先关注上层使用。
 

③查看与设置隔离级别 

全局:

select @@global.tx_isolation;  --查看全局隔级别

局部:

select @@session.tx_isolation; --查看会话(当前)全局隔级别

默认:

select @@session.tx_isolation;  --默认同上

4aa53d5d25cd4e25902da6f684d3eb0e.png

设置隔离级别:

set 全局\会话 transaction isolation level 隔离级别(serializable)

ea98ae34e77844029ec3277de5948bbe.png

(6)再谈四种隔离级别

①读未提交(Read Uncommitted) 

顾名思义,这种隔离级别在实际中 根本不可能采用。 因为就等于没隔离。

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

d09f9ea98094471888fbd7e00a582358.png因此读 未提交存在大量的 脏读情况。 

②读提交(Read Committed)

这种隔离级别会引起不可重复读,即一个事务执行时,如果多次 select,可能得到不同的结果。

cab0d34d366b4a85a10a486a97373c0c.png

如果事务不退出,那么一旦对端进行 commit 原读取的内容 就会发生改变----->不可重复读

不可重复度是不是问题? 

41760d802f664cc5adfd67e978b9a96e.png  

③可重复读(Repeatable Read)

和读提交 很大区别就是,对数据可重复读

但是会有"幻读"问题

幻读:幻读 就是一个事务读到另一个事务新增加并提交的数据(insert)。

3ab4ad9e25244af3b972225d47eef68f.png

可重复读的幻读问题:

f603b760e07f4e84bc179caf0609d388.png

c4e47ec56e714427b71dbc9b504d9bc0.png

 造成幻读的最根本原因在于:insert;

因为隔离性是 通过加锁完成的。而由于insert的数据 本来是在原来数据表中不存在的,光加锁是无法避免这个问题的。 

由此,insert的数据就会被大量读取出来,就会查找出新的记录。

很明显,mysql 在RR级别上 是进行了处理的!!

④串行化 

能从彻底解决幻读问题。它在每个读的数据行上面加上共享锁。

但是可能会导致超时和锁竞争(这种隔离级别太极端,实际生产基本不使用)。

56a2dfdeb6eb45ac9be40a8265d2c9cc.png

可以看出 A端直接 卡住了? 为什么? 

8d086d3d6ada47c9b18fdaf64e6c6ff3.png

 此时B端 commit后 A端才成功执行。

58981189404e48429e749c41f7cfed46.png

此时共享锁. 

因此串行化的本质:

be78e1ec4abc4390ade2a209170855af.png

⑤隔离级别小结 

1.隔离级别越严格,安全性越高,但数据库的并发性能也就越低。

2.事务间互相影响,指的是事务在并行执行的时候。

3.不可重复读的重点在于:修改、删除数据后 数据是否和原select 一样。

4.脏读的重点在于:读取了未commit的数据。

   幻读的重点在于:insert情况下数据无法依靠加锁保证可重读。

fd51d3a0c8af4c37b56d482c851256fa.png

(7)谈谈一致性 

前面也已经分别 举例 探讨了 原子性、持久性、隔离性。那么什么是一致性呢?

所谓一致性:事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。

也就是一种 结果的确定性!

其实一致性和用户的业务逻辑强相关,一般MySQL提供技术支持,但是一致性还是要用户业务逻辑做支撑,是由用户和ACD 一同决定。

63b496c563724e40be36aa08bdfd5636.png

(8)再谈隔离性2(底层) 

在隔离性处谈了,事务的隔离性保证了 ,事务能在多进程(线程)的情况下,互不干扰。那么,隔离性是怎么实现的呢? 它们 与 隔离级别又是什么关系呢?

数据并发的三种场景:

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


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


写-写 :有线程安全问题。

 其中最为复杂的就是 读-写场景:其余两个场景其实都较为极端,也就不在此分析。

①读-写场景;什么是MVCC

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

1.为事务分配单向增长的事务ID,为每个修改保存一个版本,版本与事务ID关联。

2.读操作只读该事务开始前的数据库的快照(undo 日志中)。

从而实现:

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

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

②MVCC隐藏的三个提前知识 

1.3个记录隐藏字段.
2.undo 日志.

3.read view.

③三个隐藏字段

DB_TRX_ID:

6byte,最近一次修改的事务ID(每个事务都会被分配的id)。

DB_ROLL_PTR:

7byte; 回滚指针 指向这条记录的上一个版本(历史版本,存放在 undo log)。

DB_ROW_ID:

6 byte; 隐含的自增ID(隐藏主键),如果主表没有主键,Innodb会自动以 DB_ROW_ID产生一个聚簇索引 

补充:实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了(不是很重要);

d940b9f14a1a40ac9dda4e3665b937b0.png

nameageDB_TRX_ID(记录该事务最近一次修改ID)DB_ROW_ID(隐式主键)DB_ROLL_PTR(回滚指针)
张三28null1null

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

④undo日志

undo日志 可以暂时简单理解,就是 MySQL 中的一段内存缓冲区,用来保存日志数据的就行。

454116ec0fd5420fbaa35e61b144fef9.png

我们就有了一个基于链表记录的历史版本链(副本)。所谓的回滚,无非就使用历史数据,覆盖当前数据。
 而这些一个一个版本,我们可以称之为一个一个的快照。

以上只是列举了 update的情况。

1.delete: 前面我们只讲了三个隐藏字段,还有一个也就是flag 其实就是把lfag置位删除即可。

2.insert: insert的前提是,之前没有任何历史版本。但是为了 适应回滚操作,也会把insert 的数据放入 到undo 日志当中。 undo日志里的insert 副本 会在commit后清除。

3.selece:select去维护多版本并发(MVCC)并没有任何实际意义。因为它是读取\不会对数据做任何更改。 

但唯一值得探讨的问题是,select 最新数据 还是 undo日志里的 快照呢?

当前读:改什么读什么,增删改 都可以认为是当前读 也适用于select;

(select * from user lock in share mode)
 

快照读:读取历史版本链里的副本。

为什么select选择快照读?

当前读 不仅仅有select,还有 insert、update、delete。这些过程在 并发执行时,一定都会进行加锁保护。如果是select 要进行 读取最新数据,也是需要加锁!! 这也就成了串行化隔离级别! IO效率极低。

但select快照读 既不需要加锁、又可以并行执行,换言之,提高了效率!!! 

然而,真正决定 select选择快照读的是 -----“隔离级别!!!”

为什么要有隔离级别? 前面说隔离级别指的是事务之间受不同的干扰程度!

换句话说,事务是原子的,总有先 有后。

所谓不同的干扰程度 是由事务先、后来决定!

为了保证事务的“有先有后”的差别,

那就要让不同事务看到应看到的内容,这就是隔离性 与 隔离级别要解决的问题。

举个简单的例子: 

如果我们和李白 处在同一时空。他是唐朝人,比我们先到这个世上,为了维护“秩序”(一致性),那么我们就应该设置“隔离性”,目的是为了不让他看到 不属于自己时代的东西。至于让他看到哪一时期的内容完完全全由他自己 出现的“先后顺序”影响!  如果是秦始皇 那么它就看不到 之后的"汉朝”,反观李白就能看到“汉朝”。这也就是 隔离性级别 在其中起作用。

⑤Read view

上面说,隔离性的本质,就是让一个事务看到它该看到的,不同事务看到,不同的内容。换句话说,也就是如何如何实现隔离级别?

Read View就是事务进行 快照读 操作的时候生产的 读视图 (Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)。
 

Read View 在 MySQL 源码中,就是一个类,本质是用来进行可见性判断的。

因此,为什么要有隔离性?是因为事务并行时 需要保护互不干扰?为什么需要隔离级别?是因为事务是原子的,总是有“先后顺序”的。他们的先后顺序决定了他们能看到的内容。 Read view就是对事务 可见性判断的 标杆!

即当我们某个事务执行快照读的时候,对该记录创建一个 Read View 读视图。把它比作条件,用来判断当前事务能够看到哪个版本的数据。

Read view类(简化);

m_ids;  用来维护 Read view生成时 系统活跃的事务ID

up_limit_id; m_ids中最小的事务 id

low_limit_id ; Read view 生成时刻系统尚未分配的下个事务id,也就是已知ID+1

create_trx_id;  创建Read view事务id

Read View快照读取范围:

此时我们有自己的历史版本链,每一个版本都对应着自己的事务id:DB_TRX_ID;

现在的问题就在于,那么多个历史版本链,我该对哪一个版本进行快照读呢?

4b28632c3f74474daa169be2d1dc46d1.png

有了上面的铺垫,我们去看看前面遗留的一个问题:

RR 与 RC的本质区别;

RC:

267eeb8141c24ab7b50a80eb7dbd1534.png

RR:

a69cf9500551479ebcee771f610827de.png

事务A操作事务A描述事务B操作事务B描述
begin开启事务begin开启事务
select* from快照读(无影响)select* from形成快照
insert into插入-
commit提交-
select* from原来数据没读到insert

2ea881d5c4c64fed8fe70c9e835b17c1.png

事务A操作事务A描述事务B操作事务B描述
begin开启事务begin开启事务
select* from快照读(无影响)--
insert into插入--
commit提交--
select* from形成快照(能够读取到新的数据)

因此,上述现象说明了什么?

事务中快照读的结果是非常依赖该事务首次出现快照读的地方,即某个事务中首次出现快照读,决定该事务后续快照读结果的能力。

RC、RR两者的区别

Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同。

对于RR为了保证可重复读 第一次快照读会创建一个快照及Read View。使用的是同一个 Read View

对于RC 每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下的事务中可以
看到别的事务提交的更新的原因。


总结

①读取磁盘的本质.就是根据地址去访问每一片扇区。

②mysql与磁盘 不直接打交道。mysql 会在内存中申请pool buffer 进行操作。

③mysql、os 采用独立的 与 磁盘扇区大小不一样的标准,一是为了提升效率,二是硬件软件解耦。

④索引的创建、索引的删除、索引的分类。

⑤索引:以Page为基本单位,以B+树的形式、进行组织管理 (数据结构+算法)。

⑥为什么需要事务?mysql存在并发的安全问题。

⑦事务特性(AICD):原子性、隔离性、一致性、持久性

⑧四大隔离级别; 脏读、幻读、不可重复读问题;

⑨隔离性是什么?各个事务独立、互不干扰 什么是隔离级别?事务互不干扰的程度

   为什么需要隔离性? 事务是原子的 

   为什么需要隔离级别? 事务到来总有先、后,各个事务由先、后 决定能看到的内容。

⑩读-写并发的 多版本并发(MVCC) 事务id由先、后自增 读数据的本质是从undo日志读取历史版本。

     如何理解MVCC三个前提(三个隐藏字段、undo日志、read view(真正保证可见性的))

本篇到此结束 感谢您的阅读 

祝你好运~

70a826f74f4f48c9a69081bc7ad001a9.png

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/402782
推荐阅读
相关标签
  

闽ICP备14008679号