「前言」文章内容大致是Mysql事务管理,续上一篇。 「归属专栏」MySQL 「主页链接」个人主页 「笔者」枫叶先生(fy) 目录 七、再次理解隔离性7.1 数据库并发的场景有7
「归属专栏」MySQL
「主页链接」个人主页
「笔者」枫叶先生(fy)
数据库并发的场景有以下三种:
其中读-写并发是数据库当中最高频的场景,下面讨论的就是这个,读-读并发不存在任何问题,写-写并发不谈
多版本并发控制(Multi-Version Concurrency Control
)是一种用来解决读-写冲突
的无锁并发控制,主要依赖记录中的3个隐藏字段列、undo
日志和Read View
实现
MVCC
为事务分配单向增长的事务ID,为每个修改保存一个版本,版本与事务ID关联,读操作只读该事务开始前的数据库的快照。 所以MVCC
可以为数据库解决以下问题:
下面先介绍3个隐藏字段列、undo
日志和Read View
数据库表中的每条记录都会有如下3个隐藏字段列:
DB_TRX_ID
:6byte,最近修改(修改/插入)事务ID,记录创建这条记录/最后一次修改该记录的事 务IDDB_ROLL_PTR
:7 byte,回滚指针,指向这条记录的上一个版本(简单理解成,指向历史版本就 行,这些数据一般在undo log
中)DB_ROW_ID
:6 byte,隐含的自增ID(隐藏主键),如果数据表没有主键, InnoDB
会自动以DB_ROW_ID
产生一个聚簇索引补充:
flag
隐藏字段列,用于记录该条数据是否被删除,便于进行数据回滚(删除数据并不是真的删除了,只是修改了flag
字段)例如,有一个测试表
create table if not exists student(name varchar(11) not null,age int not null);
插入一条数据
insert into student (name, age) values ('张三', 28);
查询该表的数据
查出来的记录不仅包含name和age字段,还包含三个隐藏字段
说明:
DB_TRX_ID
字段填的就是1,没有就为null
DB_ROW_ID
字段填的就是1DB_ROLL_PTR
的值设置为null
undo log
简单理解成就是 MySQL 中的一段内存缓冲区,用来保存日志数据的,必要时会将缓冲区中的数据刷新到磁盘
事务ID为10的事务
假设现在有一个事务ID为10的事务,要将刚才插入学生表中的记录的学生姓名“张三”改为“李四”:
因为是要进行写操作,所以需要先给该记录加行锁
修改前,现将改行记录拷贝到undo log
中,所以,undo log
中就有了一行副本数据(原理就是写时拷贝)
所以现在MySQL中有两行同样的记录
现在修改原始记录中的name,改成 ‘李四’,并且修改原始记录的隐藏字段 DB_TRX_ID
为当前 事务10 的ID,我们默认从10开始,之后递增
而原始记录的回滚指针DB_ROLL_PTR
列,里面写入undo log
中副本数据的地址,从而指向副本记录,既表示我的上一个版本就是它
最后当事务10提交后释放锁,这时最新的记录就是学生姓名为“李四”的那条记录
现在又有一个事务11
现在又有一个事务11,对student表中记录进行修改(update):将age(28)改成age(38)
undo log
中,此时undo log
中就又有了一行副本数据DB_TRX_ID
改为11,回滚指针DB_ROLL_PTR
设置成刚才拷贝到undo log
中的副本数据的地址,从而指向该记录的上一个版本上面的一个一个版本,我们可以称之为一个一个的快照
insert和delete的记录如何维护版本链
上面已经谈了update,update可以形成版本链,那insert和delete呢?
undo log
中,然后将该记录的删除flag隐藏字段设置为1,这样回滚后该记录的删除flag隐藏字段就又变回0了,相当于删除的数据又恢复了update
和delete
和insert
可以形成版本链select
不会对数据做任何修改,所以,为select
维护多版本,没有意义
select读取,是读取最新的版本呢?还是读取历史版本?
先说两个概念,当前读 VS 快照读:
事务在进行增删查改的时候,并不是都需要进行加锁保护:
而select查询时应该进行当前读还是快照读,则是由隔离级别决定的,在读未提交和串行化隔离级别下,进行的都是当前读,而在读提交和可重复读隔离级别下,既可能进行当前读也可能进行快照读。
undo log中的版本链何时才会被清除?
undo log
版本链就会被清除如何保证,不同的事务,看到不同的内容呢?也就是如何实现隔离级别?
Read View
就是事务进行快照读
操作的时候生产的读视图 (Read View
),(即使用select查看数据的时候才会产生读视图)ReadView类的源码如下:
class ReadView {// 省略...private:trx_id_t m_low_limit_id;trx_id_t m_up_limit_id;trx_id_t m_creator_trx_id;ids_t m_ids;trx_id_t m_low_limit_no;bool m_closed;// 省略...};
上述四个成员说明:
m_ids; //一张列表(集合),用来维护Read View生成时刻,系统正活跃的事务IDm_up_limit_id; //记录m_ids列表中事务ID最小的IDm_low_limit_id; //ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1m_creator_trx_id //创建该ReadView的事务ID
由于事务ID是单向增长的,因此根据Read View中的m_up_limit_id
和m_low_limit_id
,可以将事务ID分为三个部分:
m_up_limit_id
的事务m_up_limit_id
和m_low_limit_id
之间的事务m_low_limit_id
的事务注:ReadView就是一个对象,初始化一次之后就不再改变了(只初始化一次)
m_up_limit_id
的事务:一定是生成Read View
时已经提交的事务,因为m_up_limit_id
是生成Read View
时刻系统中活跃事务ID中的最小ID,因此事务ID比它小的事务在生成Read View时一定已经提交了m_up_limit_id
和m_low_limit_id
之间的事务:该区间的事务,在生成Read View
时可能正处于活跃状态,也可能已经提交了,这时需要通过判断事务ID是否存在于m_ids
中来判断该事务是否已经提交(所有活跃的事务都在m_ids
中)m_low_limit_id
的事务:一定是生成Read View
时还没有启动的事务,因为m_low_limit_id
是生成Read View
时刻,系统尚未分配的下一个事务ID注意:事务ID不一定是连续的,比如事务ID10,15,16,17…
上述对应的隐藏字段:
m_creator_trx_id
(创建该ReadView
的事务ID)== DB_TRX_ID
或者 DB_TRX_ID
< m_up_limit_id
,则说明该事务是历史已经提交了的(已commit),应该被当前事务看到DB_TRX_ID
不在m_ids
列表中,说明该事务已经提交了(已commit),应该被当前事务看到。如果在的话m_ids
列表中,说明该事务与当前事务都处于活跃状态(没有commit),不应该被当前事务看到DB_TRX_ID
>= m_low_limit_id
,说明该事务是快照之后才提交的事务,不应该被当前事务看到对应的源码策略如下:
bool changes_visible(trx_id_t id, const table_name_t& name) const MY_ATTRIBUTE((warn_unused_result)){ut_ad(id > 0);//1、事务id小于m_up_limit_id(已提交)或事务id为创建该Read View的事务的id,则可见if (id < m_up_limit_id || id == m_creator_trx_id) {return(true);}check_trx_id_sanity(id, name);//2、事务id大于等于m_low_limit_id(生成Read View时还没有启动的事务),则不可见if (id >= m_low_limit_id) {return(false);}//3、事务id位于m_up_limit_id和m_low_limit_id之间,并且活跃事务id列表为空(即不在活跃列表中),则可见else if (m_ids.empty()) {return(true);}const ids_t::value_type* p = m_ids.data();//4、事务id位于m_up_limit_id和m_low_limit_id之间,如果在活跃事务id列表中则不可见,如果不在则可见return (!std::binary_search(p, p + m_ids.size(), id));}
注意:Read View是一个可见性的一个类,并不是事务创建出来就有Read View,而是当这个事务(已经存在)进行快照读的时候,MySQL才会形成Read View(进行select的时候,会自动形成)
下面进行 Read View的理论验证,即 Read View的整体流程
假设当前有条记录
有四个事务并发进行对该记录进行操作,事务4先进行修改,事务4进行修改完成之后,事务2进行快照读
事务2
对某行数据执行了快照读
,数据库为该行数据生成一个 Read View 读视图//事务2的 Read Viewm_ids; // 1,3m_up_limit_id; // 1m_low_limit_id; // 4 + 1 = 5,原因:ReadView生成时刻,系统尚未分配的下一个事务IDm_creator_trx_id // 2
此时版本链是:
只有事务4修改过该行记录,并在事务2执行快照读前,就提交了事务
事务2在快照读该行记录的时候,就会拿该行记录的DB_TRX_ID
去跟m_up_limit_id
,m_low_limit_id
和活跃事务ID列表(m_ids
) 进行比较,判断当前事务2能看到该记录的版本
//事务2的 Read Viewm_ids; // 1,3up_limit_id; // 1low_limit_id; // 4 + 1 = 5,原因:ReadView生成时刻,系统尚未分配的下一个事务IDcreator_trx_id // 2//事务4提交的记录对应的事务IDDB_TRX_ID=4//比较步骤DB_TRX_ID(4)< up_limit_id(1) ? 不小于,下一步DB_TRX_ID(4)>= low_limit_id(5) ? 不大于,下一步m_ids.contains(DB_TRX_ID) ? 不包含,说明,事务4不在当前的活跃事务中//结论故,事务4的更改,应该看到所以事务2能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本
RR是可重复读的缩写,RC是读提交的缩写
我们之前的查询语句都是快照读,如果想进行当前读,执行下列语句:
-- 以加共享锁方式进行读取,对应的就是当前读select * from 表名字 lock in share mode; -- 当前读
实验1
启动两个终端,将隔离级别都设置为可重复读,并查看此时表中的数据
两个终端各自启动一个事务,在左终端中的事务操作之前,先让右终端中的事务查看一下表中的信息
左终端中的事务对表中的信息进行修改并提交,右终端中的事务看不到修改后的数据
左边终端提交事务;右边终端进行当前读,可以看到最新的数据
select * from account lock in share mode;
实验2(进行同样的操作,只是SQL语句执行顺序不同)
左终端中的事务对表中的信息进行修改并提交,然后再让右终端中的事务进行查看,这时右终端中的事务就直接看到了修改后的数据
右边终端进行当前读,可以看到刚才读取到的确实是最新的数据
实验对比
上面两次实验的唯一区别在于,右终端中的事务在左终端中的事务修改数据之前是否进行过快照读
实验一的操作流程:
实验2的操作流程:
事务B在事务A修改age前没有进行过快照读
结论
RR与RC的本质区别
正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同
Read View
,所以只要当前事务在其他事务提交更Read View
,所以对之后的修改不可见Read View
时,Read View会记录此时所有其他活动事务的快照,这些事 务的修改对于当前事务都是不可见的Read View
创建的事务所做的修改均是可见RC
级别下的,事务中,每次快照读都会新生成一个快照和Read View
,这就是我们在RC
级别下的事务中可以看到别的事务提交的更新的原因RC
隔离级别下,是每个快照读都会生成并获取最新的Read View
Read View
,之后的快照读获取的都是同一个Read View
Read View
,所以,RC
才会有不可重复读问题。--------------------- END ----------------------
「 作者 」 枫叶先生「 更新 」 2023.9.10「 声明 」 余之才疏学浅,故所撰文疏漏难免, 或有谬误或不准确之处,敬请读者批评指正。
来源地址:https://blog.csdn.net/m0_64280701/article/details/132743279
--结束END--
本文标题: 【MySQL系列】MySQL的事务管理的学习(二)_ 再次理解隔离性
本文链接: https://lsjlt.com/news/421739.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
2024-10-23
2024-10-22
2024-10-22
2024-10-22
2024-10-22
2024-10-22
2024-10-22
2024-10-22
2024-10-22
2024-10-22
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0