一、mysql的锁类型
(1) 共享/排它锁(Shared and Exclusive Locks)
共享锁和排他锁是InnoDB引擎实现的标准行级别锁。
拿共享锁是为了让当前事务去读一行数据。
拿排他锁是为了让当前事务去修改或删除某一行数据。。
设置共享锁:select * from user where id = 1 LOCK IN SHARE MODE;
设置排他锁:select * from user where id = 1 FOR UPDATE;
(2) 意向锁(Intention Locks)
意向锁存在的意义在于,使得行锁和表锁能够共存。
意向锁是表级别的锁,用来说明事务稍后会对表中的数据行加哪种类型的锁(共享锁或独占锁)。
当一个事务对表加了意向排他锁时,另外一个事务在加锁前就会通过该表的意向排他锁知道前面已经有事务在对该表进行独占操作,从而等待。
为什么没有意向锁的话,表锁和行锁不能共存?
举个粟子(此时假设行锁和表锁能共存): 事务A锁住表中的一行(写锁)。事务B锁住整个表(写锁)。
但你就会发现一个很明显的问题,事务A既然锁住了某一行,其他事务就不可能修改这一行。这与”事务B锁住整个表就能修改表中的任意一行“形成了冲突。所以,没有意向锁的时候,行锁与表锁共存就会存在问题!
意向锁是如何让表锁和行锁共存的?
有了意向锁之后,前面例子中的事务A在申请行锁(写锁)之前,数据库会自动先给事务A申请表的意向排他锁。当事务B去申请表的写锁时就会失败,因为表上有意向排他锁之后事务B申请表的写锁时会被阻塞。
所以,意向锁的作用就是:
当一个事务在需要获取资源的锁定时,如果该资源已经被排他锁占用,则数据库会自动给该事务申请一个该表的意向锁。如果自己需要一个共享锁定,就申请一个意向共享锁。如果需要的是某行(或者某些行)的排他锁定,则申请一个意向排他锁。
说明:意向锁之间都是兼容的,之前是我看错了,Im so sorry参考:https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html#innodb-intention-locks
意向锁是表锁还是行锁?
首先可以肯定的是,意向锁是表级别锁。意向锁是表锁是有原因的。
当我们需要给一个加表锁的时候,我们需要根据意向锁去判断表中有没有数据行被锁定,以确定是否能加成功。如果意向锁是行锁,那么我们就得遍历表中所有数据行来判断。如果意向锁是表锁,则我们直接判断一次就知道表中是否有数据行被锁定了。
(3) 记录锁(Record Locks)
记录锁是索引记录上的锁,例如:SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE;会阻止其他事务对c1=10的数据行进行插入、更新、删除等操作。
记录锁总是锁定索引记录。如果一个表没有定义索引,那么就会去锁定隐式的“聚集索引”。
(4) 间隙锁(Gap Locks)
间隙锁是一个在索引记录之间的间隙上的锁。
一个间隙可能跨越单个索引值、多个索引值,甚至为空。
对于使用唯一索引 来搜索唯一行的语句,只加记录锁不加间隙锁(这并不包括组合唯一索引)。
(5) 临键锁(Next-key Locks)
Next-Key Locks是行锁与间隙锁的组合。当InnoDB扫描索引记录的时候,会首先对选中的索引记录加上记录锁(Record Lock),然后再对索引记录两边的间隙加上间隙锁(Gap Lock)。
(6) 插入意向锁(Insert Intention Locks)
插入意向锁是在数据行插入之前通过插入操作设置的间隙锁定类型。
如果多个事务插入到相同的索引间隙中,如果它们不在间隙中的相同位置插入,则无需等待其他事务。例如:在4和7的索引间隙之间两个事务分别插入5和6,则两个事务不会发冲突阻塞。
(7) 自增锁(Auto-inc Locks)
自增锁是事务插入到有自增列的表中而获得的一种特殊的表级锁。如果一个事务正在向表中插入值,那么任何其他事务都必须等待,保证第一个事务插入的行是连续的自增值。
二、锁的实现方式
InnoDB行锁是通过给索引加锁来实现的,如果没有索引,InnoDB会通过隐藏的聚簇索引来对记录进行加锁(全表扫描,也就是表锁)。
但是,为了效率考量,MySQL做了优化,对于不满足条件的记录,会放锁,最终持有的,是满足条件的记录上的锁。但是不满足条件的记录上的加锁/放锁动作是不会省略的。所以在没有索引时,不满足条件的数据行会有加锁又放锁的耗时过程。
索引分为主键索引和非主键索引两种。如果一条sql语句操作了主键索引,MySQL就会锁定对应主键索引;如果一条语句操作了非主键索引,MySQL会先锁定非主键索引,再锁定对应的主键索引。
三、mysql锁在4种事务隔离级别里的应用
事务的四种隔离级别有:
- 读未提交(Read Uncommitted) 此时select语句不加任何锁。此时并发最高,但会产生脏读。
- 读提交(Read Committed, RC) 普通select语句是快照读 update语句、delete语句、显示加锁的select语句(select … in share mode 或者 select … for update) 等,除了在外键约束检查和重复键检查时会封锁区间,其他情况都只使用记录锁;
-
可重复读(Repeated Read, RR)
普通select语句也是快照读
update语句、delete语句、显示加锁的select语句(select … in share mode 或者 select … for update)则要分情况:
- 在唯一索引上使用唯一的查询条件,则使用记录锁。如: select * from user where id = 1;其中id建立了唯一索引。
- 在唯一索引上使用 范围查询条件,则使用间隙锁与临键锁。如: select * from user where id >20;
- 串行化(Serializable) 此时所有select语句都会被隐式加锁:select … in share mode.
四、快照读、当前读
要理解前面四种隔离级别的加锁方式,对于MVCC、快照读、当前读 都是必须要理解的。
MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。
快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。
当前读,读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。
什么是多版本并发控制(MVCC:multi-version concurrency control )
-
MVCC定义:多版并发控制系统。可认为是行级锁的一个变种,它能够避免更多情况下的加锁操作。
-
作用:避免一些加锁操作,提升并发性能。
-
实现:通过在每行记录的后面保存行的创建时间和过期时间或删除时间(它们是隐藏的),这两个时间实际都是系统的版本号。每开始一个新的事务,版本号都会自动增加。
-
具体原理
4.1) select:innoBD查询时会检查以下两个条件:一个是数据行的版本号早于当前事务的版本号;另一个是行的删除版本号,要么没有,要么大于当前事务的版本号。4.2)insert/delete:innoDB将当前的系统版本号作为新插入(删除)的数据行的版本号。
4.3)update:先新插入一行数据,并将当前系统版本号作为行的版本号,同时将当前系统版本号作为原来行的删除版本号。更新主键时,聚集索引和普通索引都会产生两个版本;而更新非主键时,只要普通索引会产生两个版本。
-
注意:MVCC只在read committed和repeatable read两个隔离级别下工作。
[参考:《高性能mysql》]
快 照 读 是 哪 些
一个正常的select…语句就是快照读。
快照读,使得在RR(repeatable read)级别下一个普通select...语句也能做到可重复读。即前面MVCC里提到的利用可见版本来保证数据的一致性。
当 前 读 是 哪 些
insert语句、update语句、delete语句、显示加锁的select语句(select… LOCK IN SHARE MODE、select… FOR UPDATE)是当前读。
为什么insert、update、delete语句都属于当前读?
这是因为这些语句在执行时,都会执行一个读取当前数据最新版本的过程。
当前读的SQL语句,InnoDB是逐条与MySQL Server交互的。即先对一条满足条件的记录加锁后,再返回给MySQL Server,当MySQL Server做完DML操作后,再对下一条数据加锁并处理。