目录
以下介绍的,都是MySQL中innodb存储引擎独有的特性
事务id
引入
- 事务虽然有执行中的过程,但mysql为了解决执行中会出现的并发问题,需要让事务变成原子的
- 并且还要判断当前事务应该看到哪些数据,为了保证隔离性
以上,都要让事务有先有后
- 事务再怎么同时到来,都一定有先后顺序
- 就像多线程去抢占锁资源一样,一定会有一个线程首先占到
我们如何区分事务的先后呢 -- 事务id
介绍
每当一个事务开始时,InnoDB都会通过全局递增计数器为每个事务分配一个唯一的事务id
- 所以,这个id是严格递增且不重复的
- 当事务完成时,事务id会随着事务的结束而不再使用,但该id仍然保存在与该事务相关的表记录中
可以根据事务id的大小,判断事务到来的顺序
- 数字越小,来的越早
如何管理事务
mysqld会同时处理多个事务,并且我们心里清楚,每个事务都有自己的生命周期(被创建,被执行,被销毁)
- 所以,mysqld必然要对这么多的事务进行管理 -- 先描述,再组织
- 所以,事务在内存中一定是一个/一套结构体/类对象(mysql是用c/c++实现的)
那么前面说到的事务id,就是这个对象中的一个字段
- 先原子性地申请一个id,然后再用该值初始化对象中对应字段
数据库表的隐藏列
介绍
在 MySQL 的 InnoDB 存储引擎中,每个数据行都会有三个隐藏列(hidden columns)
- 这些隐藏列并不是用户直接定义的,而是由 InnoDB 自动添加并用于管理内部事务和多版本控制(MVCC)等功能
- 这三个隐藏列为 MySQL 实现事务的隔离性、多版本并发控制等功能提供了基础
DX_TRX_ID
6字节字段,最近修改 ( 修改 / 插入 ) 事务id
- 该列记录创建这条记录/最后一次修改该记录的事务id
无论是手动开启的事务,还是系统默认封装单sql为事务
- 所有对数据库表做操作的sql,都会以事务的形式交给mysql
- 所以,每个操作都可以和事务关联起来,都有一个事务id
所以,记录下最新对该记录做修改的事务id,让mysql可以对信息做溯源
- 并且可以用来判断其他事务是否可以看到该行的修改
DB_ROLL_PTR
7字节字段,回滚指针
- 指向该条记录的上一个版本(也就是历史版本,在undo log中保存)
在修改表中数据之前,可能会将要改的这条记录先保存一份,然后再对数据做修改
- 类似于写时拷贝
既然保存了历史数据,就一定要让最新记录可以找到对应的历史记录
- 所以就有了这一列回滚指针,指向历史记录
- 用于支持快照读,回滚操作
DB_ROW_ID
6字节字段,隐藏自增id
- 也就是隐藏主键
如果表中没有主键,会自动以DB_ROW_ID产生一个聚簇索引(b+树)
由于DB_ROW_ID是自增的
- 所以这个聚簇索引的顺序将根据插入的顺序排列
如果表中已经定义了主键,那这一列就没用了
- 会使用定义出来的主键构建索引结构
delete flag
实际上还有一个删除flag隐藏字段
- 记录该条记录是否被删除
删除记录时:
- 并没有真的删除,只是变化flag的值
- 因为记录是内存级的,所以我们删除时并不需要清空这块内存,并进行结构上的移动,那样太麻烦了
- 我们只需要维护page是否"脏"(表示这些页已被修改但尚未写入磁盘),刷盘时再将有效数据紧凑排列在磁盘中
所以,flag也是一个典型的用空间换时间的策略
- 不仅如此,也可以用于在后续恢复数据,只需要再次变化flag
undo log
引入
mysql中索引,事务,隔离性,日志等机制,都是在内存中完成
除了buffer pool,mysql还有自己的日志缓冲区
- 保存mysql中对数据做并发提交时产生的临时数据,来支持回滚等操作
介绍
是 MySQL(特别是 InnoDB 存储引擎)中一个重要的机制,用于实现事务的回滚、维护数据的一致性,并支持多版本并发控制(MVCC)
- 会存储数据在事务执行期间的旧版本信息,以及与修改操作的相反sql语句
两种读取操作方式
当前读
介绍
读取的是数据的最新记录
- 执行增删改,和部分select时,会采用当前读的方式
当前读会在读取数据时加锁
- 确保读取到的数据在整个读取过程中不被其他事务修改
使用select的当前读方式
select * from 表名 for update; select * from 表名 lock in share mode;
快照读
介绍
读取的是数据的历史版本
- 不需要加锁,因为历史版本只会被读取,不能被修改,所以不需要保护
- 允许事务在执行期间获取特定时间点的数据快照
既提高了效率,又为隔离性提供了底层支持
与隔离性的关系
因为隔离级别的设置,可能两个事务看到的数据不同
- 就代表他们读的一定不是同一份数据
所以,为什么读写可以并发呢?
- 因为两个操作访问的不是同一份数据,也就不需要加锁,也就可以并发执行了
- 写的是当前的最新版本,读的是历史版本(指普通的select)
所以,事务的隔离性:
- 本质是在数据层面上隔离 -- 在undo log中保存了数据的历史版本
- 做法是在版本上隔离 -- 快照读通过提供独立的视图和历史数据版本,支持了事务隔离性的实现
那么我们应该看到哪个版本呢? -- 由隔离级别决定
Read View(读视图)
介绍
事务进行快照读操作时,会对该记录创建一个读视图
- 是做可见性判断的一个重要数据结构,记录并维护系统当前活跃事务的id
- 因为每条记录都有[最新修改它的事务id],所以可以通过当前id和该id的对比,来决定自己应不应该看见它
每个事务都要做可见性判断
- 可见性 -- 某一条记录是否对当前事务可见
- 所以mysql需要对所有可见性进行管理,这个管理结构就是读视图
事务和可见性的关系
- 类似于进程和进程地址空间的关系
- 都是用于判断某个结构对于某个资源的可见性
结构
class ReadView { // 省略... private: trx_id_t m_low_limit_id; /** 高水位,大于等于这个ID的事务均不可见*/ trx_id_t m_up_limit_id; /** 低水位:小于这个ID的事务均可见 */ trx_id_t m_creator_trx_id; /** 创建该 Read View 的事务ID*/ ids_t m_ids; /** 创建视图时的活跃事务id列表*/ trx_id_t m_low_limit_no; /** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG, * 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/ bool m_closed; /** 标记视图是否被关闭*/ // 省略... };
- purge -- mysql中的一个线程,进行数据刷新,undo log的清空等
总之,重要字段就是下面4个:
m_ids
ids_t 集合类型
记录当前读视图结构创建时,系统中正在活跃的事务id列表
- 正在活跃 = 处于执行中的事务
注意,活跃事务的列表中,事务id可能不是连续的
- 因为事务有长有短,无法确定谁先结束
up_limit_id
记录m_ids列表中的最小事务id
low_limit_id
当前读视图生成时,系统尚未分配的下一个事务id
- 也就是目前系统中已经出现过的最大事务id+1