InnoDB存储引擎MVCC实现原理

原文:https://liuzhengyang.github.io/2017/04/18/innodb-mvcc/

简单背景介绍

MySQL

MySQL是现在最流行的关系型数据库(RDB)的选择, 创建一个应用时,无论是用户数据还是订单数据,使用关系型数据库存储是最可靠稳定的选择,借助RDB提供的可靠性、事务等功能,为应用提供完善的支持。MySQL是开源软件,可以免费使用,MySQL在发展多年后越来越成熟,成为大部分公司的数据库首选。MySQL采用插件式的存储引擎架构,5.5版本后默认使用InnoDB存储引擎。

MySQL架构

MySQL从概念上可以分为四层,顶层是接入层,不同语言的客户端通过mysql的协议与mysql服务器进行连接通信,接入层进行权限验证、连接池管理、线程管理等。下面是mysql服务层,包括sql解析器、sql优化器、数据缓冲、缓存等。再下面是mysql中的存储引擎层,mysql中存储引擎是基于表的。最后是系统文件层,保存数据、索引、日志等。

MVCC

MVCC是Multi Version Concurrency Control的简称,代表多版本并发控制。为什么需要MVCC,还要从数据库事务的ACID特性说起。
相信很多朋友都了解ACID,它们分别代表了Atomicity(原子性), Consistency(一致性), Isolation(隔离性), Durability(持久性)。
原子性表示一个事务的操作结果要么全部执行要么全部不执行。
一致性表示事务总是从一个一致的状态转换到另一个一致的状态。
隔离性表示一个事务的修改结果在什么时间能够被其他事务看到,SQL1992规范中对隔离性定义了不同的隔离级别,
分为读未提交(READ UNCOMMITED),事务能够看到其他事务没有提及的修改,当另一个事务又回滚了修改后的情况又被称为脏读dirty read。
读已提交(READ COMMITTED),事务能够看到其他事务提交后的修改,这时会出现一个事务内两次读取数据可能因为其他事务提交的修改导致不一致的情况,称为不可重复读。 可重复读(REPEATABLE READ),在两次读取时读取到的数据的状态是一致的,和序列化(SERIALIZABLE)可重复读中可能出现第二次读读到第一次没有读到的数据,也就是被其他事务插入的数据,这种情况称为幻读phantom read, 序列化级别中不能出现幻读。
隔离级别依次增强,但是导致的问题是并发能力的减弱。
各种数据库厂商会对各个隔离级别进行实现。
和Java中的多线程问题相同,数据库通常使用锁来实现隔离性。
最原生的锁,锁住一个资源后会禁止其他任何线程访问同一个资源。但是很多应用的一个特点都是读多写少的场景,很多数据的读取次数远大于修改的次数,而读取数据间互相排斥显得不是很必要。所以就使用了一种读写锁的方法,读锁和读锁之间不互斥,而写锁和写锁、读锁都互斥。这样就很大提升了系统的并发能力。之后人们发现并发读还是不够,又提出了能不能让读写之间也不冲突的方法,就是读取数据时通过一种类似快照的方式将数据保存下来,这样读锁就和写锁不冲突了,不同的事务session会看到自己特定版本的数据。当然快照是一种概念模型,不同的数据库可能用不同的方式来实现这种功能。
之后的讨论默认均以REPEATABLE READ作为隔离级别。

InnoDB与MVCC

MySQL中的InnoDB存储引擎的特性有,默认隔离级别REPEATABLE READ, 行级锁,实现了MVCC, Consistent nonlocking read(默认读不加锁,一致性非锁定读), Insert Buffer, Adaptive Hash Index, DoubleWrite, Cluster Index。
上面列举了这么多,表示InnoDB有很多特性、很快。
InnoDB中通过UndoLog实现了数据的多版本,而并发控制通过锁来实现。
Undo Log除了实现MVCC外,还用于事务的回滚。

Redo log, bin log, Undo log

MySQL Innodb中存在多种日志,除了错误日志、查询日志外,还有很多和数据持久性、一致性有关的日志。
binlog,是mysql服务层产生的日志,常用来进行数据恢复、数据库复制,常见的mysql主从架构,就是采用slave同步master的binlog实现的, 另外通过解析binlog能够实现mysql到其他数据源(如ElasticSearch)的数据复制。
redo log记录了数据操作在物理层面的修改,mysql中使用了大量缓存,缓存存在于内存中,修改操作时会直接修改内存,而不是立刻修改磁盘,当内存和磁盘的数据不一致时,称内存中的数据为脏页(dirty page)。为了保证数据的安全性,事务进行中时会不断的产生redo log,在事务提交时进行一次flush操作,保存到磁盘中, redo log是按照顺序写入的,磁盘的顺序读写的速度远大于随机读写。当数据库或主机失效重启时,会根据redo log进行数据的恢复,如果redo log中有事务提交,则进行事务提交修改数据。这样实现了事务的原子性、一致性和持久性。

Undo Log: 除了记录redo log外,当进行数据修改时还会记录undo log,undo log用于数据的撤回操作,它记录了修改的反向操作,比如,插入对应删除,修改对应修改为原来的数据,通过undo log可以实现事务回滚,并且可以根据undo log回溯到某个特定的版本的数据,实现MVCC。

redo log 和binlog的一致性,为了防止写完binlog但是redo log的事务还没提交导致的不一致,innodb 使用了两阶段提交
大致执行序列为

1
2
3
InnoDB prepare (持有prepare_commit_mutex);
write/sync Binlog;
InnoDB commit (写入COMMIT标记后释放prepare_commit_mutex)。

MVCC实现

innodb中通过B+树作为索引的数据结构,并且主键所在的索引为ClusterIndex(聚簇索引), ClusterIndex中的叶子节点中保存了对应的数据内容。一个表只能有一个主键,所以只能有一个聚簇索引,如果表没有定义主键,则选择第一个非NULL唯一索引作为聚簇索引,如果还没有则生成一个隐藏id列作为聚簇索引。
除了Cluster Index外的索引是Secondary Index(辅助索引)。辅助索引中的叶子节点保存的是聚簇索引的叶子节点的值。
InnoDB行记录中除了刚才提到的rowid外,还有trx_id和db_roll_ptr, trx_id表示最近修改的事务的id,db_roll_ptr指向undo segment中的undo log。
新增一个事务时事务id会增加,trx_id能够表示事务开始的先后顺序。

Undo log分为Insert和Update两种,delete可以看做是一种特殊的update,即在记录上修改删除标记。
update undo log记录了数据之前的数据信息,通过这些信息可以还原到之前版本的状态。
当进行插入操作时,生成的Insert undo log在事务提交后即可删除,因为其他事务不需要这个undo log。
进行删除修改操作时,会生成对应的undo log,并将当前数据记录中的db_roll_ptr指向新的undo log

数据可见性判断

1
2
3
4
5
6
7
8
9
CREATE TABLE `testunique` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`uid` int(11) DEFAULT NULL,
`ukey` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `id_uid` (`uid`),
KEY `index_key` (`ukey`)
) ENGINE=InnoDB AUTO_INCREMENT=70 DEFAULT CHARSET=utf8;
隔离级别REPEATABLE READ
session1 session2
begin;  
  select * from testunique;
insert into testunique values(NULL, NULL, 1);  
  select * from testunique;
commit  
  select * from testunique;
  commit
  select * from testunique;

只有当session2 commit之后的查询才能查到session1插入的数据

事务可见性的处理过程:

RR级别下一个事务开始后第一个snapshot read的时候,会将当期活动的事务id记录下来,记录到read view中。RC级别则是每次snapshot read都会创建一个新的read view。
假设当前,read view中最大的事务id为tmax, 最小为tmin。则判断一个数据是否可见以及对应的版本的方法为。
如果该行中的trx_id, 赋值给tid, 如果tid和当前事务id相等或小于tmin,说明是事务内发生的或开启前的修改,则直接返回该版本数据; 如果
trx_id大于tmax, 则查看该版本的db_roll_ptr中的trx_id,赋值给tid并从头开始判断。如果tid小于tmax并且不在read view中,则返回,否则中回滚段中找出undo log的trx_id,赋值给tid从头判断。

所以可见性是,只有当第一次读之前提交的修改和自己的修改可见,其他的均不可见。

代码实现部分

在storage/innobase/include/read0types.h中

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// Friend declaration
class MVCC;
 
/** Read view lists the trx ids of those transactions for which a consistent
read should not see the modifications to the database. */
...
class ReadView {
...
private:
// Prevent copying
ids_t(const ids_t&);
ids_t& operator=(const ids_t&);
 
private:
/** Memory for the array */
value_type* m_ptr;
 
/** Number of active elements in the array */
ulint m_size;
 
/** Size of m_ptr in elements */
ulint m_reserved;
 
friend class ReadView;
};
public:
ReadView();
~ReadView();
/** Check whether transaction id is valid.
@param[in] id transaction id to check
@param[in] name table name */
static void check_trx_id_sanity(
trx_id_t id,
const table_name_t& name);
 
// 判断一个修改是否可见
/** 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));
}
 
 
private:
// Disable copying
ReadView(const ReadView&);
ReadView& operator=(const ReadView&);
 
private:
// 活动事务中的id的最大
/** The read should not see any transaction with trx id >= this
value. In other words, this is the "high water mark". */
trx_id_t m_low_limit_id;
 
// 活动事务id的最小值
/** The read should see all trx ids which are strictly
smaller (<) than this value. In other words, this is the
low water mark". */
//
trx_id_t m_up_limit_id;
 
/** trx id of creating transaction, set to TRX_ID_MAX for free
views. */
trx_id_t m_creator_trx_id;
 
/** Set of RW transactions that was active when this snapshot
was taken */
ids_t m_ids;
 
/** The view does not need to see the undo logs for transactions
whose transaction number is strictly smaller (<) than this value:
they can be removed in purge if not needed by other views */
trx_id_t m_low_limit_no;
 
/** AC-NL-RO transaction view that has been "closed". */
bool m_closed;
 
typedef UT_LIST_NODE_T(ReadView) node_t;
 
/** List of read views in trx_sys */
byte pad1[64 - sizeof(node_t)];
node_t m_view_list;
};

Undo log删除

undo log在没有活动事务依赖(用于consistent read或回滚)便可以清楚,innodb 中存在后台purge 线程进行后台轮询删除undo log。

Current Read snapshot read

REPEATABLE READ隔离级别下普通的读操作即select都不加锁,使用MVCC进行一致性读取,这种读取又叫做snapshot read。
而update, insert, delete, select … for update, select … lock in share mode都会进行加锁,并且读取的是当前版本,也就是READ COMMITTED读的效果。innodb-locks-set.html中对各种操作会进行的锁操作有详细的说明,这里我简单总结下。
InnoDB中加锁的方法是锁住对应的索引,一个操作进行前会选择一个索引进行扫描,扫描到一行后加上对应的锁然后返回给上层然后继续扫描。InnoDB支持行级锁(record lock),上述需要加锁的操作中,除了select … lock in share mode 是加shared lock(共享锁或读锁)外其他操作都加的是exclusive lock(即排他锁或写锁)。在加行级锁前,会对表加一个intention lock,即意向锁,意向所是表级锁,不会和行级锁冲突,主要用途是表明一个要加行级锁或正在加锁的操作。
另外InnoDB种除了record lock外还有一种gap lock,即锁住两个记录间的间隙,防止其他事务插入数据,用于防止幻读。当索引是主键索引或唯一索引时,不需要加gap lock。当索引不是唯一索引时,需要对索引数据和索引前的gap加锁,这种方式叫做next-key locking。
另外在插入数据时,还需要提前最插入行的前面部分加上insert intention lock, 即插入意向锁,插入意向锁之间不会冲突,会和gap锁冲突导致等待。当插入时遇到duplicated key错误时,会在要插入的行上加上share lock。

Mac下Debug mysql

1
2
3
4
5
git clone https://github.com/mysql/mysql-server
cd mysql-server
mkdir bld
cd bld
cmake .. -DWITH_DEBUG=1 -DDOWNLOAD_BOOST=1 -DWITH_BOOST=~/boost

Clion中debug
下载最新版本的Clion,
import project 打开mysql-server文件夹
配置cmake 参数 CMakeOptions为

1
-DDOWNLOAD_BOOST=1 -DWITH_BOOST=~/boost


之后Clion自动进行cmake
结束后可以看到有很多可以debug的程序

选择mysqld,即为mysql服务器程序
入口在sql/main.cc
加上断点, 点击debug后,经过一段时间build,即可进行debug了

参考

(转)InnoDB存储引擎MVCC实现原理的更多相关文章

  1. MySQL数据库InnoDB存储引擎多版本控制(MVCC)实现原理分析

    文/何登成 导读:   来自网易研究院的MySQL内核技术研究人何登成,把MySQL数据库InnoDB存储引擎的多版本控制(简称:MVCC)实现原理,做了深入的研究与详细的文字图表分析,方便大家理解I ...

  2. 浅析Mysql InnoDB存储引擎事务原理

    浅析Mysql InnoDB存储引擎事务原理 大神:http://blog.csdn.net/tangkund3218/article/details/47904021

  3. MySQL中InnoDB存储引擎的实现和运行原理

    InnoDB 存储引擎作为我们最常用到的存储引擎之一,充分熟悉它的的实现和运行原理,有助于我们更好地创建和维护数据库表. InnoDB 体系架构 InnoDB 主要包括了: 内存池.后台线程以及存储文 ...

  4. MySQL InnoDB 存储引擎原理浅析

    注:本文主要基于MySQL 5.6以后版本编写,多数知识来着书籍<MySQL技术内幕++InnoDB存储引擎>,本文章仅记录个人认为比较重要的部分,有兴趣的可以花点时间读原书. 一.MyS ...

  5. [MySQL Reference Manual]14 InnoDB存储引擎

    14 InnoDB存储引擎 14 InnoDB存储引擎 14.1 InnoDB说明 14.1.1 InnoDB作为默认存储引擎 14.1.1.1 存储引擎的趋势 14.1.1.2 InnoDB变成默认 ...

  6. MySQL InnoDB存储引擎undo redo解析

    本文介绍MySQL数据库InnoDB存储引擎重做日志漫游 00 – Undo Log Undo Log 为了实现事务原子,在MySQL数据库InnoDB存储引擎,还使用Undo Log(简称:MVCC ...

  7. InnoDB存储引擎概览

    InnoDB存储引擎概览   InnoDB存储引擎以其平衡了高可靠性和高性能性而闻名遐迩,在MySQL 8.0版本中,InnoDB存储引擎是默认的存储引擎.(历史追溯从MySQL 5.5.5版本开始, ...

  8. InnoDB存储引擎介绍-(2)redo和undo学习

    01 – Undo LogUndo Log 是为了实现事务的原子性,在MySQL数据库InnoDB存储引擎中,还用Undo Log来实现多版本并发控制(简称:MVCC). - 事务的原子性(Atomi ...

  9. 《MySQL技术内幕:InnoDB存储引擎(第2版)》书摘

    MySQL技术内幕:InnoDB存储引擎(第2版) 姜承尧 第1章 MySQL体系结构和存储引擎 >> 在上述例子中使用了mysqld_safe命令来启动数据库,当然启动MySQL实例的方 ...

随机推荐

  1. 看图说说JVM GC收集算法

  2. java中null转换成其它类型

    对null进行强转会不会抛错.测试结果是,如果把null强转给对象,是不会抛异常的,因为本身对象是可以为null的.但是如果是基本类型,比如 int i = (Integer)obj的强转,其实内部会 ...

  3. 9、Dockerfile语法

      在Dockerfile中定义了很多关键字,通过关键字来完成Dockerfile的编写.   Dockerfile官方文档 9.1 FROM   在Dockerfile中FROM主要是指定这个Doc ...

  4. 如何将图片嵌入到Html中

    将图片内嵌入到Html中,最好的方法就是用Base64 string.例如:<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUg ...

  5. 提问的智慧——其实你真的不会提问!(转)

    在黑客世界里,当提出一个技术问题时,你能得到怎样的回答?这取决于挖出答案的难度,同样取决于你提问的方法.本指南旨在帮助你提高发问技巧,以获取你最想要的答案.       首先你必须明白,黑客们只偏爱艰 ...

  6. [Elixir006]CSV(Comma-separated values)处理

    1. CSV文件格式是什么 CSV有时也称为字符分隔值,因为分隔字符也可以不是逗号),其文件以纯文本形式存储表格数据(数字和文本).纯文本意味着该文件是一个字符序列,不含必须像二进制数字那样被解读的数 ...

  7. C# 时间戳与DateTime间的互相转换

    //DateTime转换为时间戳public long GetTimeSpan(DateTime time) { DateTime startTime = TimeZone.CurrentTimeZo ...

  8. cesium编程中级(一)添加示例到Sandcastle

    cesium编程中级(一)添加示例到Sandcastle 添加示例到Sandcastle在cesium编程入门(七)3D Tiles,模型旋转中提到过,这里是一份完整的说明 创建例子 开启node服务 ...

  9. @componentscan注解的用法和作用

    刚刚开始学习spring boot,在application中忘记加上@componentscan,倒置web请求一直没有都是404,@componentscan的作用如下: @ComponentSc ...

  10. Android Performance 性能提升

    1. 经常变动的字符串要用 StringBuilder,然后每次变动用 append 方法.而不应该每次创建新的 String. 2. 使用 static final 变量. 3. It's reas ...