在并发访问同一条数据的时候,可能会造成数据的丢失情况。
例如:
事务A和事务B查询同一个账户,A查看余额为1000,B也查看余额为1000.
A存了100进去,提交事务之后余额为1100.而事务B取出100,提交事务之后余额变成900.
然而实际上存100,再取出100,总的额度应该还是1000的。
基于锁解决并发问题:
使用锁的机制,在事务里修改数据的时候加锁,同一时间只有一个实务可以修改当前的数据,,别的事务想要修改的话必须要等当前的事务释放锁。
MVCC(多版本并发控制):
MVCC使得在读的时候不需要加锁,(即普通的select不需要加锁),提高了数据库的并发访问能力。
在使用mvcc的情况下,如果事务A开始修改账户,但是未提交,此时事务B想要读取账户,那么此时事务B读到的是事务A修改操作之前的账户副本,但如果事务B想要修改数据就必须要等到事务A提交之后才行。
非锁定读:MVCC实现的就是非锁定读。在读/读的情况下,并发并不会发生问题。但是读/写就会出现问题。在非锁定读模式下,如果一个事务修改了数据但是未提交,另一个事务通过普通的select查询数据的时候,读取的是一个快照版本,这也被称为快照读,这种不加锁的读取方式大大的提高了数据库的并发能力。
锁定读:
例如:select... lock in share mode, select... for update
以上这些都是锁定读,会在读数据的时候加锁,读取的数据是最新的版本,所以他也可以被叫做是当前读。
InnoDB对于MVCC的实现:
MVCC是实现主要依赖于:隐藏字段,read view,undo log。
隐藏字段:
在InnoDB中,每一行数据都添加了三个隐藏字段。
1. TRX_ID 记录最后一次修改改行数据的事务ID。
2. ROLL_PTR 回滚指针,指向undo log,指向修改之前的一个版本数据。
3. ROW_ID 如果没有设置主键,那么InnoDB会用这个ID自动生成一个聚集索引。
ReadView:(读视图)
主要是用来做可见性判断的。这里面保存了当前对本事务不可见的活跃的事务。(当前别的事务做的修改,删除,但是未提交的事务被称为活跃的事务)。
low_limit :当前出现过的最大的事务ID+1,即下一个分配的事务ID,每个事务会颁发一个id,这个id是递增的。如果当前读的行的ID大于等于这个值,那么就是不可见的。
up_limit : 活跃事务列表里最小的事务ID,如果小于这个值,那么就直接是可见的。如果大于这个值但是小于最大事务I D,就需要去活跃列表里比对了。
m_ids: 一个保存了所有活跃事务的列表(未提交事务),当创建绒read view的时候,记录下当前活跃的事务,即使这些事务后期修改了数据也对当前的事务不可见。
Undo Log:(回滚日志)
回滚日志主要有两个作用:
1. 在事务回滚的时候,恢复之前的数据。
2. 在MVCC中,当读取记录的时候,如果该记录被其他事务占用或者对当前的事务不可见,就需要通过记录中的回滚指针在undo log中读取之前版本的数据,实现非锁定读。
在 InnoDB
存储引擎中 undo log
分为两种: insert undo log
和 update undo log
:
insert undo log
:指在insert
操作中产生的undo log
。因为insert
操作的记录只对事务本身可见,对其他事务不可见,故该undo log
可以在事务提交后直接删除。不需要进行purge
操作。update undo log
:update
或delete
操作中产生的undo log
。该undo log
可能需要提供MVCC
机制,因此不能在事务提交时就进行删除。提交时放入undo log
链表,等待purge线程
进行最后的删除。
数据可见性算法:
-
如果记录 DB_TRX_ID < m_up_limit_id,那么表明最新修改该行的事务(DB_TRX_ID)在当前事务创建快照之前就提交了,所以该记录行的值对当前事务是可见的
-
如果 DB_TRX_ID >= m_low_limit_id,那么表明最新修改该行的事务(DB_TRX_ID)在当前事务创建快照之后才修改该行,所以该记录行的值对当前事务不可见。跳到步骤 5
-
m_ids 为空,则表明在当前事务创建快照之前,修改该行的事务就已经提交了,所以该记录行的值对当前事务是可见的
-
如果 m_up_limit_id <= DB_TRX_ID < m_low_limit_id,表明最新修改该行的事务(DB_TRX_ID)在当前事务创建快照的时候可能处于“活动状态”或者“已提交状态”;所以就要对活跃事务列表 m_ids 进行查找(源码中是用的二分查找,因为是有序的)
-
如果在活跃事务列表 m_ids 中能找到 DB_TRX_ID,表明:① 在当前事务创建快照前,该记录行的值被事务 ID 为 DB_TRX_ID 的事务修改了,但没有提交;或者 ② 在当前事务创建快照后,该记录行的值被事务 ID 为 DB_TRX_ID 的事务修改了。这些情况下,这个记录行的值对当前事务都是不可见的。跳到步骤 5
-
在活跃事务列表中找不到,则表明“id 为 trx_id 的事务”在修改“该记录行的值”后,在“当前事务”创建快照前就已经提交了,所以记录行对当前事务可见
-
-
在该记录行的 DB_ROLL_PTR 指针所指向的
undo log
取出快照记录,用快照记录的 DB_TRX_ID 跳到步骤 1 重新开始判断,直到找到满足的快照版本或返回空
InnoDB在RC和RR的隔离级别下使用MVCC(多版本并发控制),但是生成read view的时机是不同的。在RC隔离级别下,每次查询都会生成一个最新的read view。而在RR的隔离级别下,在事务开启之后,只会生成一次read view,就是第一次select的语句。
在RR的情况下使用MVCC只能防止部分幻读的情况发生,通过维持只采用第一次select 查询到的read view ,那么只能读取到第一次查询之前所插入的数据。但是还有一种情况下,无法解决幻读的问题。当使用当前读的模式下(select xxx in share lock mode),这时候如果有别的事务插入了数据,那么两次读出来的行数都不同了。所以InnoDB采用了next-key lock 来解决这种情况。
总结:因此,我们可以通过MVCC+next-key lock 来解决在RR隔离级别下的幻读问题。
next-key lock:
gap lock 间隙锁:锁住行数据之间的间隙,防止有数据插入。
如下图所示,有6行数据,那么就有7个间隙。
当我们在执行select for update 这种当前读的操作的时候,不仅仅会给6个数据加锁,也会给这7个间隙加上锁,防止插入数据在间隙里。
next-key lock 是间隙锁+行锁的结合体。