多版本并发控制 MVCC 实现可重复读
参考
高性能 MySQL 第3版 1.4 多版本并发控制
MVCC 是通过保存数据在某个时间点的快照实现的。不同存储引擎的 MVCC 的实现不同,典型的有乐观并发控制和悲观并发控制。
InnoDB 的 MVCC 实现
名词
系统版本号
事务版本号
记录创建时间
记录删除时间
InnoDB 的 MVCC 是通过在每行记录后面保存 2 个隐藏列实现的。一个保存行的创建时间,一个保存行的删除时间。当然存储的不是时间值,而是系统版本号。每开始一个新的事务,系统版本号都会自动递增。事务版本号是事务开始时刻的系统版本号。
可重复读级别下,MVCC 的执行过程
事务版本号是事务开始时刻的系统版本号
每开始一个新的事务,系统版本号都会自动递增
select
返回满足下面条件的记录
创建时间 <= 当前事务版本号 && ( 删除时间 == null || 删除时间 > 当前事务版本号 )
insert
插入记录的创建时间 = 当前系统版本号
新插入的记录的创建时间设置为当前系统版本号,删除时间为 null 。
delete
被删除记录的删除时间 = 当前系统版本号
被删除记录的删除时间被设置为当前系统版本号,逻辑删除,并不会真的删除数据。
update
被更新记录的原记录的删除时间 = 当前系统版本号
新插入记录的创建时间 = 当前系统版本号
将被更新的原记录的删除时间设置为当前系统版本号,即逻辑删除原记录。插入一条新记录,其创建时间为当前系统版本号。
MVCC 只在 read committed 和 repeatable read 2 个隔离级别工作,其他隔离级别并没有使用这种机制。read uncommitted 总是读取最新的数据行,而不是符合当前事务版本的数据行,seriablizable 会对所有读取的行都加锁。
MVCC 源码解析
http://www.ningoo.net/html/tag/mysql
http://blog.sina.com.cn/s/blog_4673e603010111ty.html
https://blog.csdn.net/joy0921/article/details/80128857
https://blog.csdn.net/u012919352/article/details/87984786
https://dev.mysql.com/doc/refman/5.5/en/innodb-consistent-read.html
https://blog.csdn.net/joy0921/article/details/80128857
记录隐藏的3个字段
row_id db_trx_id db_roll_pt 记录 ID 事务 ID 回滚指针
-
首先InnoDB每一行数据还有一个DB_ROLL_PT的回滚指针,用于指向该行修改前的上一个历史版本
-
当插入的是一条新数据时,记录上对应的回滚段指针为NULL。
-
如果当前记录有主键,则不会产生 row_id 记录 ID
-
read view undo log
通过read view判断行记录是否可见
具体的判断流程如下:
RR隔离级别下,在每个事务开始的时候,会将当前系统中的所有的活跃事务拷贝到一个列表中(read view)
RC隔离级别下,在每个语句开始的时候,会将当前系统中的所有的活跃事务拷贝到一个列表中(read view)
- 《唐成-2016PG大会-数据库多版本实现内幕.pdf》
https://myslide.cn/slides/3542
MySQL · 源码分析 · InnoDB的read view,回滚段和purge过程简介 https://yq.aliyun.com/articles/560506
mysql> show engine innodb statusG;
------------
TRANSACTIONS
------------
Trx id counter AC16
Purge done for trx's n:o < AC14 undo n:o < 0
History list length 1079
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION AC15, not started
MySQL thread id 4, OS thread handle 0x43c, query id 133 localhost 127.0.0.1 root
show engine innodb status
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_TRXG;
https://www.cnblogs.com/itcomputer/articles/5084611.html
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_TRXG;
*************************** 1. row ***************************
trx_id: AC16
trx_state: RUNNING
trx_started: 2019-08-03 11:50:27
trx_requested_lock_id: NULL
trx_wait_started: NULL
trx_weight: 0
trx_mysql_thread_id: 10
trx_query: NULL
trx_operation_state: NULL
trx_tables_in_use: 0
trx_tables_locked: 0
trx_lock_structs: 0
trx_lock_memory_bytes: 376
trx_rows_locked: 0
trx_rows_modified: 0
trx_concurrency_tickets: 0
trx_isolation_level: READ COMMITTED
trx_unique_checks: 1
trx_foreign_key_checks: 1
trx_last_foreign_key_error: NULL
trx_adaptive_hash_latched: 0
trx_adaptive_hash_timeout: 10000
1 row in set (0.00 sec)
我开启了2个事务 AC18 AC17,事务计数器 = AC19
mysql> show engine innodb statusG;
------------
TRANSACTIONS
------------
Trx id counter AC19
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_TRXG;
*************************** 1. row ***************************
trx_id: AC18
trx_state: RUNNING
trx_started: 2019-08-03 11:56:40
*************************** 2. row ***************************
trx_id: AC17
trx_state: RUNNING
trx_started: 2019-08-03 11:55:08
mysql> SELECT tx.trx_id
-> FROM information_schema.innodb_trx tx
-> WHERE tx.trx_mysql_thread_id = connection_id();
+--------+
| trx_id |
+--------+
| AC17 |
+--------+
1 row in set (0.07 sec)
mysql> SELECT tx.trx_id
-> FROM information_schema.innodb_trx tx
-> WHERE tx.trx_mysql_thread_id = connection_id();
+--------+
| trx_id |
+--------+
| AC18 |
+--------+
1 row in set (0.00 sec)
mysql> SELECT tx.trx_id
-> FROM information_schema.innodb_trx tx
-> WHERE tx.trx_mysql_thread_id = connection_id();
Empty set (0.00 sec)
RR隔离级别(除了Gap锁之外)和RC隔离级别的差别是创建snapshot时机不同。 RR隔离级别是在事务开始时刻,确切地说是第一个读操作创建read view的;RC隔离级别是在语句开始时刻创建read view的。
创建/关闭 read view 需要持有 trx_sys->mutex ,会降低系统性能,5.7版本对此进行优化,在事务提交时 session 会 cache 只读事务的 read view。
Read view中保存的trx_sys状态主要包括
low_limit_id:high water mark,大于等于view->low_limit_id的事务对于view都是不可见的
up_limit_id:low water mark,小于view->up_limit_id的事务对于view一定是可见的
low_limit_no:trx_no小于view->low_limit_no的undo log对于view是可以purge的
rw_trx_ids:读写事务数组
Read view创建之后,读数据时比较记录最后更新的trx_id和view的 high/low water mark和读写事务数组即可判断可见性。
如前所述,如果记录最新数据是当前事务trx的更新结果,对应当前read view一定是可见的。
除此之外可以通过high/low water mark快速判断:
trx_id < view->up_limit_id的记录对于当前read view是一定可见的;
trx_id >= view->low_limit_id的记录对于当前read view是一定不可见的;
如果trx_id落在[up_limit_id, low_limit_id),需要在活跃读写事务数组查找trx_id是否存在,如果存在,记录对于当前read view是不可见的。
storage/innobase/read/read0read.c
http://blog.sina.com.cn/s/blog_4673e603010111ty.html
storage/innobase/include/read0read.ic
view->n_trx_ids 数量
view->up_limit_id
view->low_limit_id
/*********************************************************************//**
Checks if a read view sees the specified transaction.
@return TRUE if sees */
UNIV_INLINE
ibool
read_view_sees_trx_id(
/*==================*/
const read_view_t* view, /*!< in: read view */
trx_id_t trx_id) /*!< in: trx id */
{
ulint n_ids;
ulint i;
if (trx_id < view->up_limit_id) {
return(TRUE);
}
if (trx_id >= view->low_limit_id) {
return(FALSE);
}
/* We go through the trx ids in the array smallest first: this order
may save CPU time, because if there was a very long running
transaction in the trx id array, its trx id is looked at first, and
the first two comparisons may well decide the visibility of trx_id. */
n_ids = view->n_trx_ids;
for (i = 0; i < n_ids; i++) {
trx_id_t view_trx_id
= read_view_get_nth_trx_id(view, n_ids - i - 1);
if (trx_id <= view_trx_id) {
return(trx_id != view_trx_id);
}
}
return(TRUE);
}
storage/innobase/include/read0read.h
struct read_view_struct{
ulint type; /*!< VIEW_NORMAL, VIEW_HIGH_GRANULARITY */
undo_no_t undo_no;/*!< 0 or if type is
VIEW_HIGH_GRANULARITY
transaction undo_no when this high-granularity
consistent read view was created */
trx_id_t low_limit_no;
/*!< 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 low_limit_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 up_limit_id;
/*!< The read should see all trx ids which
are strictly smaller (<) than this value.
In other words,
this is the "low water mark". */
ulint n_trx_ids;
/*!< Number of cells in the trx_ids array */
trx_id_t* trx_ids;/*!< Additional trx ids which the read should
not see: typically, these are the active
transactions at the time when the read is
serialized, except the reading transaction
itself; the trx ids in this array are in a
descending order. These trx_ids should be
between the "low" and "high" water marks,
that is, up_limit_id and low_limit_id. */
trx_id_t creator_trx_id;
/*!< trx id of creating transaction, or
0 used in purge */
UT_LIST_NODE_T(read_view_t) view_list;
/*!< List of read views in trx_sys */
};
trx_id_t trx_id = row_get_rec_trx_id(rec, index, offsets); //获取记录上的TRX_ID这里需要解释下,我们一个查询可能满足的记录数有多个。那我们每读取一条记录的时候就要根据这条记录上的TRX_ID判断这条记录是否可见
return(view->changes_visible(trx_id, index->table->name)); //判断记录可见性
---------------------
作者:仲培艺
来源:CSDN
原文:https://blog.csdn.net/joy0921/article/details/80128857
版权声明:本文为博主原创文章,转载请附上博文链接!
对于不可见的记录都是通过row_vers_build_for_consistent_read函数查询UNDO构建老版本记录,直到记录可见。
这里需要说明一点 不同的事务隔离级别,可见性的实现也不一样:
READ-COMMITTED
事务内的每个查询语句都会重新创建Read View,这样就会产生不可重复读现象发生
REPEATABLE-READ
事务内开始时创建Read View , 在事务结束这段时间内 每一次查询都不会重新重建Read View , 从而实现了可重复读。
trx_sys->trx_list
https://blog.csdn.net/longxibendi/article/details/42012629
Read view创建之后,读数据时比较记录最后更新的trx_id和view的high/low water mark和读写事务数组即可判断可见性。
https://cloud.tencent.com/developer/ask/210171
https://github.com/jeremycole/innodb_ruby/wiki
[root@instance-fjii60o3 ~]# gem install innodb_ruby
-bash: gem: command not found
[root@instance-fjii60o3 develop]# wget https://rubygems.org/rubygems/rubygems-3.0.4.tgz
-rw-r--r-- 1 root root 883664 Jun 14 11:35 rubygems-3.0.4.tgz
Linux 安装 gem https://www.csdn.net/gather_2b/MtjaUgysNDYzNS1ibG9n.html
[root@instance-fjii60o3 rubygems-3.0.4]# bin/gem
/usr/bin/env: ruby: No such file or directory
Linux 安装 ruby
https://www.cnblogs.com/xuliangxing/p/7132656.html?utm_source=itdadao&utm_medium=referral
[root@instance-fjii60o3 rubygems-3.0.4]# yum list install | grep ruby
Error: No matching Packages to list
[root@instance-fjii60o3 rubygems-3.0.4]# yum list installed | grep ruby
ruby.x86_64 2.0.0.648-35.el7_6 @updates
ruby-irb.noarch 2.0.0.648-35.el7_6 @updates
ruby-libs.x86_64 2.0.0.648-35.el7_6 @updates
rubygem-bigdecimal.x86_64 1.2.0-35.el7_6 @updates
rubygem-io-console.x86_64 0.4.2-35.el7_6 @updates
Linux 安装 git
[root@instance-fjii60o3 ~]# yum install git
[root@instance-fjii60o3 ~]# git clone https://github.com/jeremycole/innodb_ruby.git
[root@instance-fjii60o3 rubygems-3.0.4]# gem install --user-install innodb_ruby
WARNING: You don't have /root/.gem/ruby/bin in your PATH,
gem executables will not run.
Ruby学习之RubyGems(gem)包管理器的安装和使用
https://blog.csdn.net/luyaran/article/details/85698936
[root@instance-fjii60o3 rubygems-3.0.4]# yum list installed | grep ruby
Existing lock /var/run/yum.pid: another copy is running as pid 11250.
Another app is currently holding the yum lock; waiting for it to exit...
The other application is: yum
Memory : 90 M RSS (408 MB VSZ)
Started: Sat Aug 3 14:39:05 2019 - 16:44 ago
State : Sleeping, pid: 11250
系统中活跃事务 ID
MVCC的可见性 https://blog.csdn.net/taozhi20084525/article/details/19501075
[root@instance-fjii60o3 develop]# yum localinstall mysql-community-server-8.0.13-1.el7.x86_64.rpm
Loaded plugins: langpacks, versionlock
Existing lock /var/run/yum.pid: another copy is running as pid 11250.
Another app is currently holding the yum lock; waiting for it to exit...
The other application is: yum
Memory : 89 M RSS (408 MB VSZ)
Started: Sat Aug 3 14:39:05 2019 - 1:15:13 ago
State : Traced/Stopped, pid: 11250
[root@instance-fjii60o3 develop]# tar -xvf mysql-8.0.13-1.el7.x86_64.rpm-bundle.tar -C mysql8.0
mysql-community-client-8.0.13-1.el7.x86_64.rpm
mysql-community-embedded-compat-8.0.13-1.el7.x86_64.rpm
mysql-community-libs-8.0.13-1.el7.x86_64.rpm
mysql-community-server-8.0.13-1.el7.x86_64.rpm
tar: Unexpected EOF in archive
tar: rmtlseek not stopped at a record boundary
tar: Error is not recoverable: exiting now
Ruby 工具分析 innodb 记录结构
https://cloud.tencent.com/developer/ask/210171
https://github.com/jeremycole/innodb_ruby/wiki
-
安装 ruby
-
gem install innodb_ruby
innodb 格式解析工具
https://rubygems.org/gems/innodb_ruby/versions
https://www.cnblogs.com/zengkefu/p/5678356.html
Linux 查看文件大小
[root@instance-fjii60o3 ~]# du -sh MySQL-5.5.62-1.el7.x86_64.rpm-bundle.tar
2.1M MySQL-5.5.62-1.el7.x86_64.rpm-bundle.tar
[root@instance-fjii60o3 ~]# du -sh develop/
1.1G develop/
五分钟了解Mysql的行级锁——《深究Mysql锁》 https://blog.csdn.net/zcl_love_wx/article/details/81983267
Mysql有三种级别的锁定:表级锁定、页级锁定、行级锁定
-
每次锁定的是一行数据的锁机制就是行级别锁定(row-level)。行级锁定不是MySQL自己实现的锁定方式,而是由其他存储引擎自己所实现的
-
使用行级锁定的主要有InnoDB存储引擎,以及MySQL的分布式存储引擎NDBCluster
InnoDB的行级锁定同样分为两种类型:共享锁和排他锁,而在锁定机制的实现过程中为了让行级锁定和表级锁定共存,InnoDB也同样使用了意向锁(表级锁定)的概念,也就有了意向共享锁和意向排他锁这两种。