事务对隔离性的原理理解

  • 脏读:两个事务并行,A事务做的一切,B事务就可以立刻知道。
  • 不可重复读:一个事务受到另一个事务的影响导致连续的select不统一,RU、RC都会导致。
  • 幻读:一般的数据库在可重复读情况的时候,无法屏蔽其他事务insert的数据(为什么? 因为隔离性实现是对数据加锁完成的,而insert待插入的数据因为并不存在,那么一般加锁无法屏蔽这类问题),会造成虽然大 部分内容是可重复读的,但是insert的数据在可重复读情况被读取出来,导致多次查找时,会多查找出来新的记录,就如同产 生了幻觉。这种现象,叫做幻读(phantom read)。

让我们对隔离性做深入理解

我们知道隔离性分为:

  • RU读未提交:会发生脏读,不可重复读,幻读,无需读锁。
  • RC读已提交:不会发生脏读,但是不可重复读,幻读,无需读锁。
  • RR可重复读:不会发生脏读、幻读,不可重复读,无需读锁。
  • SE可串行化:不会发生脏读、幻读,不可重复读,但是需读锁。

模仿MVCC操作了解RR于RC的意义

多版本并发控制( MVCC )是一种用来解决 读-写冲突 的无锁并发控制

为事务分配单向增长的事务ID,为每个修改保存一个版本,版本与事务ID关联,读操作只读该事务开始前的数据库的快照。所以 MVCC 可以为数据库解决以下问题

  • 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读 写的性能
  • 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题

理解 MVCC 需要知道三个前提知识:

  1. 3个记录隐藏字段
  2. undo 日志
  3. Read View

一、三个隐藏字段

  • DB_TRX_ID :6 byte,最近修改( 修改/插入 )事务ID,记录创建这条记录/最后一次修改该记录的事务ID
  • DB_ROLL_PTR : 7 byte,回滚指针,指向这条记录的上一个版本(简单理解成,指向历史版本就行,这些数据一 般在 undo log 中)name age DB_TRX_ID(创建该记录的事务 ID) DB_ROW_ID(隐式主 键)
  • DB_ROLL_PTR(回滚指 针) 张三 28 null 1 null DB_ROW_ID : 6 byte,隐含的自增ID(隐藏主键),如果数据表没有主键, InnoDB 会自动以 DB_ROW_ID 产生一 个聚簇索引
  • 补充:实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了

我们看似一个表只有这两列数据,实际是

有3列隐藏列

我们假设创建该记录的事务ID为9,隐式主键,我们就默认设置成1。第一条记录也没有其他版本,我们 设置回滚指针为null。

二、undo 日志

这里不想细讲,但是有一件事情得说清楚, MySQL 将来是以服务进程的方式,在内存中运行。我们之前所讲的所有 机制:索引,事务,隔离性,日志等,都是在内存中完成的,即在 MySQL 内部的相关缓冲区中,保存相关数据,完 成各种判断操作。然后在合适的时候,将相关数据刷新到磁盘当中的。

三、Read View

每一个事物一般来说都拥有一个Read View结构体这个结构体是我们读写不同表的关键结构体,等等细讲。

开始理解

先介绍undo工作原理,我们可以理解这是一块缓冲区,临时存放数据的地方,每次执行写操作后的对应动作,在写前会先留存前数据,后再对表行写入新数据。

假设现在来了个事务10修改了该行数据,需将id=1改为id=2:先将当前的数据行保存一份放入undo,然后再改变id值,并且改变隐藏列的DB_TR_ID改为当前事务:9->10,DB_ROLL_RLP:null->0x1111(刚拷贝在undo中的地址)。

类似写时拷贝机制。

然后事务10commit提交信息,释放该行锁。

**假设又来了个事务11修改了该行数据:**需将name='张三'->'zhangshan',再次先保存当前行数据到undo,然后才改变name值,并且改变隐藏列的DB_TR_ID改为当前事务:10-11,DB_ROLL_RLP

:0x1111->0x1222((刚拷贝在undo中的地址));

事务11提交,释放锁。

这样,我们就有了一个基于链表记录的历史版本链。所谓的回滚,无非就是用历史数据,覆盖当前数据。一个一个版本,我们可以称之为一个一个的快照


上面是以更新(`upadte`)主讲的,如果是`delete`呢?一样的,别忘了,删数据不是清空,而是设置flag为删除即可。也可以形成版本。

如果是`insert`呢?因为`insert`是插入,也就是之前没有数据,那么`insert`也就没有历史版本。但是一般为了回滚操作,insert的数据也是要被放入undo log中,如果当前事务commit了,那么这个undo log 的历史insert记录就可以被 清空了。

总结一下也就是我们可以理解成,`update`和`delete`可以形成版本链,`insert`暂时不考虑。


在select读取数据时候分为读新数据与undo中的旧版本数据

当前读:读取最新的记录,就是当前读。增删改,都叫做当前读,select也有可能当前读,比如:select lock in share mode(共享锁), select for update

快照读:读取历史版本(一般而言),就叫做快照读。

总而言之,对最新的版本增删查改操作我们称之为当前读,而查select也能当前读,但是在RC,RR下一般都是快照。

再介绍Read View的

Read View就是事务进行 快照读 操作的时候生产的 读视图 (Read View),在该事务执行的快照读的那一刻,会生成数 据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增 的,所以最新的事务,ID值越大)

cpp 复制代码
class ReadView {
// 省略...
private:
/** 高水位,大于等于这个ID的事务均不可见*/
trx_id_t m_low_limit_id
/** 低水位:小于这个ID的事务均可见 */
trx_id_t m_up_limit_id;
/** 创建该 Read View 的事务ID*/
trx_id_t m_creator_trx_id;
/** 创建视图时的活跃事务id列表*/
ids_t m_ids;
/** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,
* 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/
trx_id_t m_low_limit_no;
/** 标记视图是否被关闭*/
bool m_closed;
// 省略...
};

m_ids; //一张列表,用来维护Read View生成时刻,系统正活跃的事务ID
up_limit_id; //记录m_ids列表中事务ID最小的ID(没有写错)
low_limit_id; //ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1(也没有写错)
creator_trx_id //创建该ReadView的事务ID

当一个事务启动时,并不会直接获得readview,而是在第一次对行快照读的时候才会获得readview。

readview并不是一行版本串一个readview,而是整个事务用一个readview,readvice完成读取。

左手版本串,右手readview我们就可以存在读写无锁并发分访问的原理

重新对该表修改数据

  • 事务4:修改name(张三) 变成name(李四)
  • 当 事务2 对某行数据执行了 快照读 ,数据库为该行数据生成一个 Read View 读视图
cpp 复制代码
//事务2的 Read View
m_ids; // 并行运行的事务id有:1,3
up_limit_id; // 快照截取的m_ids最小id为:1
low_limit_id; // 4 + 1 = 5,原因:ReadView生成时刻,系统尚未分配的下一个事务ID
creator_trx_id // 该readview所属事务id:2

此时版本链是:

只有事务4修改过该行记录,并在事务2执行快照读生成readview前,就提交了事务。

我们的事务2在快照读该行记录的时候,就会拿该行记录的 DB_TRX_ID 去跟up_limit_id,low_limit_id和活跃事务ID列表(m_ids) 进行比较,判断当前事务2能看到该记录的版本。

cpp 复制代码
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级别下

cpp 复制代码
select * from user lock in share mode ,以加共享锁方式进行读取,对应的就是当前读。此处只作为测
试使用,

RR 与 RC的本质区别--readview,每次快照读是否更新

  • 正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同
  • 在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View, 将当前系统活跃的其他事务 记录起来
  • 此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过 快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见;
  • 即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于 当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见
  • 而在RC级别下的,事务中,每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下的事务中可以 看到别的事务提交的更新的原因
  • 总之在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务 中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。
  • 正是RC每次快照读,都会形成Read View,所以,RC才会有不可重复读问题。
相关推荐
YOU OU3 小时前
Redis基础常识与命令
数据库·redis·缓存
Scott9999HH3 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社4 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海4 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖5 小时前
JDK 26 新特性详解
java·开发语言
Database_Cool_5 小时前
OLTP 和 OLAP 区别详解:分析型数据库和事务型数据库怎么选(附阿里云 AnalyticDB MySQL 选型指南)
数据库·mysql·阿里云
爱写代码的阿森5 小时前
鸿蒙三方库 | harmony-utils之KvUtil键值型数据库操作详解
数据库·华为·harmonyos·鸿蒙·huawei
马优晨6 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
Database_Cool_6 小时前
单机 MySQL 迁移到分布式数据库方便吗?阿里云 PolarDB-X 100% MySQL 协议兼容零改造平滑迁移
数据库·分布式·mysql