关于MySQL的相关面试知识内容将会被记录到这个地方:

常见的问题:

  1. 事务的特性ACID:原子性、一致性、隔离性、持久性
  2. 隔离的级别、出现的问题、如何解决
  3. 锁:行级锁(间隙锁-(a, b),临键锁-(a,b],记录锁,插入意图锁)、页锁、表级锁
  4. 分布式锁
  5. MVCC
  6. 记录redo log、undo log、binlog
  7. 索引,索引的种类:唯一索引、普通索引、全文索引、primary key(聚集索引)、空间索引
  8. 最左前缀索引:(a,b,c)
  9. 索引的数据结构:hash表、B+树
  10. MySQL的分库分表
  11. 主从复制

MVCC: 多版本并发控制器

是保存数据的历史版本,通过对数据行的多个版本管理来实现数据库的并发控制。是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行。
乐观并发控制和悲观并发控制,并且只会在READ COMMIT和REPEATABLE READ两个隔离级别下工作。

表格的隐藏列

  • DB_TRX_ID: 记录操作该数据事务的事务ID;
  • DB_ROLL_PTR:指向上一个版本数据在undo log 里的位置指针;
  • DB_ROW_ID: 隐藏ID ,当创建表没有合适的索引作为聚集索引时,会用该隐藏ID创建聚集索引;

解决的问题:

1、读写之间的并发控制:通过 MVCC 可以让读写互相不阻塞,即读不阻塞写,写不阻塞读,这样就可以提升事务并发处理能力。
2、降低了死锁的概率:因为 InnoDB 的 MVCC 采用了乐观锁的方式,读取数据时并不需要加锁,对于写操作,也只锁定必要的行。
3、解决一致性读的问题:一致性读也被称为快照读,当我们查询数据库在某个时间点的快照时,只能看到这个时间点之前事务提交更新的结果,而不能看到这个时间点之后事务提交的更新结果。

快照读和当前读:

快照读是一种一致性不加锁的读,是InnoDB并发高的核心方法。一致性是指,读取到的数据要么是事务开始前读取到的数据或者是事务自身插入或者修改后果的数据。不加锁的select属于快照读。
当前读:读取的是最新的数据,而不是历史的数据。加锁的select就是属于当前读。

InnoDB中的数据是如何进行工作的?

事务的版本号:每开启一个事务,就会从数据库中获取一个事务id(事务的版本号),这个id是自增的,通过id可以判断事务的时间顺序。

undo log:InnoDB将行记录快照存储在Undo log里,可以在回滚段中找到它们。
image.png

可重复读隔离级别下的MVCC是如何工作的?

  • select InnoDB根据以下的条件检查每行记录:
    • 只查找数据行中版本号小于当前事务,确保事务读取的行,要么在事务开始之前就已经存在了,要么是事务自身插入或者修改过
    • 行的删除版本要么未定义,要么大于当前事务版本号。确保事务读取到的行,在事务开始之前未被删除。
  • 插入:
    • InnoDB为新插入的每一行保存当前系统版本号最为行版本号
  • 删除:
    • InnoDB为删除的每一行保存当前系统版本号作为行删除标识,删除在内部视为更新,行中的一个特殊位会被设置为删除
  • 更新:
    • InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。

小结:

  1. 一般我们认为MVCC有下面几个特点:
    • 每行数据都存在一个版本,每次数据更新时都更新该版本
    • 修改时Copy出当前版本, 然后随意修改,各个事务之间无干扰
    • 保存时比较版本号,如果成功(commit),则覆盖原记录, 失败则放弃copy(rollback)
    • 就是每行都有版本号,保存时根据版本号决定是否成功,听起来含有乐观锁的味道, 因为这看起来正是,在提交的时候才能知道到底能否提交成功
  2. 而InnoDB实现MVCC的方式是:
    • 事务以排他锁的形式修改原始数据
    • 把修改前的数据存放于undo log,通过回滚指针与主数据关联
    • 修改成功(commit)啥都不做,失败则恢复undo log中的数据(rollback)
  3. 二者最本质的区别是: 当修改数据时是否要排他锁定,如果锁定了还算不算是MVCC?
    • Innodb的实现真算不上MVCC, 因为并没有实现核心的多版本共存, undo log 中的内容只是串行化的结果, 记录了多个事务的过程, 不属于多版本共存。但理想的MVCC是难以实现的, 当事务仅修改一行记录使用理想的MVCC模式是没有问题的, 可以通过比较版本号进行回滚, 但当事务影响到多行数据时, 理想的MVCC就无能为力了。
    • 比如, 如果事务A执行理想的MVCC, 修改Row1成功, 而修改Row2失败, 此时需要回滚Row1, 但因为Row1没有被锁定, 其数据可能又被事务B所修改, 如果此时回滚Row1的内容,则会破坏事务B的修改结果,导致事务B违反ACID。 这也正是所谓的 第一类更新丢失 的情况。
    • 也正是因为InnoDB使用的MVCC中结合了排他锁, 不是纯的MVCC, 所以第一类更新丢失是不会出现了, 一般说更新丢失都是指第二类丢失更新。

      MVCC是如何实现的呢?

      为了判断版本链中哪个版本对当前事务是可见的,MySQL设计出了ReadView的概念。4个重要的内容如下
  • m_ids:在生成ReadView时,当前系统中活跃的事务id列表
  • min_trx_id:在生成ReadView时,当前系统中活跃的最小的事务id,也就是m_ids中的最小值
  • max_trx_id:在生成ReadView时,系统应该分配给下一个事务的事务id值
  • creator_trx_id:生成该ReadView的事务的事务id

当对表中的记录进行改动时,执行insert,delete,update这些语句时,才会为事务分配唯一的事务id,否则一个事务的事务id值默认为0。

max_trx_id并不是m_ids中的最大值,事务id是递增分配的。比如现在有事务id为1,2,3这三个事务,之后事务id为3的事务提交了,当有一个新的事务生成ReadView时,m_ids的值就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4。

mvcc判断版本链中哪个版本对当前事务是可见的过程如下:
image.png

执行过程如下:

  • 如果被访问版本的trx_id=creator_id,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问
  • 如果被访问版本的trx_id<min_trx_id,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问
  • 被访问版本的trx_id>=max_trx_id,表明生成该版本的事务在当前事务生成ReadView后才开启,该版本不可以被当前事务访问
  • 被访问版本的trx_id是否在m_ids列表中
    • 4.1 是,创建ReadView时,该版本还是活跃的,该版本不可以被访问。顺着版本链找下一个版本的数据,继续执行上面的步骤判断可见性,如果最后一个版本还不可见,意味着记录对当前事务完全不可见
    • 4.2 否,创建ReadView时,生成该版本的事务已经被提交,该版本可以被访问

MySQL的事务日志:redo log 与 undo log

nnoDB 事务日志包括redo logundo log,其中redo log是重做日志,提供前滚操作;undo log是回滚日志,提供回滚操作。undo log不是redo log的逆向过程,其实它们都算是用来恢复的日志:

  • redo log通常是物理日志,记录的是数据页的物理修改,而不是某一行或某几行修改成怎样,它用来恢复提交后的物理数据页,且只能恢复到最后一次提交的位置。
  • undo log用来回滚行记录到某个版本,undo log一般是逻辑日志,根据每行记录进行记录。

    redo log

    具体来说就是只记录事务对数据页做了哪些修改
    基本的特性:

  • redo log是只记录InnoDB引擎表中的修改

  • redo log对应的是物理格式上的日志,记录的是数据库中每个页的修改
  • redo log在数据修改前写入
  • redo log 只记录最后提交的事务,并且redo log是并发写入的

redo log包含:1)内存中的日志缓冲(redo log buffer),该部分是易失的 2)磁盘上重做的日志文件(redo log file),该部分是持久的。

在概念上,InnoDB 通过force log at commit机制实现事务的持久性,即在事务提交的时候,必须先将该事务的所有事务日志写入到磁盘上的redo log fileundo log file中进行持久化。

undo log

undo log有两个作用:提供回滚和多个行版本控制(MVCC)。
在数据修改的时候,不仅记录了redo log,还记录了相对应的undo log,如果因为某些原因导致事务失败或回滚了,可以借助该undo log进行回滚。
undo logredo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。
当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。在应用进行版本控制的时候,也是通过undo log来实现的:当读取的某一行被其他事务锁定时,它可以从undo log中分析出该行记录以前的数据是什么,从而提供该行版本信息,让用户实现非锁定一致性读取。
undo log是采用段(segment)的方式来记录的,每个undo操作在记录的时候占用一个undo log segment
另外,undo log也会产生redo log,因为undo log也要实现持久性保护。

delete/update 操作的内部机制

当事务提交的时候,InnoDB 不会立即删除undo log,因为后续还可能会用到undo log,如隔离级别为repeatable read时,事务读取的都是开启事务时的最新提交行版本,只要该事务不结束,该行版本就不能删除,即undo log不能删除。

但是在事务提交的时候,会将该事务对应的undo log放入到删除列表中,未来通过purge来删除。并且提交事务时,还会判断undo log分配的页是否可以重用,如果可以重用,则会分配给后面来的事务,避免为每个独立的事务分配独立的undo log页而浪费存储空间和性能。

通过undo log记录deleteupdate操作的结果发现:(insert操作无需分析,就是插入行而已)

  • delete操作实际上不会直接删除,而是将delete对象打上delete flag,标记为删除,最终的删除操作是purge线程完成的。
  • update分为两种情况:update的列是否是主键列。
    • 如果不是主键列,在undo log中直接反向记录是如何update的,即update是直接进行的。
    • 如果是主键列,update分两部执行:先删除该行,再插入一行目标行。

binlog

binlog用于记录数据库执行的写入性操作(不包括查询)信息,以二进制的形式保存在磁盘中。 binlogmysql的逻辑日志,并且由 Server层进行记录,使用任何存储引擎的 mysql数据库都会记录 binlog日志。

  • 逻辑日志: 可以简单理解为记录的就是sql语句 。
  • 物理日志mysql数据最终是保存在数据页中的,物理日志记录的就是数据页变更 。

binlog是通过追加的方式进行写入的,可以通过 max_binlog_size参数设置每个 binlog文件的大小,当文件大小达到给定值之后,会生成新的文件来保存日志。

binlog使用场景
在实际应用中, binlog的主要使用场景有两个,分别是 主从复制数据恢复

  1. 主从复制 :在 Master端开启 binlog,然后将 binlog发送到各个 Slave端, Slave端重放 binlog从而达到主从数据一致。
  2. 数据恢复 :通过使用 mysqlbinlog工具来恢复数据。

主从复制

主从复制的原理图如图所示:
image.png
Mysql的主从复制中主要有三个线程:master(binlog dump thread)、slave(I/O thread 、SQL thread),Master一条线程和Slave中的两条线程。

master(binlog dump thread)主要负责Master库中有数据更新的时候,会按照binlog格式,将更新的事件类型写入到主库的binlog文件中。
并且,Master会创建log dump线程通知Slave主库中存在数据更新,这就是为什么主库的binlog日志一定要开启的原因。
I/O thread线程在Slave中创建,该线程用于请求Master,Master会返回binlog的名称以及当前数据更新的位置、binlog文件位置的副本。
然后,将binlog保存在 「relay log(中继日志)」 中,中继日志也是记录数据更新的信息。
SQL线程也是在Slave中创建的,当Slave检测到中继日志有更新,就会将更新的内容同步到Slave数据库中,这样就保证了主从的数据的同步。
以上就是主从复制的过程,当然,主从复制的过程有不同的策略方式进行数据的同步,主要包含以下几种:

  1. 「同步策略」:Master会等待所有的Slave都回应后才会提交,这个主从的同步的性能会严重的影响。
  2. 「半同步策略」:Master至少会等待一个Slave回应后提交。
  3. 「异步策略」:Master不用等待Slave回应就可以提交。
  4. 「延迟策略」:Slave要落后于Master指定的时间。

MySQL范式

  • 第一范式:要求有主键,并且要求每一个字段原子性不可再分
  • 第二范式(依赖性):必须依赖第一范式,要求所有非主键字段完全依赖主键,不能产生部分依赖(主要针对联合主键),主键列与非主键列遵循完全函数依赖关系,也就是完全依赖 。
  • 第三范式(唯一性):必须依赖第二范式,所有非主键字段和主键字段之间不能产生传递依赖。 要求一个数据库表中不包含已在其它表中已包含的非主关键字信息。例如在一个表中出现了部门id,就不能出现部门名称等内容。

逆范式:逆范式化指的是通过增加冗余或重复的数据来提高数据库的读性能。例如在user-role中增加role_name增加可读性。

参考文档

  1. 详细分析 MySQL 事务日志(redo log 和 undo log)