目录
一、数据库并发的三种场景
二、读写场景的MVCC
多版本并发控制( MVCC )是一种用来解决 读-写冲突 的无锁并发控制。
为事务分配单向增长的事务ID,为每个修改保存一个版本,版本与事务ID关联,读操作只读该事务开始前的数据库的快照。 所以 MVCC 可以为数据库解决以下问题:
1、3个(4个)记录隐藏列字段
在创建表的时候,MySQL除了创建用户所需的列之外,还会创建3个记录隐藏列字段
例如创建并插入一条数据,实际的表结构应该是这样的:
2、undo log(撤销日志)
MySQL是以守护进程的方式,在内存中运行。undo log是MySQL中的一段内存缓冲区,用以保存日志数据。
3、模拟MVCC场景
3.1update场景
现有一个事务ID为10,对上方信息表进行update,将name由张三修改为李四:
此时又有一个事务11,需要对信息表的记录进行update,将李四那一行的年龄修改为30:
undo log中的一个个版本,被称为快照。除了版本链之外,还可以通过记录相反sql的方式,以备数据的回滚(比如delete数据,日志可以保存insert数据)
3.2delete场景
删除数据不是清空,只需将隐藏的flag标志位设置为删除即可。也可以形成版本。
3.3insert
insert是插入,只需插入时在undo log中记录其对应的delete语句即可,回滚时只需执行这些delete语句。如果当前的事务提交了,undo log将会删除这些备份数据。(update和delete可能有别的事务还在访问,commit之后不会立马就删除undo log的回滚数据)
3.4select场景
在MySQL的RR级别下,一个事务的写操作并不会影响另一个事务的读操作。增删改,都是对最新数据进行修改,但是读取,则可能需要读取历史的版本。
多个事务同时增删改的时候,是当前读,需要加锁,如果对select也加锁,那么隔离级别就是串行化。如果select是快照读,和增删改的当前读不影响,所以可以不用加锁,并行执行效率高。事务的隔离级别决定了select读取历史数据是当前读还是快照读。(read view是否更新)
那么如何保证不同的事务,看到不同的内容呢;先来的事务,应不应该看到后来的事务所作的修改呢?Read View进行可见性判断。
4、Read View
Read View在事务首次进行快照读的时候由MySQL生成,记录并维护系统当前活跃事务的ID。Read View 在 MySQL 源码中,就是一个类,本质是配合MVCC用来判断哪些快照我能看到,那些快照我看不到。
当某个事务执行select快照读的时候,MySQL会对其new一个对象,用其内部的条件来判断当前事务能够看到哪个版本的数据,可见的数据既能是当前最新的数据,也有可能是该行记录的 undo log 里面的某个版本的数据,这由隔离级别决定。
下面是 ReadView 简化的结构体:
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;//ids_t集合类型
/** 配合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
那么哪些数据能被事务读到,那些数据事务看不到呢?见下图:
总结一下:举个例子,我是学弟,我能看到比我早入学的学长的找工作的数据,但是学长看不到后入学的我找工作的数据。同理在形成快照的时候,我能看到:
我看不到:
如果查到不应该看到当前版本,接下来就是遍历下一个版本,直到符合条件。
此时的undo log中的版本链:
在事务2对改行进行快照读的时候,按照undo log中快照的先后版本,依次遍历,得出本次我对该行的快照读应该读取到哪一个版本的快照。
5、RR和RC的区别
5.1当前读和快照读在RR级别下的区别
准备工作:
--将全局隔离级别设置为可重复读(需重启)
mysql> set global transaction isolation level REPEATABLE READ;
Query OK, 0 rows affected (0.00 sec)
--创建一张表
mysql> create table if not exists account(
-> id int primary key,
-> age int not null,
-> name varchar(20) not null
-> )ENGINE=InnoDB DEFAULT CHARSET=UTF8;
Query OK, 0 rows affected (0.26 sec)
--插入一条数据
mysql> insert into account values (1,18,'张三');
Query OK, 1 row affected (0.04 sec)
例一:root在jly修改前快照读
使用者:jly
--1、启动事务
mysql> begin;
Query OK, 0 rows affected (0.01 sec)
--2、进行快照读
mysql> select* from account;
+----+-----+--------+
| id | age | name |
+----+-----+--------+
| 1 | 18 | 张三 |
+----+-----+--------+
1 row in set (0.00 sec)
--3、更新数据,修改id为1的字段的年龄为20
mysql> update account set age=20 where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
--4、对事务进行提交
mysql> commit;
Query OK, 0 rows affected (0.04 sec)
使用者:root
--当上方用户执行完第一步时,root同时启动事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
--当上方用户执行完第二步时,root同时进行快照读,读取的结果一样
mysql> select* from account;
+----+-----+--------+
| id | age | name |
+----+-----+--------+
| 1 | 18 | 张三 |
+----+-----+--------+
1 row in set (0.01 sec)
--当上方用户执行完第三步时,root进行快照读,发现年龄的修改并没有被读到
mysql> select* from account;
+----+-----+--------+
| id | age | name |
+----+-----+--------+
| 1 | 18 | 张三 |
+----+-----+--------+
1 row in set (0.00 sec)
--当上方用户执行完第四步提交事务时,root再次进行快照读,发现年龄的修改还是没有被读到
mysql> select* from account;
+----+-----+--------+
| id | age | name |
+----+-----+--------+
| 1 | 18 | 张三 |
+----+-----+--------+
1 row in set (0.00 sec)
--但是此时root使用当前读,使能够读到年龄的修改的
mysql> select* from account lock in share mode;
+----+-----+--------+
| id | age | name |
+----+-----+--------+
| 1 | 20 | 张三 |
+----+-----+--------+
1 row in set (0.01 sec)
例二:root在jly修改后快照读
使用者:jly
--1、启动事务
mysql> begin;
Query OK, 0 rows affected (0.01 sec)
--2、进行快照读
mysql> select* from account;
+----+-----+--------+
| id | age | name |
+----+-----+--------+
| 1 | 20 | 张三 |
+----+-----+--------+
1 row in set (0.00 sec)
--3、更新数据,修改id为1的字段的年龄为30
mysql> update account set age=30 where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
--4、提交事务
mysql> commit;
Query OK, 0 rows affected (0.03 sec)
使用者:root
--1、同时启动事务
mysql> begin;
Query OK, 0 rows affected (0.01 sec)
--当上方用户执行完第四步提交事务时,root进行快照读,发现读到的数据是被修改过的
mysql> select* from account;
+----+-----+--------+
| id | age | name |
+----+-----+--------+
| 1 | 30 | 张三 |
+----+-----+--------+
1 row in set (0.00 sec)
通过例一可以发现:root的select在jly提交之前,读到的是修改前的数据;
通过例二可以发现:root的select在jly提交之后,读到的是修改后的数据。
5.2MySQL对四种隔离级别的不同处理方式
三、写写场景
直接理解成当前读。