脏读、幻读和不可重复读的概念
- 脏读:一个事务中访问到了另外一个事务未提交的数据。 当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
- 不可重复读:一个事务查询同一条记录2次,得到的结果不一致。 是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
- 幻读:一个事务查询2次,得到的记录条数不一致。 幻读是不可重复读的一种特殊场景。第一个事务在进行第一次和第二次查询的间隔时间内,第二个事务添加或删除了对应的数据,第一个事务两次查询的条数不一致,就好像发生了幻觉一样。
事务隔离级别
- 读未提交(read uncommitted) 是指,一个事务还没提交时,它做的变更就能被别的事务看到。通俗理解,别人改数据的事务尚未提交,我在我的事务中也能读到。
- 读提交(read committed) 是指,一个事务提交之后,它做的变更才会被其他事务看到。通俗理解,别人改数据的事务已经提交,我在我的事务中才能读到。
- 可重复读(repeatable read) 是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。通俗理解,别人改数据的事务已经提交,我在我的事务中也不去读。MySQL InnoDB存储引擎默认支持的隔离级别是可重复读。
- 串行化(serializable ),顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。通俗理解,我的事务尚未提交,别人就别想改数据。
- 若隔离级别是 “读未提交”,则
V1
的值就是2。这时候事务B虽然还没有提交,但是结果已经被A看到了。因此V2、V3
也都是2。 - 若隔离级别是 “读提交”,则
V1
是1,V2
的值是2。事务B的更新在提交后才能被A看到。所以,V3
的值也是2。 - 若隔离级别是 “可重复读”,则
V1、V2
是 1,V3
是 2。之所以V2
还是 1,遵循的就是这个要求:事务在执行期间看到的数据前后必须是一致的。 - 若隔离级别是 “串行化”,则在事务A执行“查询得到值1”的时候会加读锁。直到事务A提交后,事务B才可以继续执行。所以从A的角度看,
V1、V2
值是 1,V3
的值是 2。
MVCC
MVCC英文全称为Multi-Version Concurrency Control,翻译为中文即多版本并发控制。通过读取指定版本的历史记录,并通过一些手段保证读取的记录值符合事务所处的隔离级别,在不加锁的情况下解决读写冲突。 🔗
对于使用InnoDB
存储引擎的表来说,聚集索引记录中都包含下面2个必要的隐藏列:
trx_id
:一个事务每次对某条聚集索引记录进行改动时,都会把该事务的事务id
赋值给trx_id
隐藏列。
roll_pointer
:每次对某条聚集索引记录进行改动时,都会把旧的版本写入undo日志
中。这个隐藏列就相当于一个指针,通过他找到该记录修改前的信息。
为了判断版本链中哪个版本对当前事务是可见的,MySQL设计出了ReadView
的概念。4个重要的内容如下:
m_ids
:在生成ReadView时,当前系统中活跃的事务id列表min_trx_id
:在生成ReadView时,当前系统中活跃的最小的事务id,也就是m_ids中的最小值max_trx_id
:在生成ReadView时,系统应该分配给下一个事务的事务id值creator_trx_id
:生成该ReadView的事务的事务id
当对表中的记录进行改动时,执行insert
,delete
,update
这些语句时,才会为事务分配唯一的事务id,否则一个事务的事务id值默认为0。
max_trx_id
并不是m_ids
中的最大值,事务id是递增分配的。比如现在有事务id为1,2,3这三个事务,之后事务id为3的事务提交了,当有一个新的事务生成ReadView
时,m_ids
的值就包括1和2,min_trx_id
的值就是1,max_trx_id
的值就是4。
mvcc判断版本链中哪个版本对当前事务是可见的过程
执行过程如下:
- 如果被访问版本的
trx_id = creator_id
,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问; - 如果被访问版本的
trx_id < min_trx_id
,表明生成该版本的事务在当前事务生成ReadView
前已经提交,所以该版本可以被当前事务访问; - 被访问版本的
trx_id >= max_trx_id
,表明生成该版本的事务在当前事务生成ReadView
后才开启,该版本不可以被当前事务访问; - 被访问版本的
trx_id
是否在m_ids
列表中- 是,创建
ReadView
时,该版本还是活跃的,该版本不可以被访问。顺着版本链找下一个版本的数据,继续执行上面的步骤判断可见性,如果最后一个版本还不可见,意味着记录对当前事务完全不可见 ; - 否,创建
ReadView
时,生成该版本的事务已经被提交,该版本可以被访问。
- 是,创建
MVCC实现读提交
MVCC实现读提交(Read Committed),每次读取数据前都生成一个ReadView。
建立数据表:
CREATE TABLE `girl` (
`id` int(11) NOT NULL,
`name` varchar(255),
`age` int(11),
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
下面是3个事务执行的过程,一行代表一个时间点:
时间点为5时select语句的执行过程:
- 系统中有两个事务
id
分别为100,200的事务正在执行。 - 执行select时生成一个ReadView,
mids = [100,200]
,min_trx_id = 100
,max_trx_id = 201
,creator_trx_id = 0
(select事务没有执行更改操作,事务id默认为0)。 - 最新版本的
name
列为西施,该版本trx_id
值为100,在mids
列表中,不符合可见性要求,根据roll_pointer
跳到下一个版本。 - 下一个版本的
name
列王昭君,该版本的trx_id
值为100,也在mids
列表内,因此也不符合要求,继续跳到下一个版本。 - 下一个版本的
name
列为貂蝉,该版本的trx_id
值为10,小于min_trx_id
,因此最后返回的name
值为貂蝉。
时间点为8时select语句的执行过程:
- 系统中有一个事务
id
为200的事务正在执行(事务id
为100的事务已经提交)。 - 执行select语句时生成一个ReadView,
mids = [200]
,min_trx_id = 200
,max_trx_id = 201
,creator_trx_id = 0
。 - 最新版本的
name
列为杨玉环,该版本trx_id
值为200,在mids
列表中,不符合可见性要求,根据roll_pointer
跳到下一个版本。 - 下一个版本的
name
列为西施,该版本的trx_id
值为100,小于min_trx_id
,因此最后返回的name
值为西施。 - 当事务
id
为200的事务提交时,查询得到的name
列为杨玉环。