MySQL数据库

一、MySQL索引

1、为什么mysql要选择B+树来存储索引?

B-树:
在这里插入图片描述
B+树:
在这里插入图片描述

(1)降低磁盘 IO 次数

  1. 假设每层节点能存 1000 个索引,3 层 B + 树就能存储 1000×1000×1000 = 10亿 条数据,查询时最多只需要 3 次磁盘 IO
  2. B + 树的所有查询最终都要落到叶子节点,无论查询哪个数据,所需的 IO 次数都是树的高度(比如 3-4 次)

(2)叶子节点有序且链表连接,支持范围查询

(3)非叶子节点只存索引键,叶子节点存索引键和数据

(4)适配聚簇索引,优化数据访问

  1. InnoDB 中,主键索引是聚簇索引(叶子节点直接存储完整数据行),辅助索引(如 name 索引)的叶子节点存储主键值(用于回表查完整数据)

2、索引有哪些分类?

索引的分类要按照不同的角度去进行分类:
1、从数据结构的角度可以分为B+树索引、哈希索引、全文索引
2、从物理存储角度可以分为聚簇索引和非聚簇索引
在MYSQL的innodb存储引擎中,数据在进行插入的时候必须要跟某一个索引列绑定在一起进行存储,如果有主键,那么选择主键,如果没有主键,那么选择唯一键,如果没有唯一键,那么系统会生成一个6字节的rowid进行存储,因此:

  • 跟数据绑定存储的索引称之为聚簇索引
  • 没有跟数据绑定存储的索引称之为非聚簇索引
  • 一张表中只有一个聚簇索引,其他非聚簇索引的叶子节点中存储的值为聚簇索引的列值

3、从逻辑角度可以分为主键索引、普通索引、唯一索引、组合索引

3、回表、索引覆盖、最左匹配原则、索引下推

(1)回表

回表表示使用非聚簇索引时,数据库引擎会先根据普通索引找到匹配的行,然后根据叶子节点中存储的聚簇索引的值去聚簇索引的索引树中查找整行记录的过程。例如:
有一张表有如下字段:id,name,age,gender,address,其中id是主键,name是普通索引
那么要进行如下SQL语句的查询:
select * from table where name = 'zhangsan';
上述SQL语句的查找过程是:先根据name的值去name的索引树上进行检索,找到匹配的记录之后取出id的值,然后再根据id的值去id的B+树上检索整行的记录,在这个过程中,查找了两棵树,多进行了棵树的IO,因此效率比较低,在生产环境中应该尽量避免回表

(2)索引覆盖

索引覆盖是指一个索引包含了查询所需要的所有数据,从而在查询中无需回表从原表中获取数据
假设有一张表,表中有以下字段:id,name,age,gender,address,其中id是主键,name是普通索引
那么要进行如下SQL语句的查询:
select id,name from table where name = 'zhangsan';
查找过程如下:在name的索引树上包含了要查询的所有字段,所以直接通过name字段去name的B+树上检索对应的记录即可,不需要找到id之后再去id的B+树上检索数据
索引覆盖可以提高查询的性能,所以在生产环境做SQL优化的时候,可以考虑索引覆盖

(3)最左匹配原则

最左匹配原则主要适用于组合索引,指的是多个列值进行匹配的时候要严格遵循从左到右的顺序,否则会导致索引失效
假设有一张表,表中有以下字段:id,name,age,gender,address
id是主键,(name,age)是组合索引

1Select * from table where name = 'zhangsan' and age = 10;
2Select * from table where name = 'zhangsan';
3Select * from table where age = 10;
4Select * from table where age = 10 and name = 'zhangsan';

上述的四条语句中,1,2,4都可以用到组合索引,3用不到
但是很多同学会有疑问,为什么第四条会用到,明明不符合最左匹配原则的顺序,
这里需要注意,如果把第四条SQL语句的条件换一下顺序,会影响最终的查询结果吗?
答案是不会的,所以mysql中的优化器会进行优化,调整条件的顺序

(4)索引下推

索引下推(Index Condition Pushdown,简称 ICP)是 MySQL 对查询的一种优化技术,核心作用是在索引遍历过程中提前过滤不符合条件的记录,减少回表查询的次数,从而提升查询效率

先理解:没有索引下推时的查询流程
当使用辅助索引(非聚簇索引,如联合索引)查询时,索引叶子节点只存储索引列的值和主键(用于回表查完整数据)。

假设场景:
表有联合索引 (name, age)
查询:SELECT * FROM user WHERE name LIKE '张%' AND age > 20;
没有 ICP 时,流程是:
存储引擎(InnoDB)根据索引 (name, age),先找到所有 name LIKE '张%' 的索引项(只看索引的第一列)。
不管 age 是否满足条件,都通过这些索引项中的主键回表,读取完整数据行。
把所有完整数据行返回给 MySQL 服务器层,由服务器层过滤 age > 20 的记录。

问题:很多不满足 age > 20 的记录也被回表了,导致无效的磁盘 IO(回表需要读数据页)。
有索引下推时的优化:过滤逻辑 “下推” 到存储引擎

有 ICP 时,流程变为:
存储引擎根据索引 (name, age),找到 name LIKE '张%' 的索引项。
在遍历索引时,直接检查当前索引项中的 age 是否满足 age > 20(因为联合索引的叶子节点包含 age 列的值)。
只对同时满足 name LIKE '张%' AND age > 20 的索引项执行回表,读取完整数据行。
直接返回过滤后的结果给服务器层,无需再过滤。

核心差异:把 age > 20 的过滤逻辑从服务器层 “下推” 到了存储引擎层,在索引层面就过滤掉无效记录,减少了回表次数

4、什么情况下会造成索引失效?

1、索引列上使用函数(replace\SUBSTR\CONCAT\sum count avg)、表达式
2、数据类型不匹配,当查询条件的数据类型和索引字段的类型不匹配
3、like 条件中前面带% 
4、在组合索引中,不满足最左匹配原则
5、使用is not null
6、mysql的优化器在进行分析的时候发现全表扫描比使用索引快的时候
7、使用or关键字会导致索引失效

5、如何设计性能优良的索引?

1、索引列占用的空间越小越好
2、选择索引列的时候尽量选择离散度高的列作为索引列,离散度的计算公式count(distinct(column_name)) / count(*),这个值越大,那么越适合做索引
3、在where后的order by字段上添加索引,如果有where条件,是跟where条件一起创建,如果没有就只是order by
4、在join on的条件字段上添加索引
5、索引的个数不要过多,会增加索引的维护成本
6、频繁更新的字段,不要创建索引,会增加索引的维护成本
7、随机无序的值,不建议作为主键索引,如身份证号,UUID
8、索引列在设计的时候最好不为NULL
9、可以使用列前缀作为索引列

6、SQL性能优化

(1)查询性能优化

(2)Explain执行计划

在需要分析的 SQL 前加上EXPLAIN,执行后即可得到执行计划。例如:

-- 分析单表查询
EXPLAIN SELECT * FROM user WHERE name = '张三' AND age > 20;

-- 分析关联查询
EXPLAIN SELECT u.name, o.order_no 
FROM user u 
JOIN order o ON u.id = o.user_id 
WHERE o.create_time > '2023-01-01';

执行后会返回一张表格,包含id、select_type、table、type、possible_keys、key等字段,每个字段都代表查询执行的关键信息。核心字段详解(重点关注这 8 个)
1、id:查询中每个 SELECT 子句的标识

用于区分多个 SELECT 的执行顺序(尤其是子查询或关联查询)。
规则:
id相同:执行顺序由上至下(如关联查询的多表)。
id不同:id越大,优先级越高(先执行子查询,再执行外层查询)

2、select_type:查询类型(判断查询复杂度)

SIMPLE:简单查询(无子查询、无 UNION)。
PRIMARY:最外层的查询(包含子查询时,外层查询标记为 PRIMARY)。
SUBQUERY:子查询中的第一个 SELECT(不依赖外层结果)。
DERIVED:派生表(如FROM后的子查询,MySQL 会临时生成一张派生表)。
UNION:UNION语句中第二个及以后的 SELECT。

3、table:当前行正在访问的表名

若为派生表,可能显示<derivedN>N 为 id 值)。
关联查询时,会按执行顺序依次显示每个表名

4、type:访问类型(最核心字段之一,判断索引使用效率)
表示 MySQL 如何访问表中的数据(从好到差排序,优化目标是至少达到range级别):

type 值含义性能
system表中只有一行数据(如系统表),是const的特例最优
const通过主键或唯一索引查询,最多返回 1 行数据(如WHERE id = 1)优秀
eq_ref关联查询中,被关联表通过主键或唯一索引匹配,每行只匹配 1 条记录优秀
ref非唯一索引查询,可能匹配多行(如WHERE name = ‘张三’,name 是普通索引)良好
range索引范围查询(如WHERE age BETWEEN 18 AND 30、id > 100)较好
index扫描整个索引树(比全表扫描好,因为索引文件通常比数据文件小)一般
ALL全表扫描(未使用索引,需优化)最差

5、possible_keys:可能使用的索引

MySQL 优化器认为 “可能适用” 的索引列表(可能有多个,但实际不一定使用)。
若为NULL,说明没有可使用的索引,大概率会全表扫描。

6、key:实际使用的索引

若为NULL,说明未使用任何索引(需重点优化)。
若possible_keys有值但key为NULL,可能是索引失效(如索引列用了函数、类型转换等)。

7、rows:预估扫描的行数

MySQL 优化器预估需要扫描的行数(非精确值)。
数值越小越好,说明查询效率越高。
若rows远大于实际数据量,可能是统计信息过时,可执行ANALYZE TABLE 表名更新统计信息。

8、Extra:额外信息(优化的关键线索)
包含执行计划的细节,常见重要值:

Extra 值含义优化建议
Using index使用覆盖索引(查询的字段都在索引中,无需回表查数据)理想状态
Using where使用了 WHERE 过滤,但未使用索引(或索引未覆盖所有过滤条件,需回表)检查是否可加索引
Using filesortMySQL需额外排序(未通过索引排序),大数据量时性能差加排序字段到索引
Using temporary创建临时表存储中间结果(如 GROUP BY 未用索引),性能损耗大加 GROUP BY 字段索引
Using join buffer关联查询未使用索引,用连接缓冲区存储中间结果加关联字段索引
Using index condition使用了索引下推(ICP)优化,在索引层过滤数据,减少回表正常现象

举例:若Extra出现Using filesort,说明查询需要排序但未利用索引排序(如ORDER BY age但索引中没有 age),可将 age 加入联合索引解决

(3)如何用执行计划优化 SQL?

  1. 看type:若为ALL(全表扫描)或index(全索引扫描),优先加合适的索引。
  2. 看key:若possible_keys非空但key为NULL,检查索引是否失效(如WHERE name LIKE '%三’会导致索引失效)。
  3. 看Extra:若有Using filesort或Using temporary,通过调整索引(包含排序 / 分组字段)消除。
  4. 看rows:若数值过大,检查索引是否被有效使用,或是否统计信息过时。

二、MySQL体系结构图

在这里插入图片描述
在这里插入图片描述

1、MySQL中的逻辑存储结构

在这里插入图片描述

  • 表空间:覆盖了所有的数据和索引,比如系统表在系统表空间,还有默认表空间…………
  • 段:多个段,组成表空间……
  • 区:多个区,组成段……一般每个区通常的大小是1MB。
  • 页:默认64个连续的页,组成一个区。每个页的默认大小是16kb。
  • 行:多行数据都在页里

其次,MySQL每次去磁盘中读取数据到内存时,每次至少会将一个页的数据加载到内存中。
也就是说,在数据库中,不论是读一行,还是读多行,都需要将一个或多个页的数据加载到内存中(Buffer
Pool)。页就是MySQL一次IO读取的最小单位。
连续的页在针对一些范围查询时,效果更佳。

2、InnoDB和MyISAM的区别

  • 关于聚簇索引的支持,InnoDB支持聚簇索引,MyISAM不支持聚簇索引
  • 关于数据缓存的支持,InnoDB支持数据缓存Buffer Pool,MyISAM不支持。
  • 关于事务,InnoDB支持,MyISAM不支持。
  • 关于MVCC,InnoDB支持,MyISAM不支持。
  • 关于外键的支持,InnoDB支持外键,MyISAM不支持外键。
  • 关于锁的支持,InnoDB支持行锁,而MyISAM只支持表锁。
  • 关于Hash索引,InnoDB和MyISAM都不支持。但是InnoDB支持一个AHI的自适应hash索引。
  • 关于存储容量的限制,InnoDB可以支持到64TB,而MyISAM支持到256TB

3、Buffer Pool

(1)Buffer Pool的存储结构

Buffer Pool(缓冲池)是MySQL主存中的一个区域。InnoDB在访问表数据时,会将数据从磁盘中拉取到Buffer Pool。他的目的就是为了加快查询和写入的速度。

在这里插入图片描述

(2)Buffer Pool的内存淘汰机制

存储结构是将整个Buffer Pool分为了两大块区域。

  • New SubList:占用Buffer Pool的5/8的大小
  • Old SubList:占用Buffer Pool的3/8的大小

内部的数据都是页,页直接是基于 链表连接
Buffer Pool的内存机制使用的是 LRU (最近最少使用)
当需要将从磁盘中获取的页存储到Buffer Pool时,会先将这个页的数据存放到Old SubList的head位置
当某个页的数据被操作(读写)了,就会放到New SubList的head位置。
如果某个页没有被操作(读写),慢慢的就会被放到Old SubList的tail位置
当我需要再次将一个新的页,存放到Buffer Pool时,如果空间不足,会将Old SubList的tail位置的页淘汰掉

4、Change Buffer

Change Buffer是针对MySQL中,使用二级索引(非聚簇索引)写数据时优化的一个策略。是在进行DML操作时的一个优化。
如果写的是 非聚簇索引 ,并且对应的 数据页没有在Buffer Pool ,此时他不会立即将磁盘中的数据页加载到Buffer Pool中。而是先将写操作扔到Change Buffer中,做一个缓冲。
等后面,要修改的这个数据页被读取时,再将Change Buffer中的记录合并到Buffer Pool中。这样就是为了减少磁盘IO次数,提高性能。
一级索引不会触发Change Buffer,一级索引速度快,直接把磁盘数据扔到Buffer Pool中,然后在内存修改

二级索引修改整体流程:

  • 更新一条记录时,当该记录在Buffer Pool缓冲区中时,直接在Buffer Pool中修改对应的页,一次内存操作。(end)
  • 如果该记录不在Buffer Pool缓冲区中时,在不影响数据一致性的前提下,InnoDB会将这些更新操作缓存在Change Buffer中,不去磁盘做IO操作。。
  • 当下次查询到该记录时,会将这个记录扔到Buffer Pool,然后Change Buffer会将和这个也有关的操作合并,进行修改。
    在这里插入图片描述

数据到ChangeBuffer后,MySQL宕机了咋整?

首先要清楚,当一个事务提交时,InnoDB会将事务的所有更改记录写到redo log(重做日志)中,包括哪些写入到Change
Buffer中的内容。咱们的保障是基于redo log实现的,即便宕机,redo log也有完整的信息。当前MySQL还会基于bin
log利用2PC的形式,确保数据一致性。

5、AHI

AHI(自适应Hash索引,Adaptive Hash Index),他是InnoDB存储特有的。是一个为了优化查询操作的特殊功能。
当AHI发现某些索引值使用的非常的频繁,建立hash索引来提升查询的效率。
AHI也是存储再Buffer Pool中的,会在Buffer Pool中开辟一片区域,建立这种自适应hash索引。
而且AHI默认是开启的
在生成AHI的自适应Hash索引后,查询效率可以从B+Tree结构的 O(logn) 提升到 O(1) 的效率
在这里插入图片描述

6、Log Buffer

Log Buffer是存储要写入到磁盘上的日志文件的一片内存区域。主要是写入redo log日志。
默认占用16M的大小。可以用过 innodb_log_buffer_size 参数调整。
他的目的很简单,就是在你做写操作时,尽量减少日志写入磁盘时的IO损耗,减少IO的次数……

7、redo log

redo log(重做日志)是InnoDB独有的。它让MySQL用于了崩溃回复的能力(一般配合bin log)。也就是MySQL宕机后,他可以根据redo log来恢复近期的数据,保证之前还没有写入到磁盘中的数据不会丢失,保证持久性和完整性。
在这里插入图片描述

(1)redo log如何保证数据的完整?

MySQL写操作不会立即将数据落到磁盘上,无论是数据还是日志。
比如数据,他优先走Change Buffer以及Buffer Pool的内存中,也是MySQL优化的手段,减少IO的消耗。
所以,为了保证数据的完整和持久性,在修改Change Buffer和Buffer Pool中的数据时,数据会优先落到redo log中。

在这里插入图片描述

(2)Log Buffer是何时触发刷盘操作?

主要由innodb_flush_log_at_trx_commit参数控制。
他的默认值是1。他可以提供三种值:

  • 0: 设置为0的时候,表示每次事务提交不刷盘,只靠后台提供的一个线程,每一秒刷新Log Buffer数据到File Cache
  • 1:(默认值)设置为1的时候,表示每次事务提交后,会立即进行刷盘操作,确保Log Buffer中的数据,落到File Cache,并且必须序列化到本地磁盘文件
  • 2:设置为2的时候,标识每次事务提交,我需要将Log Buffer中的数据刷到操作系统内存中,也就是到File Cache中

在这里插入图片描述
当innodb_flush_log_at_trx_commit设置为0的时候:
在这里插入图片描述

当innodb_flush_log_at_trx_commit设置为1的时候:
在这里插入图片描述

当innodb_flush_log_at_trx_commit设置为2的时候:
在这里插入图片描述

(3)redo log以什么形式存储的

redo log是一个文件组的形式存在的。默认是2个文件,可以配置为多个文件。每个文件的大小是一样的。
默认情况下,redo log存满了,默认大小是48M。

在这里插入图片描述

其实在写入数据到redo log文件中时,为了提升他的写入性能,他的特点是 顺序写 的操作。
在写redo log文件的时候,他会有两个指针

  • write pos(应该没问题):是记录当前要写入的位置,一边写入,这个指针一边往后移动
  • check point:记录当前要擦除的位置,一边删,一边往后移

在这里插入图片描述

8、binlog

bin log不属于存储引擎层面的日志,他是MySQL服务层的一个日志文件

(1)bin log的日志存储形式

bin log提供了三种格式来存储信息。 通过 binlog_format指定存储的形式。默认是ROW。

  • statement: 如果指定statement记录,会记录的内容是 SQL语句的原文 ,比如你在修改语句中涉及到了一些函数,比如 now(),在恢复数据时,如果是基于statement形式存储的bin log恢复的话,可能会造成重新执行 now() 函数,会斗导致时间会更新为当前系统时间,和原数据的时间 不一致
  • row(默认): 这个是默认的,他记录的内容不但具备SQL语句的内容,还会记录 当前行的具体数据 ,他不会有函数之后的时间不一致的问题。 他记录的内容 不会存在不一致 的问题,但是他需要记录的内容更多,占用的空间也会更大 ,自然同步的时候,需要的时间也就更多……
  • mixed: mixed类似是在statement和row中做了一个 权衡 ,如果设置为mixed,他会基于MySQL服务自行判断,当前数据是否会引起不一致的问题,如果没有不一致问题,用statement方式,如果可能存在不一致的问题,那就使用row

在这里插入图片描述

(2)bin log的存储数据的时机

bin log也有一个binlog cache的东西。在事务提交后,会将binlog cache中的内容存储到bin log文件中
因为一个事务中的bin log不能拆,无论多大的事务,也要保证一次性写入,所以MySQL会给每一个线程分配一块binlog cache的空间。
binlog cache的大小咱们可以自己指定,发现,binlog_cache_size的空间不大,就32kb,如果一个长事务,或者涉及 到操作的数据比较大,一个binlog_cache_size的空间不足,怎么办?
不用怕,如果查过了binlog_cache_size的空间,他会自动再次扩容内存,但是会有一个限制,不能超过max_binlog_cache_size这个参数的值,但是这个参数的值,贼大,基本不用考虑长事务或者数据量大的操作导致binlog_cache_size爆炸的问题………… 当然,咱们也要尽可能的规避这种长事务的情况……

在这里插入图片描述
在这里插入图片描述

binlog cache中的数据什么时候写入到磁盘?根据sync_binlog参数决定
整个同步的配置默认值是1,在官方也看到可以设置为0

  • 0:设置为0时,表示每次提交事务都只会做write操作,将binlog cache中的数据甩到系统的内存中,至于系统什么时候执行fsync持久化到磁盘,由系统自行控制。
  • 1(默认值):设置为1时,标识每次提交事务都会执行fsync操作,确保binlog cache中的数据一定能落到本地的磁盘中。数据是完整的。
  • 最后一个方式就是可以设置为大于1的值N,几都成。这种方式每次提交事务都会做write操作,当积累了N个事务才会主动的去做fsync操作。最惨的情况,就是丢失了N个事务的数据。优点就是,可以解决一部分IO瓶颈的问题

在这里插入图片描述

9、MySQL的两阶段提交

MySQL 的两阶段提交(Two-Phase Commit,2PC)是保证事务中 redolog(重做日志)和 binlog(二进制日志)一致性的核心机制,目的是解决 “两种日志分属不同层级(InnoDB 引擎层 vs MySQL 服务器层),可能因崩溃导致日志不同步” 的问题,最终确保数据恢复和主从复制的正确性。

(1)为什么需要两阶段提交?

redolog 和 binlog 是 MySQL 中两类关键日志,但它们的设计和作用不同:

  • redolog:InnoDB 引擎层日志,记录数据页的物理修改,保证事务的持久性(宕机后可恢复未刷盘的修改)。
  • binlog:MySQL 服务器层日志,记录逻辑操作(如 SQL 语句或行修改),用于主从复制(从库通过 binlog > 同步主库数据)和数据备份恢复。

如果没有两阶段提交,直接按 “先写 redolog 再写 binlog” 或 “先写 binlog 再写 redolog” 的顺序执行,可能出现日志不一致:

  • 假设先写 redolog,事务提交时崩溃:redolog 已记录,但 binlog 未记录。此时重启后,InnoDB 会通过 redolog 恢复事务(数据有效),但 binlog 缺失,导致从库同步不到该事务,主从数据不一致。
  • 假设先写 binlog,事务提交时崩溃:binlog 已记录,但 redolog 未记录。此时重启后,事务未被 InnoDB 恢复(数据无效),但从库会通过 binlog 执行该事务,导致主从数据不一致。

(2)两阶段提交的具体流程

两阶段提交将事务提交拆分为 “准备阶段” 和 “提交阶段”,确保 redolog 和 binlog
要么都成功写入,要么都失败,从而保持一致:

  1. 准备阶段(Prepare Phase)
    InnoDB 将事务的所有修改写入 redolog,并将 redolog 标记为 “prepare” 状态(准备完成,但未最终提交)。
    此时事务的修改已记录到 redolog(具备恢复能力),但未正式提交。
  2. 提交阶段(Commit Phase)
    MySQL 服务器层将事务的操作记录写入 binlog,并确保 binlog 刷盘(持久化)。
    若 binlog 写入成功,MySQL 通知 InnoDB 将 redolog 从 “prepare” 状态标记为 “commit” 状态(最终提交)。
    事务正式完成,所有修改生效

(3)崩溃恢复时的一致性保证(在恢复数据时,以binlog为主)

如果在两阶段中发生崩溃,InnoDB 会在重启时检查 redolog 和 binlog 的状态,通过以下逻辑恢复一致性:

  • 若 redolog 处于 “prepare” 状态:检查 binlog 中是否有该事务的完整记录。
    • 若 binlog 存在:将 redolog 标记为 “commit”,事务生效(保证两日志一致)。
    • 若 binlog 不存在:回滚该事务(丢弃 redolog 的准备状态,保证两日志都不生效)。
  • 若 redolog 处于 “commit” 状态:直接确认事务生效(此时 binlog 必然已写入,因为 commit 阶段依赖 binlog 成功)。

三、MySQL事务

1、事务的特性(ACID)

  • 原子性(Atomicity):要么成功,要么全部失败回滚
  • 一致性(Consistency):一个事务在执行之前和执行之后,数据库都必须处于一致性状态。例:事务执行一半数据库宕机,有一部分数据已经写入物理磁盘,这就产生了不一致的情况
  • 隔离性(Isolation):各个事务之间的操作互不干扰
  • 持久性(Durability):事务提交后,数据的改变就是永久性的

2、因隔离性产生的问题

当数据库存在多个事务同时执行的时候,就会产生脏读、不可重复读、幻读的问题

  • 脏读:一个事务读取到另一个事务未提交的数据
  • 不可重复读:同个事务内多次读取数据不一致
  • 幻读:同个事务多次读取,数据量不一致(其他事务往表里插数据了)

为了解决上面出现的问题,就有了事务隔离级别

3、事务隔离级别

  • 读未提交 (READ-UNCOMMITTED 简称 RU):会出现脏读、不可重复读、幻读的情况。不推荐使用。
  • 读已提交(READ-COMMITTED 简称 RC):会出现不可重复读、幻读的现象。(事务每查询一次生成一次readview)
  • 可重复读(REPEATABLE-READ 简称 RR):会出现幻读的现象。(事务开始查询只生成一次readview)
  • 可串行化(SERIALIZABLE 简称 S):实现串行化(加锁)读取数据,效率慢。
隔离级别脏读不可重复 读幻读
READ - UNCOMMITTED
READ - COMMITTED×
REPEATABLE - READ××
SERIALIZABLE×××

Oracle: 默认的隔离级别是读已提交(Read Committed)

MySQL: 默认的隔离级别是可重复读(Repeatable Read)

4、如何解决数据的读一致性问题

(1)MVCC

全称Multi-Version Concurrency Control,即多版本并发控制,指的是维持一个数据的多个版本,使得读写操作没有冲突。(多版本(一行数据的版本链)并发控制(控制读取的结果))

(2)当前读和快照读

当前读:像select lock in share mode(共享锁), select for update, update, insert ,delete(排他锁)这些操作都是一种当前读,会对读取的记录进行加锁,读取的都是最新数据。
快照读:select操作就是快照读,不加锁的非阻塞读。读取结果会根据隔离级别决定,如果是串行级别的,快照读会退化成当前读。其他就视隔离级别而定,因此快照读读到的并不一定是数据的最新版本,而有可能是之前的历史版本。

(3)MVCC解决的问题

数据库并发场景有三种,分别为:
1、读读:不存在任何问题,也不需要并发控制
2、读写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读、幻读、不可重复读
3、写写:有线程安全问题,可能存在更新丢失问题
MVCC是用来解决读写冲突的无锁并发控制,所以MVCC可以为数据库解决以下问题:
1、在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
2、解决脏读、幻读、不可重复读等事务隔离问题,但是不能解决更新丢失问题

(4)MVCC实现原理

MVCC主要依赖三个隐藏字段,undolog,read view来实现的
隐藏字段
每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段
DB_TRX_ID:记录创建这条记录或者最后一次修改该记录的事务id
DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本,用于配合undolog,指向上一个旧版本
DB_ROW_ID:隐藏的主键,如果数据表没有主键,那么innodb会自动生成一个6字节的row_id
在这里插入图片描述
undo log

回滚日志,是在进行insert,delete,update操作的时候产生的。
当进行insert操作的时候,产生的undolog只在事务回滚的时候需要,并且在事务提交之后可以被立刻丢弃
当进行update和delete操作的时候,产生的undolog不仅仅在事务回滚的时候需要,在快照读的时候也需要,所以不能随便删除,只有在快照读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除

在事务修改该行记录数据时,数据库会对该行加排他锁
然后把该行数据拷贝到undolog中,作为 旧记录,即在undolog中有当前行的拷贝副本
拷贝完毕后,修改该行name的值,并且修改隐藏字段的事务id为当前事务的id,回滚指针指向拷贝到undolog的副本记录中
事务提交后,释放锁

基于UNDO_LOG版本链
在这里插入图片描述

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

  1. trx_ids: 当前系统活跃(未提交)事务版本号集合。
  2. low_limit_id: 创建当前read view 时“当前系统最大事务版本号+1”。
  3. up_limit_id: 创建当前read view 时“系统正处于活跃事务最小版本号”
  4. creator_trx_id: 创建当前read view的事务版本号;

Read View可见性判断条件:

  1. DB_TRX_ID == creator_trx_id:说明数据就是自己这个事务修改的,所以当前事务是可见的
  2. DB_TRX_ID < up_limit_id:说明该数据是在当前事务启之前就已经存在了的,所以当前事务是可见的
  3. DB_TRX_ID >= low_limit_id:说明该数据是在当前read view 创建之后才产生的,所以当前事务是不可见的
  4. up_limit_id <= DB_TRX_ID < low_limit_id:判断DB_TRX_ID是否在活跃事务(trx_ids)中
  • 不存在:则说明read view产生的时候事务已经commit了,这种情况数据则可见的。
  • 已存在:则代表我Read View生成时刻,你这个事务还在活跃,还没有Commit,你修改的数据,我当前事务也是不可见的

(5)RC、RR级别下的InnoDB快照读区别

1、在读已提交(RC)的隔离级别下:每次select都生成一个ReadView
在这里插入图片描述
在RC隔离级别下两次select获取到的结果是不同的
在这里插入图片描述

2、在可重复读(RR)的隔离级别下:仅在第一次执行select的时候生成ReadView,后续会复用该readView

在这里插入图片描述
在RR隔离级别下两次select获取到的结果是相同的
在这里插入图片描述

(6)MVCC+RR是如何解决幻读的?为什么MVCC+RR并不能完全解决幻读?

在RR隔离级别下,如果两次快照读之间没有进行当前读操作,则不会出现幻读
在这里插入图片描述
MVCC+RR并不能解决幻读的场景:
1、两次快照读之间存在更新语句,更新其他事务已经更新过的数据
在这里插入图片描述

2、两次快照读之间存在更新语句,更新其他事务insert的数据
在这里插入图片描述

3、两次快照读之间存在更新语句,要更新的数据不冲突
在这里插入图片描述

4、使用锁定读取

在这里插入图片描述

总结:

  • RR 级别通过 MVCC 保证快照读不会出现幻读,通过间隙锁保证当前读不会出现幻读;
  • 但不能保证先快照读,再当前读的情况,所以严格意义上不算完全解决幻读
    • 如果两次查询之间出现Update、Delete语句这类当前读,并且操作的语句数据与别的事务提交的数据冲突,则DML对当前事务可见
    • 如果是锁定读取,会直接读取最新数据

(7)如何彻底解决幻读

有两个方案:

  1. 使用串行化(Serializable)隔离级别:官方推荐方案,但这种解决方案,并发性能比较低。
  2. RR + 锁:使用 RR 隔离级别,但在事务开启之后立即加锁
    事务一开启之后就加锁,之后其他事务在操作此表的相关数据时,就只能等待锁释放(事务一提交或回滚锁自动释放)

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值