问题

不同的事务同时操作同一条数据,会出现某个事务的操作被覆盖而导致数据丢失;
如:
image.png

image.png

LBCC解决数据丢失问题

基于锁并发控制,使用锁机制,在当前事务需要对数据修改的时候,将当前事务加上锁,同一时间只允许一条事务修改当前数据,其他事务必须等待锁释放之后才可以操作。

MVCC解决数据丢失问题

多版本并发控制,Multi-Version-Concurrency- Control
实现对数据库的并发访问,实现事务内存;

当事务B开始修改某条记录(未提交)时,事务A需要查询这条记录,这时候就会读到事务B修改操作之前的这条记录的副本数据,但是如果事务A需要对这条记录进行修改,就必须等事务B先提交事务;

使用MVCC时,数据库读操作不会加锁,普通的select请求不会加锁,提高并发处理效率;(只有写写之间会相互阻塞)

实现读提交,可重复读等隔离级别,用户可以查看当前数据的前一个或者几个历史版本,保证事务的隔离性;

InnoDB实现MVCC

通过在每行记录后面保存两个隐藏的列来实现,一个是保存行的事务ID【DB_TRX_ID】,一个是保存行的回滚指针(DB_ROLL_PT);

每次开始一个新的事务,都会自动递增产生一个新的事务id,事务开始时刻就会把事务id放到当前事务影响的行对应的事务id中,当查询时需要用当前事务id时就会和每行记录的事务id进行比较;

InnoDB中MVCC操作流程

SELECT 根据以下条件来检查每行的记录:

  1. 只会查找版本号早于当前事务版本的数据行,可以确保事务读取的行,要么是在事务开始之前已经存在,要么就是事务自身插入或者修改过的;
  2. 删除的行要事务id判断,读到事务开始之前的状态版本,只有符合这些条件的记录,才会作为查询结果;

INSERT
为新插入的每一行保存事务编号作为版本号;

DELETE
为删除的每一行保存当前事务编号作为删除标识;

UPDATE
插入的时候保存当前事务编号为版本号,同时保存当前事务编号到原来的行作为行删除标识;
保存这两个额外事务编号,让大多数读操作不用加锁,性能很好,但是每行记录都需要额外的存储空间,需要做更多的行检查工作以及额外的维护工作;

MySql中MVCC的实现

MVCC底层依赖MySql的undo log和ReadView

  1. undo log<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/2787894/1652422520130-bf3095aa-8f94-4116-a7a0-9919e9e3e0f1.png#clientId=u0d72166a-d26e-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u1bcd591f&margin=%5Bobject%20Object%5D&name=image.png&originHeight=332&originWidth=691&originalType=url&ratio=1&rotation=0&showTitle=false&size=28570&status=done&style=none&taskId=ud3d65716-ba34-44f8-b1ef-30c6081aae4&title=)
  1. undo log是逻辑日志,记录了数据库的操作(反向操作的语句),当delete一条记录的时候,undo log会记录一条对应的insert记录;update一条记录的时候,undo log会记录一条相反的update记录,当事务失败需要回滚操作时,就可以通过读取undo log中相应的内容进行回滚;比如:执行update user set name =”b”where id=1(在修改之前是name= “a”);在undo log中记录的是一条相反的update语句:update user set name = “a” where id = 1;
  2. undo log保证事务的原子性和一致性;(事务回滚)
  3. 用于MVCC快照读(顺着undo log链找到满足条件的记录行版本)
  4. 版本链:多个事务并行操作某一行数据时,不同事务对该行数据的修改会产生多个版本然后通过回滚指针(roll_pointer)连成一个链表,这个链表就是版本链。image.png

      <br />    当前读<br />    读取的记录数据是最新版本的, 显示加锁的都是当前读;
    

    快照读
    读取的是记录数据的可见版本(有旧版本),不进行加锁控制,一般来说select语句都是快照读;

    ReadView

  5. 事务在进行快照读的时候生成的记录快照,相当于一个保存事务id的list列表。记录的是本事务执行的时候,MySql还有哪些事务在执行,并且还没有提交;

  6. 显示的是可见的记录,并不会将所有的数据行都展示;
  7. 相关属性:
    1. m_ids:当前有哪些事务正在运行(不包含当前事务本身和已提交的事务),数据结构是一个list;
    2. min_trx_id:指的是m_ids中的最小值;
    3. max_trx_id:是指下一个要生成的事务id,比现在所有事务的id都要大;
    4. creator_trx_id:每开启一个事务都会生成一个ReadView,而creator_trx_id就是开启这个事务的id;(当前事务的id)
  8. 可见性规则
    1. 如果被访问的版本数据的事务id=creator_trx_id,说明当前事务访问的是自己修改过的记录,那么这个版本的数据是可见的;
    2. 如果被访问的的版本数据的事务id<min_trx_id,表示这个版本的数据事务在当前事务生成ReadView前已经提交了,所以数据是可见的;
    3. 如果被访问的版本的数据的事务id>max_trx_id,表示这个版本的数据事务在当前事务生成ReadView后才开启的,那么这个版本的数据不能被当前的事务访问,不可见;
    4. 如果被访问的版本数据的事务id在min_trx_id和max_trx_id之间,则说明该版本数据的事务是在活跃中(未提交),需要进一步判断版本事务id是不是等于creator_trx_id,如果等于则说明是当前事务自己修改的记录,可见否则不可见;如果版本不在list中,则说明版本数据的事务在当前事务生成ReadView时已经被提交了,这个版本数据可以访问,可见;
  9. ReadView何时创建:
    1. 在读提交隔离级别下,是每个select都会创建最新的ReadView;
    2. 可重复读隔离级别下,是在事务中第一个select请求才创建ReadView;(一个事务的开始和结束就是ReadView的生命周期);
    3. insert、update、delete不会创建ReadView,但是这些操作在事务开启(begin)且未提交的时候,这个事务的id会被保存到其他查询事务的ReadView记录中(放在对应ReadView的m_ids中)。

MVCC能否解决幻读

mvcc在快照读的情况下可以解决幻读问题,但是在当前读的情况下是不能解决幻读的;要加上间隙锁才能解决幻读,MVCC+next-lock 。