InnoDB并发机制

InnoDB锁机制

InnoDB默认使用行锁,实现了两种标准的行锁--共享锁与排他锁;

行锁类型锁功能锁兼容性加锁释放锁
共享锁(读锁、S锁)允许读取共享锁的事务读数据与共享锁兼容,与排他锁不兼容只有SerializaWe 隔离级别会默认为:读加共享锁;其他隔离级别下,可显示使用select ... lock in share model为读加共享锁在事务提交或回滚后会自动同时释放锁;除了使用start transaction 的方式显示开启事务,InnoDb也会自动为增删改查语句开启事务,并自动提交或回滚;(autocommit=1)
排他锁(写锁、X锁)允许获取排它锁的事务更新或删除数据与共享锁不兼容,于排他锁不兼容在默认的Reapeatable Read隔离级别下,InnoDb会自动为增删改查操作的行加排它锁;也可显式使用select ... for update为读加排它锁
  1. 除了显式加锁的情况,其他情况下的加锁与解锁都无需人工干预
  2. innodb所有的行锁算法都是基于索引实现的,锁定的也都是索引或索引区间

当前读&快照读

当前读:即加锁读,读取记录的最新版本,会加锁保证其他并发事务不能修改当前记录,直至获取锁的事务释放锁;使用当前读的操作主要包括:显示加锁的读操作与插入/更新/删除等写操作,如下所示:

select * from table where ? lock in share mode;
select * from table where ? for update;
insert into table values (...);
update table set ? where ?;
delete from table where ?;

注:当update sql被发给mysql 后,mysql server 会根据where 条件,读取第一条满足条件的记录,然后InnodDB引擎会将第一条记录返回,并加锁,待mysql server收到这条加锁的记录之后,会再发起一个update请求,更新这条记录。一条记录操作完成,再读取下一条记录,直至没有满足条件的记录为止。因此,Update操作内部,就包含了当前读。同理,delete 操作也一样。Insert操作会稍微有些不同,简单的说,就是insert操作可能触发Unique Key的冲突检查,也会进行一个当前读。

快照读:即不加锁,读取记录的快照版本而非最新版本,通过mvcc实现; Innodb默认的rr(repeatable-read可重复读),不显示加lock in share mode与for update的select 操作都属于快照读,保证事务执行过程中只有第一次读之前提交的修改和自己的修改可见,其他的均不可见;

共享锁和独占锁

意向锁

innodb支持多粒度的锁,允许表级锁和行级锁共存。一个类似于lock table ... write 的语句会获得这个表的x锁。为了实现多粒度锁,innodb使用了意向锁(简称I锁)。I锁是表明一个事务稍后要获得针对一行记录的某种锁(sorx)的对应表的表级锁,有两种:

  • 意向排它锁(简称IX锁)表明一个事务意图在某个表中设置某些行的x锁
  • 意向共享锁(简称IS锁)表明一个事务意图在某个表中设置某些行的s锁

select ... lock in share mode 设置一个IS锁,select ... for update设置一个IX锁。意向锁的原则如下:

  • 一个事务必须先持有该表上的IS或者更强的锁才能持有该表中某行的S锁
  • 一个事务必须先持有该表上的IX锁才能持有该表中某行的X锁

新请求的锁只有兼容已有锁才能被允许,否则必须等待不兼容的已有锁被释放。一个不兼容的锁请求不被允许是因为它会引起死锁,错误会发生。意向锁只会阻塞全表请求(比如lock tables ... write)。意向锁的主要目的是展示某人正在锁定表中的一行,或者将要锁定一行。

Record Lock

记录锁(Record Lock)是加到索引记录上的锁,假如我们存在下面一张表users:

    CREATE TABLE users(
        id INT NOT NULL AUTO_INCREMENT,
        last_name VARCHAR(255) NOT NULL,
        first_name VARCHAR(255),
        age INT,
        PRIMARY KEY(id),
        KEY(last_name),
        KEY(age)
    );

如果我们使用id或者last_name作为sql中where 语句的过滤条件,那么InnoDb就可以通过索引建立的B+树找到行记录并添加索引,但是如果使用first_name作为过滤条件时,由于InnoDB不知道待修改的记录具体存放的位置,也无法对将要修改哪条记录提前做出判断就会锁定整个表。

Gap Lock

记录锁是存储在存储引擎中最为常见的锁,除了记录锁之外,Innodb中还存在间隙锁(gap lock),间隙锁是对索引记录中的一段连续区域的锁;当使用类似

select * from users where id between 10 and 20 for update;

的sql语句时,就会阻止其他事务向表中插入id=15的记录,因为整个范围都被间隙锁锁定了。

间隙锁是存储引擎对于性能和并发做出的权衡,并且只用于某些事务隔离级别。

虽然间隙锁中也分为共享锁和互斥锁,不过他们之间并不是互斥的,也就是不同的事务可以同时持有一段相同范围的共享锁和互斥锁,它唯一阻止的就是其他事务向这个范围中添加新的记录

间隙锁的缺定

  • 间隙锁有个比较致命的弱点,就是当锁定一个范围键值之后,即使某些不存在的键值也会被无辜的锁定,而造成在锁定的时候无法插入锁定键值范围内的任何数据。在某些场景下这可能对性能造成很大的危害
  • 当query无法利用索引的时候,Innodb会放弃使用行级别锁定而改用表级别的锁定,造成并发性能低下;
  • 当query使用的索引并不包含所有过滤条件的时候,数据检索使用到的索引键所指向的数据可能有部分并不属于该query的结果集行列,但是也会被锁定,因为间隙锁锁定的是一个范围,而不是具体的索引键
  • 当query 在使用索引定位数据的时候,如果使用的索引键一样但访问的数据行不同的时候(索引只是过滤条件的一部分),一样会被锁定。

Next-Key Lock

Next-key锁相比前两者就稍微有点复杂,它是记录锁和记录前的间隙锁的结合,在users表有以下记录:

idlast_namefirst_nameage
4starktony21
1tomhideleston30
3morganfreeman40
5jeffdean50
2donaldtrump80

如果使用Next-key锁,那么Next-key锁就可以在需要的时候锁定以下的范围:

    (-∞, 21]
    (21, 30]
    (30, 40]
    (40, 50]
    (50, 80]
    (80, ∞)

既然叫next-key锁,锁定应该是当前值和后面的范围,但是实际并不是,Next-key锁锁定的事当前值和前面的范围。

当我们更新一条记录,比如select * from users where age=30 for update;innodb不仅会在范围(21,30]上加next-key锁,还会在这条该记录索引增加的方向的范围(30,40]加间隙锁,所以插入(21,40]范围内的记录都会被锁定。

Next-key锁的作用其实是为了解读幻读的问题。

插入意向锁

插入意向锁是在插入一行记录操作之前设置的一种间隙锁,这个锁释放了一种插入方式的信号,亦即多个事务在相同的索引间隙插入时如果不是插入间隙中相同的位置就不需要互相等待。假设有索引值4、7,几个不同的事务准备插入5、6,每个锁都在获得插入行的独占锁之前用插入意向锁各自锁住了4、7,但是不阻塞对方因为插入行不冲突。

自增锁

自增锁是一个特殊的表级锁,事务插入自增列的时候需要获取,最简单情况下如果一个事务插入一个值到表中,任何其他事务都要等待,这样第一个事务才能获得连续的主键值。

锁选择

idname
1title1
2title2
3title3
9title9
10title10

按照原理来说,id>5 and id<7 这个查询条件,在表中找不到满足条件的项,因此会对第一个不满足条件的项(id=9)上加上gap锁,防止后续其他事务插入满足条件的记录。

而Gap锁与Gap锁是不冲突的,那么为什么两个同时执行id>5 and id<7查询的事务会冲突呢?

原因在于,Mysql Server并没有将id<7这个查询条件下降到innodb引擎层,因此InnoDb看到的查询,是id>5,正向扫描。读出的记录id=9,先加上next key锁(Lock X+Gap lock),然后返回给MySql Server进行判断。Mysql server此时才会判断返回的记录是否满足id<7的查询条件。此处不不满足,查询结束。

因此,id=9记录上,真正持有的锁是next key,而next key锁之间是相互冲突的,这也说明了为什么两个id>5 and id<7查询的事务会冲突的原因。

MVCC

InnoDb引擎支持Mvcc(Multiversion COncurrency Control):InnoDb保存了行的历史版本,已支持事务的并发控制和回滚。这些历史信息保存在表空间的回滚段(Rollback Segement)里,回滚段中存储着Undo Log。当事务需要进行回滚时,Innodb就会使用这些信息来进行Undo操作,同时这些信息也可以来实现一致性读。

Innodb在存储的每行数据中都增加了三列隐藏属性:

  • DB_TRX_ID:最后一次插入或更新的事务id
  • DB_ROLL_PTR:指向已写入回滚段的Undo Log记录。如果这行记录是更新的,那么就可以根据这个Undo Log记录重建之间的数据
  • DB_ROW_ID:自增序列,如果表未指定主键,则由该列作为主键

在回滚段的Undo Log被分为Insert Undo Log和Update Undo Log。Insert Undo Log只是在事务回滚的时候需要,在事务提交后就可丢弃。Update Undo Log 不仅仅在回滚的时候需要,还要提供一致性读,所以只有在所有需要该Update Undo Log构建历史版本数据的事务都提交后才能丢弃。Mysql建议尽量频繁的提交事务,这样可以保证InnoDb快速的丢弃Update Undo Log,防止其过大。

在InnoDB中,行数据的物理删除不是立刻执行,Innodb会在行删除的Undo Log被丢弃时才会进行物理删除。这个过程被称之为清理(Purge),其执行过程十分迅速。

MVCC二级索引

InnoDB在更新时对二级索引和聚集索引的处理方式不一样。在聚集索引上的更新是原地更新(in-place),其中的隐藏属性DB_ROLL_PTR指向Undo Log可以重建历史数据。但是二级索引没有隐藏属性,所以不能原地更新。

当二级索引的数据被更新时,旧的二级索引记录标记为标记删除(delete-marked),然后插入一条新的索引记录,最终标记删除的索引记录会被清除。当二级索引记录被标记为delete-marked或者有更新的事务更新时,InnoDb会查找聚集索引。在聚集索引中检查行的DB_TRX_ID,如果事务修改了记录,则从Undo Log中构建行数据的正确版本。如果二级索引被标记为delete-marked或者二级索引有更新的事务更新,覆盖索引技术不会被使用(获取行任意数据均需要回表)。、

mvcc vs 乐观锁

MVCC并不是一个与乐观和悲观并发控制对立的东西,它能够与两者很好的结合以增加食物的并发量,在目前最流行的SQL数据库Mysql和PostgreSql中都对MVCC进行了实现;但是由于他们分别实现了悲观锁和乐观锁,所以MVCC实现的方式也不同。

MVCC可以保证不阻塞地读到一致的数据。但是,MVCC并没有对实现细节做约束,为此不同的数据库的语义有所不同,比如:

  • postgres对写操作也是乐观并发控制;在表中保存同一行数据记录的多个不同版本,每次写操作,都是创建,而回避更新;在事务提交时,按版本号检查当前事务提交的数据是否存在写冲突,则抛异常告知用户,回滚事务;
  • innodb则只对读无锁,写操作扔是上锁的悲观并发控制,这意味着,innodb中只能见到因死锁和不变性约束而回滚,而见不到因为写冲突而回滚,不像postgres那样对数据库修改在表中创建新纪录,而是每行数据只在表中保留一份,在更新数据时上行锁,同时将旧版数据写入undo log。表和undo log中行数据都记录着事务ID,在检索时,只读取来自当前已提交的事务的行数据。

可见MVCC中的写操作仍可以按悲观并发控制实现,而CAS的写操作只能是乐观并发控制。还有一个不同在于,MVCC在语境中倾向于"对多行数据打快照平行宇宙",然而CAS一般只保护单行数据而已。比如mongodb有CAS的支持,单不能说明这是MVCC。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值