事务的隔离性底层
- 1.数据库并发的场景
- 2.读-写
- 2.1MVCC三个变量
- 2.1.1 3个记录隐藏列字段
- 2.1.2 undo日志
- 模拟MVCC
- select 的读取
- 2.1.3 Read View(读视图)
- 3.RR与RC的区别
1.数据库并发的场景
- 读-读:不存在问题,也不需要并发控制
- 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
- 写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失
2.读-写
一般来说上面的场景中读写的场景占比是最大的。
多版本并发控制(MVCC )是一种用来解决读-写冲突的无锁并发控制。
他会给事务分配一个单向递增的事务ID,为每个修改保存一个版本,版本和事务ID关联,读操作只读取改事务开始前的数据快照,它解决了
- 并发读写数据库的时候,可以做到在读操作的时候不用阻塞写操作,写操作的时候不用阻塞读操作
- 还解决了脏读,幻读,不可重读的问题,但不能解决更新丢失的问题
2.1MVCC三个变量
2.1.1 3个记录隐藏列字段
2.1.2 undo日志
我们把它理解为MySQL 中的一段内存缓冲区,用来保存日志数据的就行
模拟MVCC
假如有一个事务10,他现在要更新数据;
- 先给事务10加锁
- 我们把原始的数据拷贝一份到undo log 中;
- 现在数据库中有两份数据,把张三的名字改成李四后,那么DB_TRX_ID就变成了事务10,回滚指针里面存放的就是undo log中被修改的原始数据的地址。
- 事务10提交,释放锁
现在又多了一个事务11要update李四里面的age改称38
- 先给事务11加锁
- 把要被修改的数据做一份拷贝,到undo log中,
- 现在有两份数据,把age改成38后,DB_TRX_ID变成事务11,回滚指针就变成了undo log中的第一个数据的地址。
这样,我们就有了一个基于链表记录的历史版本链。所谓的回滚,无非就是用历史数据,覆盖当前数据。上面的一个一个版本,我们可以称之为一个一个的快照。
select 的读取
当前读:读取最新的记录,就是当前读。增删改,都叫做当前读
快照读:读取历史版本(一般而言),就叫做快照读。
很多事务在CURD的时候都是当前读,是要加锁的,那么这个时候有select要进行读取最新版本的数据的时候也需要加锁,这就是串行化。
当时如果是快照读,是不受加锁限制的, 所以加锁只针对于最新版本
决定select是当前读还是快照读的是隔离级别;
事务都是原子的。所以,无论如何,事务总有先有后。 那么多个事务在执行中,CURD操作是会交织在一起的。那么,为了保证事务的“有先有后”,是不是应该让不同的事务看到它该看到的内容,这就是所谓的隔离性与隔离级别要解决的问题。也就是先来的事务不应该看到后来事务的数据。
2.1.3 Read View(读视图)
**Read View就是事务进行快照读操作的时候生产的读视图(Read View),**在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID,这个ID是递增的,所以最新的事务,ID值越大)。
Read View在MySQL源码中,就是一个类,本质是用来进行可见性判断的。即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件,用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。
我们看看它的结构
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;
// 省略...
};
我们在实际读取数据版本链的时候,可以读取到每一个版本对应的事务ID,即:当前记录的DB_TRX_ID;
我们现在手里面有的东西就有,当前快照读的ReadView 和 版本链中的某一个记录的DB_TRX_ID.
问题:当前快照读,应不应该读取到当前版本的记录
通过版本对应的事务ID与Read View的ID的大小进行判断
注意:视图是一个对象,初始化之后就不会在改变了。
我们能看到的额数据应该是:
- up_limit_id>DB_TRX_ID
- 同一个时期活跃的ID,但是不在我的m_ids中(这个事务在我之前已经提交了)
3.RR与RC的区别
- 正是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才会有不可重复读问题。