全站最帅😎
发布于 2022-11-02 / 988 阅读
0
0

MySQL全篇详解

[TOC]

1. 一条sql查询语句是如何执行的?

mysql 一条语句的执行过程

mysql语句执行过程.jpg

大体来说,MySQL 可以分为 Server 层和存储引擎层两部分

Server 层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等

而存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始成为了默认存储引擎。

-- 查看连接超时时间
show variables like 'wait_timeout'
  • 连接器:连接器负责跟客户端建立连接、获取权限、维持和管理连接。一个用户成功建立连接后,即使你用管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限。修改完成后,只有再新建的连接才会使用新的权限设置
  • 分析器:对sql语句进行词法分析,判断是否满足mysql的语法要求
  • 优化器:优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。优化器阶段完成后,这个语句的执行方案就确定下来了,然后进入执行器阶段
  • 执行器:开始执行的时候,要先判断一下你对这个表 T 有没有执行查询的权限,如果没有,就会返回没有权限的错误。如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口。在有些场景下,执行器调用一次,在引擎内部则扫描了多行,因此引擎扫描行数跟 rows_examined 并不是完全相同的

2. 一条sql更新语句是如何执行的?

流程和查询语句类似

修改某条数据,先从buffer pool中寻找,如果数据不在则再从磁盘加载,然后在 buffer pool 里面更新对应的数据页,然后将这种对某个数据页的操作写入redo log buffer缓存中,接着刷盘到redo log文件(redo log是磁盘顺序写)

redo log刷盘控制:innodb_flush_log_at_trx_commit

  • 0 :设置为 0 的时候,表示每次事务提交时不进行刷盘操作

  • 1 :设置为 1 的时候,表示每次事务提交时都将进行刷盘操作(默认值)

  • 2 :设置为 2 的时候,表示每次事务提交时都只把 redo log buffer 内容写入 page cache

    另外,InnoDB 存储引擎有一个后台线程,每隔1 秒,就会把 redo log buffer 中的内容写到文件系统缓存(page cache),然后调用 fsync 刷盘。

    除了后台线程每秒1次的轮询操作,还有一种情况,当 redo log buffer 占用的空间即将达到 innodb_log_buffer_size 的一半的时候,后台线程会主动刷盘。

  • redo log是物理日志,记录了在某个数据页上做了哪些修改,属于innodb存储引擎。redo log是循环写,推进checkpoint的时候会写入

  • binlog是逻辑日志,记录的是执行语句的原始逻辑。属于mysql server 层

1.首先客户端通过tcp/ip发送一条sql语句到server层的SQL interface

2.SQL interface接到该请求后,先对该条语句进行解析,验证权限是否匹配

3.验证通过以后,分析器会对该语句分析,是否语法有错误等

4.接下来是优化器器生成相应的执行计划,选择最优的执行计划

5.之后会是执行器根据执行计划执行这条语句。在这一步会去open table,如果该table上有MDL,则等待。 如果没有,则加在该表上加短暂的MDL(S) (如果opend_table太大,表明open_table_cache太小。需要不停的去打开frm文件)

6.进入到引擎层,首先会去innodb_buffer_pool里的data dictionary(元数据信息)得到表信息

7.通过元数据信息,去lock info里查出是否会有相关的锁信息,并把这条update语句需要的 锁信息写入到lock info里(锁这里还有待补充)

8.然后涉及到的老数据通过快照的方式存储到innodb_buffer_pool里的undo page里,并且记录undo log修改的redo (如果data page里有就直接载入到undo page里,如果没有,则需要去磁盘里取出相应page的数据,载入到undo page里)

9.在innodb_buffer_pool的data page做update操作。并把操作的物理数据页修改记录到redo log buffer里 由于update这个事务会涉及到多个页面的修改,所以redo log buffer里会记录多条页面的修改信息。 因为group commit的原因,这次事务所产生的redo log buffer可能会跟随其它事务一同flush并且sync到磁盘上

10.同时修改的信息,会按照event的格式,记录到binlog_cache中。(这里注意binlog_cache_size是transaction级别的,不是session级别的参数, 一旦commit之后,dump线程会从binlog_cache里把event主动发送给slave的I/O线程)

11.之后把这条sql,需要在二级索引上做的修改,写入到change buffer page,等到下次有其他sql需要读取该二级索引时,再去与二级索引做merge (随机I/O变为顺序I/O,但是由于现在的磁盘都是SSD,所以对于寻址来说,随机I/O和顺序I/O差距不大)

12.此时update语句已经完成,需要commit或者rollback。这里讨论commit的情况,并且双1

13.commit操作,由于存储引擎层与server层之间采用的是内部XA(保证两个事务的一致性,这里主要保证redo log和binlog的原子性), 所以提交分为prepare阶段与commit阶段

14.prepare阶段,将事务的xid写入,将binlog_cache里的进行flush以及sync操作(大事务的话这步非常耗时)

15.commit阶段,由于之前该事务产生的redo log已经sync到磁盘了。所以这步只是在redo log里标记commit

16.当binlog和redo log都已经落盘以后,如果触发了刷新脏页的操作,先把该脏页复制到doublewrite buffer里,把doublewrite buffer里的刷新到共享表空间,然后才是通过page cleaner线程把脏页写入到磁盘中

第6步那里有点问题,因为第5步已经去open table了,第6步还有没有必要去buffer里查找元数据呢?这元数据是表示的系统的元数据嘛,还是所有表的?

3. 事务隔离级别

在一致性视图中,查询语句是快照读,不受别的事务的影响。而更新语句则是当前读

典型案例:

mysql> CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `k` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t(id, k) values(1,1),(2,2);

image.png

先说结论:事务A查询id=1的k值是1,事务B查询到的k值是3

  1. 事务A开启了一致性视图,查询语句为 快照读,因此在事务A被提交前,读到的k的值永远是1。
  2. 事务B也是启动了一次性视图,在B中执行update之前,事务C先执行update且自动提交事务,所以此时k的值是2。
  3. 事务C更新完K值之后自动提交,然后事务B再执行update。在mysql中,更新语句属于当前读,因此读取到的k值是2,执行完更新之后k为3,且当前数据最新版本的row trx_id变为事务B的transaction id,因为接下来在事务B中查询到的K值也是3

现在我们对语句进行一下修改:

image.png

将事务c改为在事务B的语句之后执行再提交将会发生什么?

先说结论:k值为3,但是在事务C被提交之前,事务B的更新语句将会被阻塞。
根据两阶段锁协议,只有被用到才会上锁,且只有事务提交锁才会被释放。因此事务C会对id为1的记录上锁,直到事务被提交之后才会释放锁。因此当事务C的updae语句执行后,事务B也要执行update语句,执行当前读,需要获得锁,所以只能等事务C被提交之后释放锁,拿到id为1的锁之后才能继续进行。

小结

InnoDB 的行数据有多个版本,每个数据版本有自己的 row trx_id,每个事务或者语句有自己的一致性视图。普通查询语句是一致性读,一致性读会根据 row trx_id 和一致性视图确定数据版本的可见性

  • 对于可重复读,查询只承认在事务启动前就已经提交完成的数据;
  • 对于读提交,查询只承认在语句启动前就已经提交完成的数据;

如何避免长事务对业务的影响?

首先,从应用开发端来看

  1. 确认是否使用了 set autocommit=0。这个确认工作可以在测试环境中开展,把 MySQL 的 general_log 开起来,然后随便跑一个业务逻辑,通过 general_log 的日志来确认。一般框架如果会设置这个值,也就会提供参数来控制行为,你的目标就是把它改成 1。
  2. 确认是否有不必要的只读事务。有些框架会习惯不管什么语句先用 begin/commit 框起来。我见过有些是业务并没有这个需要,但是也把好几个 select 语句放到了事务中。这种只读事务可以去掉。
  3. 业务连接数据库的时候,根据业务本身的预估,通过 SET MAX_EXECUTION_TIME 命令,来控制每个语句执行的最长时间,避免单个语句意外执行太长时间。

其次,从数据库端来看:

  1. 监控 information_schema.Innodb_trx 表,设置长事务阈值,超过就报警 / 或者 kill;
  2. Percona 的 pt-kill 这个工具不错,推荐使用;
  3. 在业务功能测试阶段要求输出所有的 general_log,分析日志行为提前发现问题;如果使用的是 MySQL 5.6 或者更新版本,把 innodb_undo_tablespaces 设置成 2(或更大的值)。如果真的出现大事务导致回滚段过大,这样设置后清理起来更方便

4. 索引

  • 哈希表:适用于等值查询,不适合范围查找。

  • 有序数组:适合等值查询、范围查找,但是更新数据的代价很大。因此有序数组适合静态数据。

  • B+树:查询时间复杂度为O(logN),因为其非叶子节点不存储数据,所以整棵树显得矮胖,对比B树因为每一个节点都存储数据所以树很高,树的层高越高那么每一次查询需要的IO的次数也越多,又因为每个节点都是通过指针连接的,所以也就意味着随机IO次数也越多。其次B+树每一层的节点之间通过双向链表连接,因为可以进行范围查询。而B树只能通过中序遍历才能实现。
    所以为了让查询尽可能的减少IO次数,那么我们应该尽可能的扩大分支树,也就是N叉树的N
    因为数据页page中数据有序,所以内部是二分查找。

    mysql中一个数据页的大小是16k,以bigint索引字段(8byte)为例,N是 16 * 1024 / 8 ≈ 1200,当层高为4时,可以存储1200的3次方也就是17亿。

    计算机存储概念:

    扇区:硬盘的读写以扇区为基本单位,通常扇区的大小是512字节,linux下通过fdisk -l 查看

    磁盘块(簇):磁盘块是文件系统的读写的基本单位,因为扇区数量太过于庞大,因此才将多个连续的扇区组合起来进行操作,其大小通常为4k,通过 sudo stat /boot 查看。且一个块只能存储一个文件,因此文件的占用空间只能为块的整数倍。磁盘块的大小可以通过 blockdev 命令进行修改。

    页:内存的最小存储单位,页的大小通常为磁盘块大小的 2^n 倍,可以通过命令 getconf PAGE_SIZE 查看。

    mysql为什么符合磁盘的存储设计呢?因为mysql中page的大小是16k,操作系统中磁盘块的大小一般为4k,一般操作系统会加载相邻的数据块,所以往往一次io就可以读取到一次完整的page。每一层节点之间通过指针连接,假设在树高20层,那么极端情况下需要进行20次随机io,这就是为什么B+树相对于B树的优势。

    L1 cache reference 0.5 ns

    Branch mispredict 5 ns

    L2 cache reference 7 ns

    Mutex lock/unlock 100 ns

    Main memory reference 100 ns

    Compress 1K bytes with Zippy 10,000 ns

    Send 2K bytes over 1 Gbps network 20,000 ns

    Read 1 MB sequentially from memory 250,000 ns

    Round trip within same datacenter 500,000 ns

    Disk seek 10,000,000 ns

    Read 1 MB sequentially from network 10,000,000 ns

    Read 1 MB sequentially from disk 30,000,000 ns

    Send packet CA->Netherlands->CA 150,000,000 ns

关于联合索引的好处:

  1. 当满足覆盖索引的条件时,可以避免回表,减少io次数,显著提升查询性能。
  2. 最左前缀:使用联合索引时,需要按照索引字段的顺序来使用查询条件,且字符串匹配遵循最左前缀原则。
  3. 索引下推:mysql 5.6之后引入了索引下推,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不符合条件的记录,减少回表的次数。

什么是change buffer?

     当我们要更新某条数据,首先判断对应的数据页是否在内存中,如果在内存中,则直接更新内存中的数据页,然后写 redo log。如果不在内存中,且开启了change buffer,则会先将数据操作写入 change buffer 中,等待合适的时机才会将操作应用到原始数据页。通过change buffer,我们减少了数据更新时需要进行随机IO来载入数据页的这个操作,加速了更新过程。需要注意的是,change buffer 使用的是 buffer pool 中的内存,而且 change buffer 也会持久化。

将 change buffer 里面的数据持久化在系统表空间这一步操作称为 purge

     将 change buffer 中的操作应用到原数据页,得到最新结果的过程称为 merge。除了访问这个数据页会触发 merge 外,系统有后台线程会定期 merge,并且在数据库正常关闭(shutdown)的过程中,也会执行 merge 操作

唯一索引在针对数据更新、插入的时候,会进行唯一性校验,必须将对应的数据页读入内存,因此唯一索引无法应用change buffer

     说了change buffer的优点,那么是否change buffer就没有缺点呢?或者说change buffer是否在所有场景下都适用呢?如果所有的更新后面,都马上伴随着对这个记录的查询,则会立即触发 merge 过程。这样随机访问 IO 的次数不但不会减少,反而增加了 change buffer 的维护代价,此时应该关闭 change buffer。而在其他情况下,change buffer 都能提升更新性能。特别是用在一些记录历史数据、归档数据的场景,尤其有效。

redo log 主要节省的是随机写磁盘的 IO 消耗(转成顺序写),而 change buffer 主要节省的则是随机读磁盘的 IO 消耗

change buffer总结:
如果数据刚被更新后又要被查询,那么应当关闭 change buffer。而在其他情况下,change buffer 都能明显提升更新性能。

为什么数据库会选错索引?

  1. 数据库统计索引区分度不准,采样的,导致优化器判断使用不同情况下的预计扫描扫描行球有序
  2. sql有排序或其他情况,优化器容易因考虑其他因素反而选错了索引

如果是第一点(expain预计扫描行数明显感觉与实际偏差过大的情况),可以通过执行 analyze table t重新进行一次索引区分度统计。对于其他情况,可以 使用 force 强制使用某索引尝试修改sql,引导数据库使用更快的索引,删除或修改不必要索引,避免优化器误判

字符串如何创建索引

  1. 直接创建完整索引,这样可能比较占用空间;
  2. 创建前缀索引,节省空间,但会增加查询扫描次数,并且不能使用覆盖索引;
  3. 倒序存储,再创建前缀索引,用于绕过字符串本身前缀的区分度不够的问题;
  4. 创建 hash 字段索引,查询性能稳定,有额外的存储和计算消耗,跟第三种方式一样,都不支持范围扫描。

5. mysql中的锁

根据加锁的范围,MySQL 里面的锁大致可以分成全局锁、表级锁和行锁三类。

全局锁

# 全局读锁
Flush tables with read lock (FTWRL)
# 解锁
unlock tables

执行读锁后,数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句将会被阻塞。

全局锁的典型使用场景是,做全库逻辑备份,但是会产生一下问题:

  • 如果在主库上进行,那么业务系统会直接停摆
  • 如果在备库上进行,那么会导致主从延迟。

官方自带的逻辑备份工具是 mysqldump。当 mysqldump 使用参数–single-transaction 的时候,备份数据之前就会启动一个事务,来确保拿到一致性视图。而由于 MVCC 的支持,这个过程中数据是可以正常更新的,所以可以通过设置隔离级别为可重复读保证一致性视图来进行备份

问题来了,为什么有了一致性视图还需要全局读锁呢?因为某些引擎(MyISAM)就不支持事务

还有一种方式可以使全库进入只读状态:set global readonly=true,但是不建议使用,原因如下:

  • 某些系统中可能通过判断readonly的值来判断一个库是主库还是备库,因为修改readonly带来的影响十分大。
  • 执行FTWRL之后,如果客户端出现异常断开,那么MySQL会自动释放这个全局锁,整个库恢复到正常状态,但是将readonly设置为true之后,客户端发生异常,数据库会一直保证readonly状态,导致整个库一直处于不可写状态,风险较高。

表级锁

MySQL中表级别锁有两种:一种是表锁,一种是元数据锁(meta data lock-MDL)。

表锁 的语法是 lock tables xxx read/write,与FTWRL类似,通过 unlock tables解锁,并且在客户端断开连接时自动解锁。

例如执行 lock tables test1 read, test2 write,那么test1上只能执行读操作,test2上能执行读写操作。

元数据锁 在访问一个表的时候会自动被加上,且 读读共享、读写互斥、写写互斥。在当前session的事务被提交的时候才释放

  • 关于元数锁提一嘴,我们在对表结构进行变更的时候,如果表的数据量非常大或者读写频繁,请使用如下方式,避免大量的会话被阻塞超时重试,而导致数据库线程被打满。
-- mysql方式
set lock_wait_timeout=N;
alter table xxx add column xxx int2 not null DEFAULT 0;

-- mariadb方式
ALTER TABLE tbl_name WAIT N add column ...

行锁

MySQL中的行锁是由各个存储引擎实现的。所以意味着不支持行锁的引擎只在并发控制的时候只能使用表锁。

行锁使用技巧:如果事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放

行锁的二阶段锁协议:只有在需要上锁的时候才会上锁,在事务被提交之后才会被释放。

这种热点行更新问题时,会存在大量的死锁检测,耗费大量CPU资源。
如何解决这种问题呢?可以将这行分成很多行(例如账户余额),这样就降低了冲突的概率。

6. 深入日志与存储

6.1 MySQL为什么有时候会抖一下?

首先,数据在被更新的时候,先更新InnoDB内存中的数据,然后将更新操作,通过组提交顺序写入redo log,这种叫做 WAL(预写日志),此时更新操作就完成了。

image.png

第一种情况:redo log 一组一般有4个文件,假设每个文件1g大小,合起来一共4g。
如果redo log写满,那么数据库会暂停所有的更新操作,推进check point,这个时候整个数据库的更新能力为0。会导致语句的执行时间明显变长。

第二种情况:“内存不够用了,要先将脏页写到磁盘”,这种情况其实是常态。InnoDB 用缓冲池(buffer pool)管理内存,缓冲池中的内存页有三种状态:

  • 第一种是,还没有使用的
  • 第二种是,使用了并且是干净页
  • 第三种是,使用了并且是脏页

InnoDB 的策略是尽量使用内存,因此对于一个长时间运行的库来说,未被使用的页面很少。而当要读入的数据页没有在内存的时候,就必须到缓冲池中申请一个数据页。这时候只能把最久不使用的数据页从内存中淘汰掉:如果要淘汰的是一个干净页,就直接释放出来复用;但如果是脏页,就必须将脏页先刷到磁盘,变成干净页后才能复用。因此,一个查询要淘汰的脏页个数太多,也会导致查询的响应时间明显变长

针对上述两种情况,如何解决呢?
首先我们需要告诉MySQL当前硬盘的IO能力,可以通过下面的命令查询。同时根据磁盘的io能力适当调整 innodb_io_capacity 配置项

fio -filename=$filename -direct=1 -iodepth 1 -thread -rw=randrw -ioengine=psync -bs=16k -size=500M -numjobs=10 -runtime=10 -group_reporting -name=mytest 

查看当前脏页比例

select VARIABLE_VALUE into @a from performance_schema.global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_dirty'; 
select VARIABLE_VALUE into @b from performance_schema.global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_total'; 
select @a/@b;

6.2 MySQL数据存储

在 MySQL 8.0 版本以前,表结构是存在以 .frm 为后缀的文件里。而 MySQL 8.0 版本,则已经允许把表结构定义放在系统数据表中了。

表数据既可以存在共享表空间里,也可以是单独的文件。这个行为是由参数 innodb_file_per_table 控制,

  • 这个参数设置为 OFF 表示的是,表的数据放在系统共享表空间,也就是跟数据字典放在一起;
  • 这个参数设置为 ON 表示的是,每个 InnoDB 表数据存储在一个以 .ibd 为后缀的文件中。

6.3 重建表

  • alter table t engine = InnoDB(也就是 recreate),这样做可以减少数据空洞,使得数据页更加紧凑。
  • analyze table t 其实不是重建表,只是对表的索引信息做重新统计,没有修改数据,这个过程中加了 MDL 读锁
  • optimize table t 等于 recreate + analyze

灵魂发问

1. 如果要删除一个表里面的前 10000 行数据,有以下三种方法可以做到:

  • 第一种,直接执行 delete from T limit 10000;
  • 第二种,在一个连接中循环执行 20 次 delete from T limit 500;
  • 第三种,在 20 个连接中同时执行 delete from T limit 500。

你会选择哪一种方法?为什么呢?
先说说三种做法带来的影响:

  • 第一种做法:直接在一个事务里面删除10000行记录,将会是一个长事务,产生一个超大的回滚段,可能导致大量的死锁检测和锁等待,还会引起主从延迟。
  • 第二种做法:一次删除500行,数量适中,最优。
  • 第三种做法:20个连接同时删除500行,可能会存在激烈的锁冲突与竞争,严重时可能导致死锁,并耗费大量的CPU资源。

2. 备份一般都会在备库上执行,你在用 –single-transaction 方法做逻辑备份的过程中,如果主库上的一个小表做了一个 DDL,比如给一个表上加了一列。这时候,从备库上会看到什么现象呢?


Q1:SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
Q2:START TRANSACTION  WITH CONSISTENT SNAPSHOT;
/* other tables */
Q3:SAVEPOINT sp;
/* 时刻 1 */
Q4:show create table `t1`;
/* 时刻 2 */
Q5:SELECT * FROM `t1`;
/* 时刻 3 */
Q6:ROLLBACK TO SAVEPOINT sp;
/* 时刻 4 */
/* other tables */

tips: 备份过程中需要开启一致性视图来保证数据一致性,Q3的保存点有大用处:在备份完某张表之后,回滚到之前的保存点,将会释放持有的MDL锁,这样不用提交事务能释放锁,就可以进行下一张表的备份了,将锁的占用时间控制到最短。

  1. 如果在 Q4 语句执行之前到达,现象:没有影响,备份拿到的是 DDL 后的表结构。
  2. 如果在“时刻 2”到达,则表结构被改过,Q5 执行的时候,报 Table definition has changed, please retry transaction,现象:mysqldump 终止;
  3. 如果在“时刻 2”和“时刻 3”之间到达,mysqldump 占着 t1 的 MDL 读锁,binlog 被阻塞,现象:主从延迟,直到 Q6 执行完成。
  4. 从“时刻 4”开始,mysqldump 释放了 MDL 读锁,现象:没有影响,备份拿到的是 DDL 前的表结构。

3. 如果表 T 中没有字段 k,而你执行了这个语句 select * from T where k=1, 那肯定是会报“不存在这个列”的错误: “Unknown column ‘k’ in ‘where clause’”。你觉得这个错误是在我们上面提到的哪个阶段报出来的呢?

解析器处理语法和解析查询, 生成一课对应的解析树。 预处理器进一步检查解析树的合法。比如: 数据表和数据列是否存在, 别名是否有歧义等。如果通过则生成新的解析树,再提交给优化器。所以答案应该是 分析器

4. 如果要重建索引 k,下面有两个 SQL 语句:

alter table T drop index k;
alter table T add index(k);

如果你要重建主键索引,也可以这么写:

alter table T drop primary key;
alter table T add primary key(id);

对于上面这两个重建索引的作法,说出你的理解。如果有不合适的,为什么,更好的方法是什么?

为什么要重建索引呢?因为索引可能因为删除,或者页分裂等原因,导致数据页有空洞,重建索引的过程会创建一个新的索引,把数据按顺序插入,这样页面的利用率最高,也就是索引更紧凑、更省空间。

解答:通过 alter table T engine=InnoDB 重建表。因为重建主键索引,会导致普通索引失效。

备案恢复后要把name的二级索引案例补全

5. 如下表结构和初始化语句作为试验环境,事务隔离级别是可重复读。现在,我要把所有“字段 c 和 id 值相等的行”的 c 值清零,但是却发现了一个“诡异”的、改不掉的情况。请你构造出这种情况,并说明其原理。

mysql> CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t(id, c) values(1,1),(2,2),(3,3),(4,4);

image.png

解答如下:
image.png

当session B 将所有行的c值进行更新,且自动提交事务。session A接下来的update语句是当前读,所以就没有符合id=c的数据行。又因为事务隔离级别是可重复读,因此session A中的所有select语句都是快照读,所以最终查询结果没有发生变化。

5. 如下图,change buffer 一开始是写内存的,那么如果这个时候机器掉电重启,会不会导致 change buffer 丢失呢?change buffer 丢失可不是小事儿,再从磁盘读入数据可就没有了 merge 过程,就等于是数据丢失了。会不会出现这种情况呢?

image.png

解答:并不会丢失change buffer的修改,因为change buffer的操作记录也一起被提交到redo log,而redo log存在crash safe能力,因此崩溃恢复的时候,change buffer也能一起恢复,之后再访问对应的数据页时,发现是脏页,于是将change buffer中的操作更新到数据页上,将数据页保持最新,这个过程称为meger。


评论