聊聊 mysql 的 MVCC
很久以前,有位面试官问到,你知道 mysql 的事务隔离级别吗,“额 O__O …,不太清楚”,完了之后我就去网上找相关的文章,找到了这篇MySQL 四种事务隔离级的说明, 文章写得特别好,看了这个就懂了各个事务隔离级别都是啥,不过看了这个之后多思考一下的话还是会发现问题,这么神奇的事务隔离级别是怎么实现的呢
其中 innodb 的事务隔离用到了标题里说到的 mvcc,Multiversion concurrency control, 直译过来就是多版本并发控制,先不讲这个究竟是个啥,考虑下如果纯猜测,这个事务隔离级别应该会是怎么样实现呢,愚钝的我想了下,可以在事务开始的时候拷贝一个表,这个可以支持 RR 级别,RC 级别就不支持了,而且要是个非常大的表,想想就不可行
腆着脸说虽然这个不可行,但是思路是对的,具体实行起来需要做一系列(肥肠多)的改动,首先根据我的理解,其实这个拷贝一个表是变成拷贝一条记录,但是如果有多个事务,那就得拷贝多次,这个问题其实可以借助版本管理系统来解释,在用版本管理系统,git 之类的之前,很原始的可能是开发完一个功能后,就打个压缩包用时间等信息命名,然后如果后面要找回这个就直接用这个压缩包的就行了,后来有了 svn,git 中心式和分布式的版本管理系统,它的一个特点是粒度可以控制到文件和代码行级别,对应的我们的 mysql 事务是不是也可以从一开始预想的表级别细化到行的级别,可能之前很多人都了解过,数据库的一行记录除了我们用户自定义的字段,还有一些额外的字段,去源码data0type.h里捞一下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17/* Precise data types for system columns and the length of those columns;
NOTE: the values must run from 0 up in the order given! All codes must
be less than 256 */
/** Transaction id: 6 bytes */
constexpr size_t DATA_TRX_ID = 1;
/** Transaction ID type size in bytes. */
constexpr size_t DATA_TRX_ID_LEN = 6;
/** Rollback data pointer: 7 bytes */
constexpr size_t DATA_ROLL_PTR = 2;
/** Rollback data pointer type size in bytes. */
constexpr size_t DATA_ROLL_PTR_LEN = 7;
一个是 DATA_ROW_ID
,这个是在数据没指定主键的时候会生成一个隐藏的,如果用户有指定主键就是主键了
一个是 DATA_TRX_ID
,这个表示这条记录的事务 ID
还有一个是 DATA_ROLL_PTR
指向回滚段的指针
指向的回滚段其实就是我们常说的 undo log,这里面的具体结构就是个链表,在 mvcc 里会使用到这个,还有就是这个 DATA_TRX_ID
,每条记录都记录了这个事务 ID,表示的是这条记录的当前值是被哪个事务修改的,下面就扯回事务了,我们知道 Read Uncommitted
, 其实用不到隔离,直接读取当前值就好了,到了 Read Committed
级别,我们要让事务读取到提交过的值,mysql 使用了一个叫 read view
的玩意,它里面有这些值是我们需要注意的,
m_low_limit_id
, 这个是 read view 创建时最大的活跃事务 id
m_up_limit_id
, 这个是 read view 创建时最小的活跃事务 id
m_ids
, 这个是 read view 创建时所有的活跃事务 id 数组
m_creator_trx_id 这个是当前记录的创建事务 id
判断事务的可见性主要的逻辑是这样,
- 当记录的事务
id
小于最小活跃事务 id,说明是可见的, - 如果记录的事务
id
等于当前事务 id,说明是自己的更改,可见 - 如果记录的事务
id
大于最大的活跃事务id
, 不可见 - 如果记录的事务
id
介于m_low_limit_id
和m_up_limit_id
之间,则要判断它是否在m_ids
中,如果在,不可见,如果不在,表示已提交,可见
具体的代码捞一下看看剩下来一点是啥呢,就是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/** Check whether the changes by id are visible.
@param[in] id transaction id to check against the view
@param[in] name table name
@return whether the view sees the modifications of id. */
bool changes_visible(trx_id_t id, const table_name_t &name) const
MY_ATTRIBUTE((warn_unused_result)) {
ut_ad(id > 0);
if (id < m_up_limit_id || id == m_creator_trx_id) {
return (true);
}
check_trx_id_sanity(id, name);
if (id >= m_low_limit_id) {
return (false);
} else if (m_ids.empty()) {
return (true);
}
const ids_t::value_type *p = m_ids.data();
return (!std::binary_search(p, p + m_ids.size(), id));
}Read Committed
和Repeated Read
也不一样,那前面说的read view
都能支持吗,又是怎么支持呢,假如这个read view
是在事务一开始就创建,那好像能支持的只是 RR 事务隔离级别,其实呢,这是通过创建read view
的时机,对于 RR 级别,就是在事务的第一个select
语句是创建,对于 RC 级别,是在每个select
语句执行前都是创建一次,那样就可以保证能读到所有已提交的数据