0.背景
在mysql的并发访问中,有几个典型的并发场景:
-
读-读:无需处理,都是读取,不会对数据有影响。
-
写-写:由于都涉及到数据的修改,不可能乱改,所以没有较好的方式来处理,一般都得加锁。
-
读-写:读写场景,加锁当然ok。不过读操作是很频繁的,一但写数据就不让读取了,这种情况是让人很难受的。
Mysql的前辈们通过Mvcc的方案来优化这个问题。通过Mvcc,可以无锁的方式兼顾读写的场景,无论数据是否在修改,都不会阻塞读操作。
1.快照读和当前读
mysql读取数据有当前读和快照读的区分
数据嘛,无非就是增删改查,读取的时候,由于要考虑并发,可能会有别的事务在修改这个数据。
然后呢,Mysql引入了一个快照的概念,快照嘛,就跟照片一样,将一个临时的状态记录下来。
所以上面的2个呆板的词语,可以理解为:
- 当前读:读取最新的数据
- 快照读:读取快照时的数据
回到最开始的问题,写数据的时候,一定要阻塞吗?写数据的时候,可不可以读数据。
读数据,我不是读取最新的数据吗?我读取快照那个瞬间的数据有啥意思?
这个点在于我们处在当前事务的角度来看,事务的基本特性是ACID。
如果我们在一个事务里面,读取到的数据都是混乱的,那还能操作吗?
快照读的意义就是解决之前提到的不可重复读和幻读这些问题,来保证同一事务中的数据一致性。
关键点:是为了在当前事务中,保证数据读取的一致性。
1.1 当前读
对读取的记录加锁,读取最新的数据,并阻塞其他事务同时修改。
- 读取的是数据库中当前的最新数据。
- 如果其他事务正在对数据进行修改,在当前读中读取到的是已经提交的最新版本数据,而不考虑其他事务的未提交变更。
- 在当前读中,事务读取数据时会获取短暂的共享锁,以确保其他事务在该事务读取数据期间不能修改数据,但不阻塞其他事务的读取操作。
场景:
-- 更新
-- 更新嘛,必须拿着最新的记录进行更新,不能拿个快照数据来更新,假设别的事务把数据给你删除了,还更新啊?
update ...
-- 删除
delete ...
-- 插入
insert ...
-- 共享锁
select ... lock in share mode
-- 独占锁
select ... for update
1.2 快照读
读数据不需要进行加锁,也不会阻塞和被其他事务阻塞。
- 快照读是指事务在执行查询操作时,读取的是事务开始时的一致性快照,即查询操作在事务启动时所能看到的数据版本。
- 快照读不会受到其他事务正在修改数据的影响,读取的是一致性的数据快照。
- 在快照读中,事务读取数据时不会获取锁,因此不会阻塞其他事务的读取或写入操作。
场景:
-- 单纯的select操作(不包括上面的共享锁和独占锁)
select ...
2.Mvcc关键点
MVCC(Multi-version concurrency control 多版本并发控制)是一种并发控制机制,用于在事务并发执行时保证数据的一致性和隔离性,它就是快照读的一种解决方案。
2.1 隐藏字段
在Mysql中,事务id是默认递增的。
在数据库行记录中,存在一些隐藏字段,例如DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID等。
-
db_trx_id
6-byte的事务ID,处理这条数据对应的事务id。
-
db_roll_ptr
7-byte的回滚指针,就是指向行记录的上一个版本。
-
db_row_id
6-byte的隐藏主键。如果数据表中没有主键,那么InnoDB会自动生成单调递增的隐藏主键(表中有主键或者非NULL的UNIQUE键时都不会包含 DB_ROW_ID列)。
DB_TRX_ID、DB_ROLL_PTR这2个字段是Mvcc涉及到的主要字段。
2.2 Undo Log(回滚日志)
在Mysql中进行数据修改时,会在undo log中记录存量的数据值,多个版本的数据进行记录后,就形成了一个版本链。
例如:回滚指针会指向上个版本的数据记录,例如在事务101、102中先后将name修改为了yang1和yang2。
这个设计,方便实现以下功能。
- 方便回滚,直接根据回滚指针找到之前的数据进行回退。
- 在Mvcc中用于读取数据(后文描述)
2.3 Read View(读视图)
Read View(读视图)是进行查询操作时生成的一个视图,事务开始前会创建Read View(RC、RR级别有差异),根据生成的这个Read View,判断读取哪个版本的数据。
额,当做是个临时对象好了,存几个值方便后续进行比较。
包含4个关键字段:
活跃事务:创建了事务但是还没提交
- creator_trx_id:创建这个Read View的事务ID,即创建者的事务ID。
- m_ids:创建ReadView时,活跃事务的ID集合。
- min_trx_id:创建ReadView时,活跃事务中最小事务ID。
- max_trx_id:创建ReadView时,当前最大事务ID+1(应该分配给下一个事务的id值)。
这里要注意下min和max跟m_ids的关系:
1.min_trx_id是m_ids活跃事务集合中的最小值。
2.max_trx_id通常跟m_ids没关系,max_trx_id是在创建ReadView时,计划分配给下一个事务的ID值。
3.min_trx_id <= m_ids < max_trx_id
好,那我为啥要关注min_trx_id和max_trx_id呢?
ReadView记录了我们当前这瞬间的一些快照信息。
min_trx_id是快照时的最小活跃事务id,最小的活跃事务呀,如果一个事务id比这个min_trx_id小的话,这个事务在快照时就肯定被提交过了,相当于一个过去的事件,所以一定是可以看到的。
max_trx_id是快照时的待分配事务id,最大的事务id,如果一个事务id比这个max_trx_id还要大的话,这个事务快照时肯定还没提交,相当于一个未来的事件,那么肯定是不可见的。
2.3.1 RC和RR的差异
- read committed (读已提交):事务每次select时都创建ReadView
- repeatable read (可重复读):事务第一次select时创建ReadView,后续一直使用。
2.4 可见性判断
2.4.1 事务ID比较可见性
每个数据版本都有一个db_trx_id
,代表创建该版本的事务ID(db_trx_id
)。
将db_trx_id
与和ReadView中的参数进行比较,以确定该版本对当前事务的可见性。
2.4.2 可见性匹配原则
这里我的时间角度,就是说提交数据的那个事务(版本链中的事务),提交这个动作发生在什么时候。
首先,显而易见的几个条件是:
序号 | 条件 | 可见性 | 时间角度 | 描述 |
---|---|---|---|---|
1 | db_trx_id < min_trx_id | 可见 | 过去 | 创建ReadView时,数据已经提交。 |
2 | db_trx_id >= max_trx_id | 不可见 | 未来 | 创建ReadView时,数据还位提交。 |
3 | db_trx_id = creator_trx_id | 可见 | 当前 | 就是当前事务,数据对当前事务可见。 |
对于min_trx_id <= db_trx_id < max_trx_id这个区间的数据,要结合m_ids来判断。
序号 | 条件 | 可见性 | 时间角度 | 描述 |
---|---|---|---|---|
1 | db_trx_id 在 m_ids 中 | 不可见 | 未来 | 创建ReadView时,数据仍然活跃,还未提交。 |
2 | db_trx_id 不在 m_ids 中 | 可见 | 过去 | 创建ReadView时,数据已经提交 |
2.4.3 判断检查结果
- 如果数据版本对当前事务可见,则可以读取该数据版本。
- 如果数据版本对当前事务不可见,则继续检查下一个版本,直到找到一个可见的版本或者版本链结束。