MySQL-突击班(第四天)

一、MVCC

1.1 MVCC是个啥?

MVCC(Multi Version Concurrency Control),多版本并发控制。

他就是一种提升并发能力的技术。 最早的时候操作,基本只有读读是可以并发执行的。只要涉及到了写操作,那就必须要阻塞。

但是引入了MVCC之后,咱们可以做到读写,写读的并发。但是写写依然是互斥的。

在MVCC内部,是基于InnoDB通过undo log保存的数据记录的版本信息来实现的。每个事务读到的数据版本可能会不一样。在同一个事务中,用户只能看到当前事务创建快照前就已经提供了的数据,以及事务本身操作的数据。

MVCC在 Read Committed以及Repeatable Read中才会使用到。

MVCC的实现是基于三点来玩的:隐藏字段、undo log、Read View 这三者配合实现的。

1.2 隐藏字段是个啥?

InnoDB向数据库中存储的每一行添加了三个字段:

DB_TRX_ID: 标识最近一次对当前行数据做修改(Insert,Update)的事务ID。至于delete操作,属于Update。事务ID是递增的~~

DB_ROLL_PTR: 回滚指针,undo log中记录的多个版本之间,使用DB_ROLL_PTR来连接上。

DB_ROW_ID: 如果表里没有主键,没有非空唯一索引,那么这个隐藏字段会作为聚簇索引存在。这玩应和MVCC关系不大,了解即可。

1.3 undo log存储的数据结构

比如现在有一张user表,里面有id,name两个字段。现在数据存在一条,id = 1,name = 张三

如果,现在长这样

现在有一个事务ID为2,要修改这个数据,将name修改为李四

  • 获取互斥锁
  • 需要先将当前数据行复制到undo log中,作为旧版本。
  • 复制完毕后,将张三修改为李四,并且将DB_TRX_ID修改为2,并且将回滚指针指向undo log里的旧版本
  • 提交事务后,释放锁。

现在又来了一个事务ID为3,修改这行数据,将李四修改为王五

  • 获取互斥锁
  • 需要先将当前数据行复制到undo log中,作为旧版本。
  • 复制完毕后,将李四修改为王五,并且将DB_TRX_ID修改为3,并且将回滚指针指向undo log里的旧版本
  • 提交事务,释放锁

1.4 Read View内部结构存储了个啥?

Read View其实和快照是一个意思。

Read View是读操作中的可见性判断的核心,也就是当前事务能不能读取undo log中的某行数据以及当前行数据。Read View内部还维护的很多的属性以及逻辑。

在开启事务后, 执行第一个select操作后 ,会创建一个Read View,也就是快照。

在Read View中会保存不应该被当前事务看到的,其他 活跃事务的列表

当用户在这个事务中要读取某行记录时,InnoDB会将该行的 DB_TRX_IDRead View 中的一些变量去做比较,判断当前数据能否查看到。

要查看一下Read View 里面的存储结构信息。

https://github.com/facebook/mysql-8.0/blob/8.0/storage/innobase/include/read0types.h

这四个属性中,分别聊一下:

  • m_creator_trx_id: 当前事务的TRX_ID,也就是事务ID
    • 人话:当前事务的ID,当前事务要创建这个Read View快照。
  • m_ids: 创建快照时,处于活跃事务的ID集合。
    • 人话:未提交事务的事务的集合。因为事务没提交,所以这里的数据是不可见的。并且活跃事务列表不会记录当前事务。
  • m_low_limit_id: 读取时不应看到任何trx id>=此值的事务。换句话说,这是“高水位线”
    • 人话:当前行数据的事务ID,大于等于m_low_limit_id,数据是不可见的。说白了,当前事务在创建Read View快照时,这个事务他还没开始呢,他的数据必然是不可见的。通过查看m_low_limit_id的赋值,可以得知他是还未被分配的事务的最小事务ID其实就是最大活跃事务 + 1。
  • m_up_limit_id: 读取应该看到所有严格小于(<)此值的trx id。换句话说,这是低水位线”
    • 人话: 他是活跃事务列表中的最小事务ID ,比这个事务ID还要小的值,他事务必然已经提交了,所以如果当前行数据的事务ID 小于 m_up_limit_id,我是可见的。如果活跃事务列表为空,m_up_limit_id = m_low_limit_id。

在RC的隔离级别下,每次执行select操作时,都会创建一个全新的Read View。

在RR的隔离级别下,只有第一次select操作时,会创建Read View,后续再查询,都基于第一次的Read View做可见性判断。

1.5 ReadView可见性判断的逻辑

在源码中,可以看到可见性判断的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// id参数,是你想查看的那行数据的事务ID
bool changes_visible(
trx_id_t id, const table_name_t& name) const MY_ATTRIBUTE((warn_unused_result)){
ut_ad(id > 0);
// m_up_limit_id 是活跃事务的最小id,如果当前行的事务ID,小于m_up_limit_id,说明这个事务必然已经提交了,这个数据是可见的。
// 如果当前行的事务ID和当前创建ReadView的事务ID相等,说明就是当前事务修改的数据,必然可见。
if (id < m_up_limit_id || id == m_creator_trx_id) {
return(true);
}

check_trx_id_sanity(id, name);
// 当前行的事务ID,大于了m_low_limit_id,必然不可见。创建Read View的时候,m_low_limit_id这个事务还没有呢。
if (id >= m_low_limit_id) {
return(false);
// 没有活跃事务,并且当前行数据的事务ID,还小于m_low_limit_id,那这个数据必然可见。
} else if (m_ids.empty()) {
return(true);
}

const ids_t::value_type* p = m_ids.data();
// 如果上述情况都不满足,无法判断可见还是不可见,此时需要拿着当前行的事务ID,以及活跃事务列表开始判断。
// 1、如果我发现当前行的事务ID,在活跃事务列表中。此时在Read View来说,这个事务没提交,不可见。
// 2、如果我发现当前行的事务ID,不在活跃事务列表中,说明创建Read View时候,你就提交了,可见。
return(!std::binary_search(p, p + m_ids.size(), id));
}

各种例子:

1、id < up_limit_id,直接可见。

2、RC隔离级别下,第二次查询会重新创建Read View,可以读取到刚刚提交事务的数据

3、RR隔离级别下,第二次查询不会重新构建Read View,新数据不可见。

4、当前行事务ID不在活跃事务列表中。


事务A 事务B
查询数据
修改了这个数据,并且提交事务
再次查询数据

事务A,能传到事务B提交的数据么?

面试被问到的话,先问是RC还是RR的隔离级别。~~~

二、MySQL主从同步的原理

MySQL的主从同步的过程中,要从几个维度聊。

  • 你的Master在做写操作时,会将写操作记录到bin log中。
  • 你的Slave从库会监听Master节点中的bin log的变化,如果有变化。
    • Slave需要主动的找Master节点要bin log中的数据,发起请求
    • Master会将bin log的内容发送给Slave。
  • Slave接收到bin log信息后,不会立即同步,会扔到relay log中缓冲一下。
  • Slave再从Relay log中将数据同步到Slave中。

三、MySQL主从同步必然有延迟,怎么办?

只能尽量的减少延迟,想解决的话,同步的成本很高。

如果必须要数据强一致,那就不能用主从的效果,自己搞一个众生平等的套路。

让写数据的时,同步写到多个MySQL节点中,都成功才成功!但是这样,写操作的效率必然大打折扣!(基本没有这么干的)

但是一般情况下,大多是尽可能的提升主从同步的效率。。。

N、bin log、undo log存哪了?(了解)

bin log、undo log存哪了?

存到磁盘是必然的。

其次,bin log是你指定存在哪个位置,他就存在哪个位置。

 undo log根据版本不同,存放的位置也各不一样。

在8.0之后,会有一个单独的undo log的目录,来存放undo log文件。

之前的版本也会存放到ibdata1文件中…………