MVCC实现

MVCC

基本思想

通过mvcc解决读未提交、不可重复读的问题,但是要解决幻读,则需要加锁解决(next-key-locks)

  1. MVCC是乐观锁的一种实现,是通过保存数据在某一个时间点的快照实现的,写操作更新最新的版本(写操作是要加锁的),读操作读取旧版本。
  2. MVCC是实现隔离级别的一种机制,用于实现读取已提交可重复读这两种隔离级别(MVCC单单解决的是可重复读,没有解决幻读)。
  3. 大多数事务型存储引擎实现都不是简单的行锁,基于并发性的考虑,一般会同时实现多版本并发控制(MVCC)处理读写冲突。
  4. MVCC中事务的修改操作(增删改)会为行记录新增一个版本快照,并把当前事务id写入trx_id。
  5. 对于读未提交,直接读取最新版本的数据即可。
  6. 对于串行化,是使用加锁的方式访问记录。

MVCC的实现

  • 增加记录的字段
    • 在InnoDB中,聚簇索引记录中包含两个隐藏列:

      • trx_id:对记录进行改动时,trx_id会记录当前事务id,也就是当前系统版本号(每开始一个事务,系统版本号递增);
      • roll_pointer:对记录进行改动,会把旧版本记录写入undo日志(注意只是修改数据才把历史记录放到undo log中),roll_pointer指向修改之前的版本,形成一个版本链;
    • 在undo日志中,保存着每条记录的版本链(即历史版本),那么事务在读的时候,读哪一个版本呢。比如读提交隔离机制,必须读已经提交的事务所修改的数据,对于可重复读隔离机制,必须保证同一个事务两次读到的记录是一样的(尽管可能其他事务已经更新了记录,并已经提交)。为了到底读取哪个版本的问题,引入了ReadVIew

  • ReadView(快照)
    • 作用:在读提交和可重复读的隔离机制下,用来判断到底读取版本链中的哪个版本。

    • 构成

      • 活跃事务id列表:包含了对该条记录进行写操作已经开始,但是还没有提交的事务id列表
      • min_id:活跃事务id列表中最小值
      • max_id:活跃事务id列表中最大值
    • 生成时机

      • 读已提交隔离机制:在一个事务中,只要执行select都会生成一个ReadView
      • 读未提交隔离机制:在一个事务中,在第一次执行select的时候生成一个ReadView,之后的select复用第一次生成的。
  • 版本比较规则

    从版本链中的第一个版本(即最新记录)开始,使用roll_pointer挨个比较每个版本

    • 如果记录的trx_id小于min_id,那么说明该记录版本已经被提交。
    • 如果记录的trx_id大于max_id,那么说明这个记录版本是ReadVIew生成之后发生的,不能访问
    • 如果记录的trx_id在min_id和max_id之间,则判断trx_id是否在ReadView中:
      1. 如果在ReadView中,说明该事务还没有提交,该版本不能访问。
      2. 如果不再ReadView中,说明该事务已经提交,可以访问。
  • 快照读和当前读
    • 快照读:MVCC中的SELECT操作是读取快照中的数据,不需要进行加锁;

    • 当前读:MVCC中修改数据的操作(增删改)需要进行加锁操作,从而读取最新的数据;

例子

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DJtEVgZO-1618648744655)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210416102001536.png)]

  1. 如上图,当前事务A对该记录进行了修改,其事务id为100。那么历史版本被保存到undo log中,并用roll_pointer指向历史版本。注:此时其他事务只能读该条记录,如果对该记录进行写的话,写操作会被挂起,因为事务A在对该记录进行写时,会对该记录加锁。
  2. 此时B事务执行select,假设B事务的id为120。此时B事务会生成ReadView,保存的列表为{120, 100},min_id=100,max_id=120然后根据版本读取规则,遍历版本链,找到读取的那个版本。
  3. 首先找到trx_id=100的版本,其在ReadView的列表中,所以还没有提交,故不可以读。
  4. 如果找到trx_id=60的版本,其小于min_id,所有该版本对应的事务已经提交,故读取成功。
  5. 当A事务提交之后,B事务再执行select。那么根据隔离级别会有所不同:
    • 若是读提交隔离机制:那么事务B会再次生成ReadView,保存的列表为{120},min_id=120,max_id=120。那么根据版本链,首先读到trx_id=100的版本,小于min_id,说明已经提交,可以读。
    • 若是可重复读隔离机制:还是使用第一次生成的ReadView,列表{120, 100}, min_id=100,max_id=120。然后根据还是读到trx_id=60的版本。

索引

第一组分类
  1. 聚簇索引:在B+树的叶子节点保存了完整的记录。注意一个表中最多只能有一个聚簇索引,这个聚簇索引一般建立在主键上,mysql会自己会主键创建一个聚簇索引。CREATE CLUSTERED INDEX IndexTestTable_index_uniquecode ON IndexTestTable(UniqueCode1,…)。
  2. 辅助索引:在B+树的叶子节点保存了主键+索引属性。如果本次是覆盖索引(即查询的是主键+索引属性的子集)那么不用回表。否则会根据查询到的主键在主键索引中再次查询。CREATE NONCLUSTERED INDEX IndexTestTable_index_name ON IndexTestTable(Name1, …)。
第二组分类
  1. 普通索引:允许索引的数据列包含重复的值。如果有两个重复的值,那么得把主键加上去。建立普通索引的目的是加快查询速度。CREATE INDEX ind_user_info_name ON user_info(name,…);
  2. 唯一索引:索引的数据列的值不能重复。即主键索引就是唯一索引。建立唯一索引的好处:(1)简化了索引管理工作,不存在在插入一条记录时,该记录会插入到之前的页中,即减少索引的维护工作。(2)保证数据的唯一性,在插入数据时,会自动检查新记录的这个字段的值是否已经存在,如果存在则拒绝插入。往往建立唯一索引就是为了避免数据的重复。CREATE UNIQUE INDEX uni_user_info_pass ON user_info(pass,…);
第三组分类
  1. 复合索引:即由多个属性列组合成一个索引。

默认情况下,InnoDB默认对update\delete\insert自动加排他锁,对于普通的select语句,不加锁。

事务并发访问同一条记录时,可能发生这三种情况:读-读、写-写、读-写

  • 读-读:并发事务同时访问同一条记录。由于读不会对记录产生影响,因此是允许同时读的。
  • 写-写:并发事务同时修改同一条记录。这种情况可能导致脏写问题,因此是不允许的,故需要通过加锁实现。即事务对某条记录修改时,先加锁。其他访问该记录的事务必须排队。当事务提交或者回滚时,释放锁。
  • 读-写:并发事务一个事务正在读,一个事务要进行写。这可能导致脏读不可重复读幻读解决方案是,读操作利用MVCC(乐观锁),写操作进行加锁

锁粒度

根据锁的作用范围(即锁的粒度),分为:

  1. 行锁:对一条记录进行加锁。
  2. 表锁:对整个表进行加锁。

锁的分类

为了实现读-读之间不收影响;并且写-写、读-写之间能够相互阻塞,引入了共享锁(即读锁)、排他锁(即写锁)。

  1. 共享锁:事务要读取一条记录记录时,对该记录加共享锁。共享锁可以被多个事务同时持有。加锁方式select…lock in share mode,即在读的时候对该记录加共享锁。
  2. 排他锁:在事务要修改一条记录时候(update、insert、delete),给该记录加排他锁。排他锁只能一个事务持有。排他锁的加锁方式有两种:(1)自动加锁,即对记录进行update、insert、delete时,默认加上排他锁。(2)通过…for update手动加。
  3. 意向锁:当给一条记录加上共享锁、或排他锁之前。数据库会自动给记录所在的表分别加上意向共享锁、意向排他锁。意向锁的作用:可以认为是共享锁和排他锁在表上的标识,通过意向锁可以快速判断表的记录是否被加锁,从而避免遍历表中的所有记录,看这些记录有没有上锁。如当要对表加排他锁时,直接根据意向锁,就可以判断表中记录是否存在排他锁和共享锁。

InnoDB中的行级锁

InnoDB的行锁,就是通过锁住索引来实现的

  • 读取的方式:

    • 快照读:通过MVCC可以解决读未提交(脏读)、不可重复读。但实际上这只是解决了普通select语句的数据读取问题。事务利用MVCC进行的读取称为快照度,所有普通的select语句在读提交、可重复读隔离级别下都算是快照度。
  • 锁定读:即在读取的时候对记录加锁—行级锁。

  • 行级锁的分类

    1. 记录锁(record locks):所谓记录就是聚簇索引中真实存在的数据,记录锁就是把某行数据锁住。

    2. 间隙锁(gap locks用来解决幻读问题,在读提交隔离机制下会失效):间隙指的是两条记录之间逻辑上尚未填入的记录,间隙锁就是锁定某些间隙区间。

      当进行等值查询或者范围查询时,并且没有命中任何一条记录时,就会把对应的间隙区间锁定(防止幻读)。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H5ufHN8O-1618648744656)(F:\自学相关\MySQL\mvcc_锁_索引\MVCC.assets\image-20210417153243333.png)]

      如上图,1 、4、7、10就是记录。没两条记录中间的逻辑部分就是间隙,当执行select * from t where id =3 for update(等值查询);或者执行select * from t where id > 1 and id < 4 for update(范围查询)。由于这两个语句都没命中,故都会把间隙(1,4)锁住。

    3. 临键锁(next-key locks,用来解决幻读问题,在读提交隔离机制下会失效):临键指间隙加其右边的记录,构成的左开右避的区间,起始就是记录锁+间隙锁,除了锁住记录,还要锁住记录的间隙。如(1,4]、(7, 10]。

      当使用范围查询,并且命中部分记录,此时锁住的就是临键区间,注临键锁锁住的区间会包含最后一条记录的右边的临键区间。如执行select * from t where id > 5 and id <= 7 for update。会锁住(4, 7]、(7,无穷)

      临键锁是默认使用的行锁。当使用唯一索引,等值匹配到一条记录时,临建锁退化为记录锁;没有匹配到任何记录时,退化为间隙锁

InnoDB的行级锁到底锁住的是什么

https://blog.csdn.net/qq_33762302/article/details/114048569

  • InnoDB的行锁,就是通过锁住聚簇索引。如果加锁查询时没有使用过索引,会将整个聚簇索引都锁住(即锁住整个表),(当没有显示构建聚簇索引时,InnoDB会自动构建一个聚簇索引,具体见下面分析)

    若一个表有以下聚簇索引,查询select * from t1 where id = 15 for update。那么锁定的就是如下的索引。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mSHyVIrV-1618648744658)(F:\自学相关\MySQL\mvcc_锁_索引\MVCC.assets\image-20210417111112916.png)]

  • 那么当表中没有显示构建聚簇索引时,InnoDB会自动创建一个聚簇索引:

    1. 如果表定义了主键,则InnoDB会选择主键作为聚集索引。

    2. 如果没有显示定义主键,则InnoDB会选择一个不包含NULL值的唯一索引作为主键索引。

    3. 如果没有这样的唯一索引,则InnoDB会选择内置6字节长的RowID作为隐藏的聚集索引,RowID自增。

      在select语句中后加了for update,在没有走聚簇索引的情况下,会进行全表扫描,InnoDB会把聚集索引都锁住。即其他事务就不能访问和修改表中的任何记录。

  • 如果通过辅助索引给数据行加锁,主键索引也会被锁主。(因为有回表的过程)

    在辅助索引中存的是索引+主键。主键索引中存的是完整的数据。当通过索引查询到记录ID时,通过主键索引找到完整的记录。索引索然是对辅助索引加锁,但是因为有回表过程,过也需要给主键索引加锁。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kUoBRjHM-1618648744660)(F:\自学相关\MySQL\mvcc_锁_索引\MVCC.assets\image-20210417114020643.png)]

并发事务下产生死锁

  • 产生死锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mQHz2mQ3-1618648744661)(F:\自学相关\MySQL\mvcc_锁_索引\MVCC.assets\image-20210417162100803.png)]

如上图所示,事务A和事务B并发执行。事务A更新id=1的记录,因此要先获得排他锁。同时事务B更新id=2的记录,获得了该记录的排他锁。此时A事务要更新id=2的记录,但是该记录被事务B加锁,故事务A被阻塞;而事务B更新id=1的记录,但是该记录的锁被事务A获取了,故被阻塞。这样AB两个事务都在等待彼此的资源被释放,就形成了循环等待,造成了死锁。

  • 解除死锁
    1. 设置超时时间参数set global innodb_lock_wait_timeout=50(默认为50s),或者在my.cnf中配置,即在该时间内没有获得锁,就会报错,并把自己获得的锁释放。
    2. 将参数 innodb_deadlock_detect 设置为 on,发起死锁检测,当发现死锁后,主动回滚死锁链条中的某一个事务(即通过回滚来释放锁),让其它事务得以继续执行。

nodb_lock_wait_timeout=50(默认为50s),或者在my.cnf中配置,即在该时间内没有获得锁,就会报错,并把自己获得的锁释放。
2. 将参数 innodb_deadlock_detect 设置为 on,发起死锁检测,当发现死锁后,主动回滚死锁链条中的某一个事务(即通过回滚来释放锁),让其它事务得以继续执行。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值