mysql事务原理及MVCC

本文涉及的产品
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
简介:

mysql事务原理及MVCC
事务是数据库最为重要的机制之一,凡是使用过数据库的人,都了解数据库的事务机制,也对ACID四个
基本特性如数家珍。但是聊起事务或者ACID的底层实现原理,往往言之不详,不明所以。在MySQL中
的事务是由存储引擎实现的,而且支持事务的存储引擎不多,我们主要讲解InnoDB存储引擎中的事
务。所以,今天我们就一起来分析和探讨InnoDB的事务机制,希望能建立起对事务底层实现原理的具
体了解。

事务的特性

原子性:事务最小工作单元,事务开始要不全部成功,要不全部失败.
一致性:事务的开始和结束后,数据库的完整性不会被破坏
隔离性:不同事务之间互不影响,四种隔离级别为RU(读未提交)、RC(读已提
交)、RR(可重复读)、SERIALIZABLE (串行化)。
持久性:事务提交后,对数据的修改是永久性的,即使系统故障也不会丢失 。
隔离级别
有一张表,结构如下:

未提交读(RU)
一个事务读取到另一个事务尚未提交的数据,称之为脏读
发生时间编号 session A session B
1 begin;
2
begin;
3
update t set c="关羽" where id = 1;
4 select * from t where id = 1;

时间编号为4时,AB两个session均未提交事务,select语句读取到的值为关羽,读取到了B尚未提交的事务,此为脏读,这种隔离级别是最不安全的一种.

已提交读(RC)
一个事务读取到另一个事务已提交的数据,导致对同一条记录读取两次以上的结果不一致,称之为不可重复读
发生时间编号 session A session B
1 begin;
2
begin;
3
update t set c="关羽" where id = 1;
4 select * from t where id = 1;
5
commit;
6 select * from t where id = 1;
时间编号为4时,B尚未提交,此时读取到的数据依然是刘备,时间编号为5,B事务提交,时间编号为6时再次读取到的数据变成了关羽.这种情况是可以被理解的,因为B事务已经提交了.

可重复读(RR)
一个事务读取到另一个事务已经提交的delete或者insert数据,导致对同一张表读取两次以上结果不一致,称之为幻读
幻读可以通过串行化或者间隙锁来解决
发生时间编号 session A session B
1 begin;
2
begin;
3
update t set c="关羽" where id = 1;
4 select * from t where id = 1;
5
commit;
6 select * from t where id = 1;
时间编号为4时,B尚未提交,此时读取到的数据依然是刘备,时间编号为5,B事务提交,时间编号为6时再次读取到的数据依然是刘备.同一个事务中读取到的数据永远是一致的.

串行化
简单来说就是加锁,这种隔离级别是最安全的,可以解决其他隔离级别所产生的问题,但是效率较低.
发生时间编号 session A session B
1 begin;
2
begin;
3
update t set c="关羽" where id = 1;
4 select * from t where id = 1;
5
commit;
6 select * from t where id = 1;
时间编号为4时,B尚未提交,此时读取时,将会被阻塞,处于等待中直到B事务提交释放锁,时间编号为5,B事务提交释放锁,时间编号为6时再次读取到的数据是关羽.

丢失更新,两个事务同时对一条数据进行修改时,会存在丢失更新问题.
时间 取款事务A 取款事务B
1 开始事务
2
开始事务
3 查询余额为1000元
4
查询余额为1000元
5
汇入100元,余额变为1100
6
提交事务
7 取出100元,余额变为900元
8 回滚事务
9 余额恢复为1000元,丢失更新
mysql的默认隔离级别为RR

数据库的事务并发问题需要使用并发控制机制去解决,数据库的并发控制机制有很多,最为常见
的就是锁机制。锁机制一般会给竞争资源加锁,阻塞读或者写操作来解决事务之间的竞争条件,
最终保证事务的可串行化。

而MVCC则引入了另外一种并发控制,它让读写操作互不阻塞,每一个写操作都会创建一个新版
本的数据,读操作会从有限多个版本的数据中挑选一个最合适的结果直接返回,由此解决了事务
的竞争条件。

MVCC
mvcc也是多版本并发控制,mysql中引入了这种并发机制.我们接下来就聊聊mvcc

版本链
回滚段/undo log
insert undo log
是在 insert 操作中产生的 undo log。
因为 insert 操作的记录只对事务本身可见,对于其它事务此记录是不可见的,所以 insert undo
log 可以在事务提交后直接删除而不需要进行 purge 操作。
update undo log
是 update 或 delete 操作中产生的 undo log
因为会对已经存在的记录产生影响,为了提供 MVCC机制,因此 update undo log 不能在事务提交时就进行删除,而是将事务提交时放到入 history list 上,等待 purge 线程进行最后的删除操作
为了保证事务并发操作时,在写各自的undo log时不产生冲突,InnoDB采用回滚段的方式来维护undo
log的并发写入和持久化。回滚段实际上是一种 Undo 文件组织方式。

InnoDB行记录有三个隐藏字段:分别对应该行的rowid、事务号db_trx_id和回滚指针db_roll_ptr,其
中db_trx_id表示最近修改的事务的id,db_roll_ptr指向回滚段中的undo log。

对于使用 InnoDB 存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列( row_id 并不是
必要的,我们创建的表中有主键或者非NULL唯一键时都不会包含 row_id 列):

trx_id :每次对某条聚簇索引记录进行改动时,都会把对应的事务id赋值给 trx_id 隐藏列。
roll_pointer :每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo日志 中,然
后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
我们有一张表
create table user(

id int,
name varchar,
primary key (id)

)

insert into user values(1,'张三');
我们此时插入这条数据,假设事务id为80.

ps:咳咳~~理解意思就好,捂脸.jpg

每次对记录进行改动,都会记录一条 undo日志 ,每条 undo日志 也都有一个 roll_pointer 属性
( INSERT 操作对应的 undo日志 没有该属性,因为该记录并没有更早的版本),可以将这些 undo日志
都连起来,串成一个链表,所以现在的情况就像下图一样:

对该记录每次更新后,都会将旧值放到一条 undo日志 中,就算是该记录的一个旧版本,随着更新次数
的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,我们把这个链表称之为 版本链 ,版本
链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务id,这个信息很
重要,我们稍后就会用到。

如下图所示(初始状态):

当事务2使用UPDATE语句修改该行数据时,会首先使用排他锁锁定改行,将该行当前的值复制到undo
log中,然后再真正地修改当前行的值,最后填写事务ID,使用回滚指针指向undo log中修改前的行。
如下图所示(第一次修改):

当事务3进行修改与事务2的处理过程类似,如下图所示(第二次修改):

REPEATABLE READ隔离级别下事务开始后使用MVCC机制进行读取时,会将当时活动的事务id记录下
来,记录到Read View中。READ COMMITTED隔离级别下则是每次读取时都创建一个新的Read View。

ReadView
对于使用 READ UNCOMMITTED 隔离级别的事务来说,直接读取记录的最新版本就好了,对于使用
SERIALIZABLE 隔离级别的事务来说,使用加锁的方式来访问记录。对于使用 READ COMMITTED 和
REPEATABLE READ 隔离级别的事务来说,就需要用到我们上边所说的 版本链 了,核心问题就是:需要
判断一下版本链中的哪个版本是当前事务可见的。所以设计 InnoDB 的大叔提出了一个 ReadView 的概
念,这个 ReadView 中主要包含当前系统中还有哪些活跃的读写事务,把它们的事务id放到一个列表
中,我们把这个列表命名为为 m_ids 。这样在访问某条记录时,只需要按照下边的步骤判断记录的某个
版本是否可见:

如果被访问版本的 trx_id 属性值小于 m_ids 列表中最小的事务id,表明生成该版本的事务在生成
ReadView 前已经提交,所以该版本可以被当前事务访问。
如果被访问版本的 trx_id 属性值大于 m_ids 列表中最大的事务id,表明生成该版本的事务在生成
ReadView 后才生成,所以该版本不可以被当前事务访问。
如果被访问版本的 trx_id 属性值在 m_ids 列表中最大的事务id和最小事务id之间,那就需要判断
一下 trx_id 属性值是不是在 m_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还
是活跃的,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提
交,该版本可以被访问。
如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的
步骤判断可见性,依此类推,直到版本链中的最后一个版本,如果最后一个版本也不可见的话,那么就
意味着该条记录对该事务不可见,查询结果就不包含该记录。

在 MySQL 中, READ COMMITTED 和 REPEATABLE READ 隔离级别的的一个非常大的区别就是它们生成
ReadView 的时机不同,我们来看一下。

RC隔离级别和RR隔离级别区别
每次读取数据前都生成一个ReadView
比方说现在系统里有两个 id 分别为 100 、 200 的事务在执行:

Transaction 100

BEGIN;
UPDATE user SET name = '张三' WHERE id = 1;
UPDATE user SET name = '李四' WHERE id = 1;
复制代码

Transaction 200

BEGIN;

更新了一些别的表的记录

...
假设现在有一个使用 READ COMMITTED 隔离级别的事务开始执行:

使用READ COMMITTED隔离级别的事务

BEGIN;

SELECT1:Transaction 100、200未提交

SELECT * FROM user WHERE id = 1; # 得到的列name的值为'王五'
这个 SELECT1 的执行过程如下:

在执行 SELECT 语句时会先生成一个 ReadView , ReadView 的 m_ids 列表的内容就是 [100,
200] 。
然后从版本链中挑选可见的记录,最新版本的列name 的内容是 '张三' ,该版本的trx_id 值为 100 ,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。
下一个版本的列 name 的内容是 '李四' ,该版本的 trx_id 值也为 100 ,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。
下一个版本的列 name 的内容是 '王五' ,该版本的 trx_id 值为 80 ,小于 m_ids 列表中最小的事务id 100 ,所以这个版本是符合要求的,最后返回给用户的版本就是这条列 name 为 '王五' 的记录。
之后,我们把事务id为 100 的事务提交一下,就像这样:

Transaction 100

BEGIN;
UPDATE user SET name = '关羽' WHERE id = 1;
UPDATE user SET name = '张飞' WHERE id = 1;
COMMIT;
然后再到事务id为 200 的事务中更新一下表 user 中 id 为1的记录:

Transaction 200

BEGIN;

更新了一些别的表的记录

...
UPDATE user SET name = '云六' WHERE id = 1;
UPDATE user SET name = '王麻子' WHERE id = 1;
然后再到刚才使用 READ COMMITTED 隔离级别的事务中继续查找这个id为 1 的记录,如下:

使用READ COMMITTED隔离级别的事务

BEGIN;

SELECT1:Transaction 100、200均未提交

SELECT * FROM user WHERE id = 1; # 得到的列name的值为'李四'

SELECT2:Transaction 100提交,Transaction 200未提交

SELECT * FROM user WHERE id = 1; # 得到的列name的值为'张三'
这个 SELECT2 的执行过程如下:

在执行 SELECT 语句时会先生成一个 ReadView , ReadView 的 m_ids 列表的内容就是 [200] (事务id为 100 的那个事务已经提交了,所以生成快照时就没有它了)。
然后从版本链中挑选可见的记录,最新版本的列 name 的内容是 '王麻子' ,该版本的 trx_id 值为 200 ,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。
下一个版本的列 name 的内容是 '云六' ,该版本的 trx_id 值为 200 ,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。
下一个版本的列 name 的内容是 '张三' ,该版本的 trx_id 值为 100 ,比 m_ids 列表中最小的事务
id 200 还要小,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name 为 '张三' 的记录。
以此类推,如果之后事务id为 200 的记录也提交了,再此在使用 READ COMMITTED 隔离级别的事务中查询表user 中 id 值为 1 的记录时,得到的结果就是 '王麻子' 了,具体流程我们就不分析了。总结一下就
是:使用READ COMMITTED隔离级别的事务在每次查询开始时都会生成一个独立的ReadView。

只在第一次读取数据生成一个ReadView
对于使用 REPEATABLE READ 隔离级别的事务来说,只会在第一次执行查询语句时生成一个
ReadView ,之后的查询就不会重复生成了。我们还是用例子看一下是什么效果。

比方说现在系统里有两个 id 分别为 100 、 200 的事务在执行:

Transaction 100

BEGIN;
UPDATE user SET name = '张三' WHERE id = 1;
UPDATE user SET name = '李四' WHERE id = 1;
复制代码

Transaction 200

BEGIN;

更新了一些别的表的记录

...
假设现在有一个使用 REPEATABLE READ 隔离级别的事务开始执行:

使用REPEATABLE READ隔离级别的事务

BEGIN;

SELECT1:Transaction 100、200未提交

SELECT * FROM user WHERE id = 1; # 得到的列name的值为'王五'
这个 SELECT1 的执行过程如下:

在执行 SELECT 语句时会先生成一个 ReadView , ReadView 的 m_ids 列表的内容就是 [100,
200] 。
然后从版本链中挑选可见的记录,最新版本的列 name 的内容是 '张三' ,该版本的trx_id 值为 100 ,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版
本。
下一个版本的列name 的内容是 '李四' ,该版本的 trx_id 值也为 100 ,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。
下一个版本的列name 的内容是 '王五' ,该版本的 trx_id 值为 80 ,小于 m_ids 列表中最小的事务id 100 ,所以这个版本是符合要求的,最后返回给用户的版本就是这条列 name 为 '王五' 的记录。
之后,我们把事务id为 100 的事务提交一下,就像这样:

Transaction 100

BEGIN;
UPDATE user SET name = '李四' WHERE id = 1;
UPDATE user SET name = '张三' WHERE id = 1;
COMMIT;
然后再到事务id为 200 的事务中更新一下表user 中 id 为1的记录:

Transaction 200

BEGIN;

更新了一些别的表的记录

...
UPDATE user SET name = '云六' WHERE id = 1;
UPDATE user SET name = '王麻子' WHERE id = 1;
然后再到刚才使用 REPEATABLE READ 隔离级别的事务中继续查找这个id为 1 的记录,如下:

使用REPEATABLE READ隔离级别的事务

BEGIN;

SELECT1:Transaction 100、200均未提交

SELECT * FROM user WHERE id = 1; # 得到的列name的值为'李四'

SELECT2:Transaction 100提交,Transaction 200未提交

SELECT * FROM user WHERE id = 1; # 得到的列name的值仍为'李四'
这个 SELECT2 的执行过程如下:

因为之前已经生成过 ReadView 了,所以此时直接复用之前的 ReadView ,之前的 ReadView 中的
m_ids 列表就是 [100, 200] 。
然后从版本链中挑选可见的记录,最新版本的列 name 的内容是 '王麻子' ,该版本的 trx_id 值为 200 ,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。
下一个版本的列 name的内容是 '云六' ,该版本的 trx_id 值为 200 ,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。
下一个版本的列 name 的内容是 '张三' ,该版本的 trx_id 值为 100 ,而 m_ids 列表中是包含值为
100 的事务id的,所以该版本也不符合要求,同理下一个列 name的内容是 '关羽' 的版本也不符合要求。继续跳到下一个版本。
下一个版本的列 name 的内容是 '李四' ,该版本的 trx_id 值为 80 , 80 小于 m_ids 列表中最小的事务id 100 ,所以这个版本是符合要求的,最后返回给用户的版本就是这条列 name 为 '李四' 的记录。
也就是说两次 SELECT 查询得到的结果是重复的,记录的列 name 值都是 '李四' ,这就是 可重复读 的含义。如果我们之后再把事务id为 200 的记录提交了,之后再到刚才使用 REPEATABLE READ 隔离级别的事务中继续查找这个id为 1 的记录,得到的结果还是 '李四' ,具体执行过程大家可以自己分析一下。

InnoDB的MVCC实现
我们首先来看一下wiki上对MVCC的定义:

Multiversion concurrency control (MCC or MVCC), is a concurrency control
method commonly used by database management systems to provide
concurrent access to the database and in programming languages to
implement transactional memory.

由定义可知,MVCC是用于数据库提供并发访问控制的并发控制技术。与MVCC相对的,是基于锁的并
发控制, Lock-Based Concurrency Control 。MVCC最大的好处,相信也是耳熟能详:读不加锁,读
写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能,这
也是为什么现阶段,几乎所有的RDBMS,都支持了MVCC。

多版本并发控制仅仅是一种技术概念,并没有统一的实现标准, 其核心理念就是数据快照,不同的事务
访问不同版本的数据快照,从而实现不同的事务隔离级别。虽然字面上是说具有多个版本的数据快照,
但这并不意味着数据库必须拷贝数据,保存多份数据文件,这样会浪费大量的存储空间。InnoDB通过
事务的undo日志巧妙地实现了多版本的数据快照。

数据库的事务有时需要进行回滚操作,这时就需要对之前的操作进行undo。因此,在对数据进行修改
时,InnoDB会产生undo log。当事务需要进行回滚时,InnoDB可以利用这些undo log将数据回滚到修
改之前的样子。

原文地址https://www.cnblogs.com/sx-wuyj/p/12567440.html

相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
12天前
|
SQL 关系型数据库 MySQL
轻松入门MySQL:保障数据完整性,MySQL事务在进销存管理系统中的应用(12)
轻松入门MySQL:保障数据完整性,MySQL事务在进销存管理系统中的应用(12)
|
5天前
|
存储 SQL 关系型数据库
【MySQL实战笔记】03.事务隔离:为什么你改了我还看不见?-02
【4月更文挑战第7天】数据库通过视图实现事务隔离,不同隔离级别如读未提交、读已提交、可重复读和串行化采用不同策略。以可重复读为例,MySQL使用多版本并发控制(MVCC),每个事务有其独立的视图。回滚日志在无更早视图时被删除。长事务可能导致大量存储占用,应避免。事务启动可显式用`begin`或设置`autocommit=0`,但后者可能意外开启长事务。建议使用`autocommit=1`并显式管理事务,若需减少交互,可使用`commit work and chain`。
25 5
|
19天前
|
存储 缓存 关系型数据库
MySQL事务的四大特性是如何保证的
在MySQL数据库中还有一种二进制日志,其用来基于时间点的还原及主从复制。从表面上来看其和重做日志非常相似,都是记录了对于数据库操作的日志。但是,从本质上来看有着非常大的不同。
10 1
|
1月前
|
存储 SQL 关系型数据库
[MySQL]事务原理之redo log,undo log
[MySQL]事务原理之redo log,undo log
|
SQL 关系型数据库 MySQL
【mysql】—— 事务
【mysql】—— 事务
|
1月前
|
SQL 关系型数据库 MySQL
深入理解MySQL事务特性:保证数据完整性与一致性
深入理解MySQL事务特性:保证数据完整性与一致性
55 1
|
8天前
|
关系型数据库 MySQL 数据库
mysql卸载、下载、安装(window版本)
mysql卸载、下载、安装(window版本)
|
1月前
|
关系型数据库 MySQL 数据库连接
关于MySQL-ODBC的zip包安装方法
关于MySQL-ODBC的zip包安装方法
|
26天前
|
关系型数据库 MySQL 数据库
rds安装数据库客户端工具
安装阿里云RDS的数据库客户端涉及在本地安装对应类型(如MySQL、PostgreSQL)的客户端工具。对于MySQL,可选择MySQL Command-Line Client或图形化工具如Navicat,安装后输入RDS实例的连接参数进行连接。对于PostgreSQL,可以使用`psql`命令行工具或图形化客户端如PgAdmin。首先从阿里云控制台获取连接信息,然后按照官方文档安装客户端,最后配置客户端连接以确保遵循安全指引。
76 1
|
20天前
|
Ubuntu 关系型数据库 MySQL
Ubuntu 中apt 安装MySQL数据库
Ubuntu 中apt 安装MySQL数据库
65 0