@[toc]
一、锁的分类
1.1 加锁的目的
当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的情况,若对并发操作不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性。加锁的目的其实就是保证多用户环境下保证数据库完整性和一致性。
1.2 锁的类别
- 基于加锁机制分类:乐观锁、悲观锁
- 基于锁的属性分类:共享锁、排他锁
- 基于锁的粒度分类:行级锁(INNODB)、表级锁(INNODB、MYISAM)、页级锁(BDB引擎)
- 基于锁的模式分类:记录锁、间隙锁、临键锁、意向共享锁、意向排他锁
二、乐观锁和悲观锁
2.1. 乐观锁
- 乐观锁假设数据不会发生冲突,只有在数据提交更新时才会检测是否冲突,如果冲突了才会返回错误信息。
- 使用乐观锁,如果发现冲突了,则返回错误信息给用户,让用户自已决定如何操作。
- 乐观锁的实现不依靠数据库提供的锁机制,需要我们自已实现,实现方式一般是记录数据版本,一种是通过版本号控制,一种是通过时间戳控制。原理如下:
- 给表加一个版本号或时间戳的字段,读取数据时,将版本号一同读出,数据更新时,将版本号加1。
- 当我们提交数据更新时,版本号会作为where条件,来判断当前库里的版本号与第一次读取出来的版本号是否相等。如果相等,则予以更新,否则认为数据过期,拒绝更新,让用户重新操作。
- 乐观锁是基于程序实现的,所以不存在死锁的情况,适用于读多写少的应用场景。如果经常发生冲突,上层应用不断的让用户进行重新操作,这反而降低了性能,这种情况下悲观锁就比较适用。
2.2. 悲观锁:
- 悲观锁总认为会发生并发冲突,获取和修改数据时,别人会修改数据。所以在整个数据处理过程中,需要将数据锁定,具有强烈的独占和排他特性。
- 悲观锁通常依靠数据库提供的锁机制实现,Mysql中的共享锁和排他锁都属于悲观锁。
- 适用于并发量不太大、写入比较频繁、数据一致性比较高的场景。
三、共享锁与排他锁
InnoDB 实现了标准的行级锁,包括两种:共享锁(简称 s 锁)、排它锁(简称 x 锁)。
- 共享锁(S锁):允许不同事务之间加共享锁读取,但不允许其它事务修改或者加入排他锁,如:SELECT ... LOCK IN SHARE MODE。
- 共享锁的特性是会阻塞其他事务对其插入、更新、删除。主要是为了支持并发的读取数据,读取数据的时候不支持修改,避免出现重复读的问题。
- 排他锁(X锁):当一个事务加入排他锁后,不允许其他事务加共享锁或者排它锁读取,更加不允许其他事务修改加锁的行。排他锁的目的是在数据修改时候,不允许其他人同时修改,也不允许其他人读取,避免了出现脏数据和脏读的问题。
- 操作类型加锁机制:对于 UPDATE 、DELETE 和 INSERT 语句,InnoDB会自动给涉及的数据集增加排他锁(X)。对于普通的 SELECT 语句,InnoDB不会加任何锁,但事务也可以通过以下语句显式的给SELECT语句加共享锁和排他锁,分别如下:
SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
SELECT * FROM table_name WHERE ... FOR UPDATE
- 共享锁和排他锁的兼容性
例如,如果事务 T1 持有行 r 的 s 锁,那么另一个事务 T2 请求 r 的锁时,会做如下处理:
- T2 请求 s 锁立即被允许,结果 T1 T2 都持有 r 行的 s 锁
- T2 请求 x 锁不能被立即允许
- 如果 T1 持有 r 的 x 锁,那么 T2 请求 r 的 x、s 锁都不能被立即允许,T2 必须等待T1释放 x 锁才可以,因为X锁与任何的锁都不兼容。 ![S锁和X锁兼容性](/i/ll/?i=88fbf432837044c480ff9758214d0542.png#pic_center =600x)
四、表锁
- 表锁是指上锁的时候锁住的是整个表,当下一个事务访问该表的时候,必须等前一个事务释放了锁才能进行对表进行访问;
- 特点: 粒度大,加锁简单,容易冲突;
五、意向锁
- 意向锁的解释:当一个事务试图对整个表进行加锁(共享锁或排它锁)之前,首先需要获得对应类型的意向锁(意向共享锁或意向排他锁)。
- 意向共享锁( IS 锁):当一个事务试图对整个表经行加共享锁之前,首先需要获得这个表的意向共享锁。
- 意向排他锁( IX 锁):当一个事务试图对整个表经行加排他锁之前,首先需要获取这个表的意向排他锁。
- 意向锁的作用:如果一个事务试图添加表级别的共享锁或排它锁,则需要检查表的各个页或行是否有锁,这样效率非常低下。意向锁是表示对某一个表进行加锁的状态,此时只需检查表上的意向锁即可。意向锁是 InnoDB 自动加的,不需要用户干预。
- 举例如下:
- 事务A对user_info表执行一个SQL:update user_info set name =”张三” where id=6 加锁情况如下图: 此时,是对"id=6"记录加的行锁。
- 与此同时数据库又接收到事务B修改数据的请求:SQL: update user_info set name ='李四';
- 因为事务B是对整个表进行修改操作,那么此SQL是需要对整个表进行加排它锁的(update加锁类型为排他锁);
- 我们首先做的第一件事是先检查这个表有没有被别的事务锁住,只要有事务对表里的任何一行数据加了共享锁或排他锁我们就无法对整个表加锁(排他锁不能与任何属性的锁兼容)。
- 因为INNODB锁的机制是基于行锁,那么这个时候我们会对整个索引每个节点一个个检查。检查每个节点是否被别的事务加了共享锁或排它锁。
- 最后检查到索引ID为6的节点被事务A锁住了,最后导致事务B只能等待事务A锁的释放才能进行加锁操作。
- 意向锁的思考:在A事务的操作过程中,后面的每个需要对user_info加持表锁的事务都需要遍历整个索引树才能知道自己是否能够进行加锁,这种方式是不是太浪费时间和损耗数据库性能了?
- 所以就有了意向锁的概念:如果当事务A加锁成功之后就设置一个状态告诉后面的人,已经有人对表里的行加了一个排他锁了,你们不能对整个表加共享锁或排它锁了,那么后面需要对整个表加锁的人只需要获取这个状态就知道自己是不是可以对表加锁,避免了对整个索引树的每个节点扫描是否加锁,而这个状态就是我们的意向锁。
- 意向锁和其他锁的兼容性 InnoDB存储引擎中锁的兼容性如下表: ![InnoDB存储引擎中锁的兼容性](/i/ll/?i=80215228ad7f4fe7a52cf8c04222200c.jpeg#pic_center =700x)
- 插入意向锁(Insert Intention)
- 插入意向锁是在插入一行记录操作之前设置的一种间隙锁,这个锁释放了一种插入方式的信号,亦即多个事务在相同的索引间隙插入时如果不是插入间隙中相同的位置就不需要互相等待。
- 插入意向锁, 一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了所谓的gap锁,如果有的话,插入操作需要等待,直到拥有gap锁的那个事务提交。InnoDB规定事务在等待的时候也需要在内存中生成一个锁结构,表明有事务想在某个间隙中插入新记录,但是现在在等待。
- 插入意向锁和间隙锁互斥。插入意向锁互相不互斥。
- 事务数据类似于下面:
RECORD LOCKS space id 31 page no 3 n bits 72 index `PRIMARY` of table `test`.`child`
trx id 8731 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 80000066; asc f;;
1: len 6; hex 000000002215; asc " ;;
2: len 7; hex 9000000172011c; asc r ;;...
六、行级锁
- 行锁是指上锁的时候锁住的是表的某一行或多行记录,其他事务访问同一张表时,只有被锁住的记录不能访问,其他的记录可正常访问。
- 在MySQL的InnoDB中,行级锁并不是直接锁记录,行锁是通过给索引项加锁实现的,如果没有索引,InnoDB会通过隐藏的聚簇索引来对记录加锁。
- 特点:粒度小,加锁比表锁麻烦,不容易冲突,相比表锁支持的并发要高。
- 索引分为主键索引和非主键索引两种,如果一条sql语句操作了主键索引,MySQL就会锁定这条主键索引;如果一条语句操作了非主键索引,MySQL会先锁定该非主键索引,再锁定相关的主键索引。
- 也就是说:如果不通过索引条件检索数据,比如update时,where条件没有走索引,那么InnoDB将对表中所有数据加锁,实际效果跟表锁一样,在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。
- 行锁分为三种情形:Record lock、Gap lock、Next-key Lock。
七、记录锁(Record Locks)
- 记录锁也属于行锁中的一种,是最简单的行锁,事务在加锁后锁住的仅仅只是表的某一条记录。
- 记录锁并不是锁住该条记录,永远都是加在索引上的,即使一个表没有索引,InnoDB也会隐式的创建一个索引,并使用这个索引实施记录锁。如: 记录排他锁(x):SELECT * FROM t WHERE id = 10 FOR UPDATE;记录读锁(s):SELECT * FROM t WHERE id = 10 LOCK IN SHARE MODE。
- 触发条件:精准条件命中,并且命中的条件字段是唯一索引。例如:update user_info set name=’张三’ where id=1 ,这里的id是唯一索引。
- 加了记录锁之后数据可以避免数据在查询的时候被修改的重复读问题(s),也避免了在修改的事务未提交前被其他事务读取的脏读问题(x)。
- 记录锁的事务数据(关键词:lock_mode X locks rec but not gap),记录如下:
RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t`
trx id 10078 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 8000000a; asc ;;
1: len 6; hex 00000000274f; asc 'O;;
2: len 7; hex b60000019d0110; asc ;;
八、间隙锁(Gap Locks)
- 间隙锁是属于行锁的一种,间隙锁是在事务加锁后其锁住的是表记录的某一个区间,当表的相邻ID之间出现空隙则会锁住一个区间,遵循左开右闭原则。
- 比如下面的表里面的数据ID 为 1,4,5,7,10 ,那么会形成以下几个间隙区间,-n~1区间,1-4区间,7-10区间,10-n区间 (-n代表负无穷大,n代表正无穷大)
- 触发条件:范围查询并且查询未命中记录,查询条件必须命中索引。间隙锁只会出现在REPEATABLE_READ(重复读)的事务级别中和唯一主键冲突修改的情况(比如REPLACE INTO操作)。 -使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据。 可以加在两个索引之间,或者加在第一个索引之前,或最后一个索引之后的间隙。主要作用是避免幻读问题,不过mysql幻读问题主要是通过mvcc版本机制控制的。
- 间隙锁只阻止其他事务插入到间隙中,但不阻止其他事务在同一个间隙上获得间隙锁,所以 gap x lock 和 gap s lock 有相同的作用。
- 间隙锁的事务数据(关键词:gap before rec),记录如下:
RECORD LOCKS space id 177 page no 4 n bits 80 index idx_name of table `test2`.`account`
trx id 38049 lock_mode X locks gap before rec
Record lock, heap no 6 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 3; hex 576569; asc Wei;;
1: len 4; hex 80000002; asc ;;
九、临键锁(Next-Key Locks)
- 临键锁也属于行锁的一种,它是INNODB的行锁默认算法,是记录锁和间隙锁的组合。临键锁会把查询出来的记录行锁住,同时也会把该范围查询内的所有间隙空间也会锁住,再之它会把相邻的下一个区间也会锁住。
- 触发条件:范围查询并命中,查询命中了索引。
- 作用:索引命中多行数据时加锁,避免脏读、幻读和不可重复读三个问题。加了临键锁之后,在范围区间内数据不允许被修改和插入。 - 锁模式兼容矩阵 横向是已持有锁,纵向是正在请求的锁:
十、mysql各种操作加锁情况
10.1 SELECT ... FROM
- SELECT ... FROM是一致读取,读取数据库的快照并且不设置锁,除非事务隔离级别设置为 SERIALIZABLE。
- 对于 SERIALIZABLE级别,搜索在它遇到的索引记录上设置共享的下一个键锁。但是,对于使用唯一索引锁定行以搜索唯一行的语句,只需要一个索引记录锁。
10.2 SELECT ... FOR UPDATE or SELECT ... LOCK IN SHARE MODE
- 对于SELECT ... FOR UPDATE or SELECT ... LOCK IN SHARE MODE,会为扫描的行获取锁,并为不符合包含在结果集中的行(例如,如果它们不符合WHERE子句中给定的条件)释放锁。
- 但是,在某些情况下,可能不会立即解锁行,因为结果行与其原始源之间的关系在查询执行期间丢失。例如,在一个 UNION,在评估它们是否符合结果集的条件之前,可能会将表中的已扫描(和锁定)行插入到临时表中。在这种情况下,临时表中的行与原始表中的行之间的关系丢失,并且后面的行直到查询执行结束才解锁。
10.3 SELECT ... LOCK IN SHARE MODE
- SELECT ... LOCK IN SHARE MODE在搜索遇到的所有索引记录上设置共享的 next-key 锁。但是,对于使用唯一索引锁定行以搜索唯一行的语句,只需要一个索引记录锁。
10.4 SELECT ... FOR UPDATE
- SELECT ... FOR UPDATE在搜索遇到的每条记录上设置一个排他的 next-key 锁。但是,对于使用唯一索引锁定行以搜索唯一行的语句,只需要一个索引记录锁。
- 对于搜索遇到的索引记录, SELECT ... FOR UPDATE阻止其他会话执行 SELECT ... LOCK IN SHARE MODE或读取某些事务隔离级别。一致读取忽略读取视图中存在的记录上设置的任何锁定。
10.5 UPDATE ... WHERE ...
- UPDATE ... WHERE ...在搜索遇到的每条记录上设置一个排他的 next-key 锁。但是,对于使用唯一索引的记录,只需要一个索引记录锁。其他事务对持有独占锁的记录进行修改的时候会被阻塞。这个锁并不是执行完update语句才会释放,而是会等事务结束时才会释放。
- 修改聚集索引记录时UPDATE,会对受影响的二级索引记录进行隐式锁定。
- UPDATE当在插入新的二级索引记录之前执行重复检查扫描时,以及在插入新的二级索引记录时,该操作也会对受影响的二级索引记录进行共享锁。
- **注意:update 语句的 where 条件没有使用索引,就会全表扫描,于是就会对所有记录加上 next-key 锁(记录锁 + 间隙锁),相当于把整个表锁住了。如果存在索引的话,则对索引进行加锁。**因此,当在数据量非常大的数据库表执行 update 语句时,如果没有使用索引,就会给全表的加上 next-key 锁, 那么锁就会持续很长一段时间,直到事务结束。而这期间除了 select ... from 语句,其他语句都会被锁住不能执行。
10.6 DELETE FROM ... WHERE ...
- DELETE FROM ... WHERE ...在搜索遇到的每条记录上设置一个排他的 next-key 锁。但是,对于使用唯一索引锁定行以搜索唯一行的语句,只需要一个索引记录锁。
10.7 INSERT
- 简单的insert会在insert的行对应的索引记录上加一个排它锁(事务未提交,对该行任何相关操作都会阻塞),不是next-key锁(即没有间隙锁),所以并不会阻塞其他session在gap间隙里插入记录。
- 不过在insert操作之前,innodb还会设置插入意图间隙锁(insertion intention gap lock)。这个插入意向间隙锁的作用就是预示着当多事务并发插入相同的空隙时,只要插入的记录不是间隙中的相同位置,则无需等待其他session就可完成,这样就使得insert操作无须加真正的gap lock。假设有一个记录索引包含键值4和7,不同的事务分别插入5和6,每个事务都会产生一个加在4-7之间的插入意向锁,然后再获取在插入行上的排它锁,但是不会被互相锁住,因为数据行并不冲突。
- 如果发生重复键错误,则会在重复索引记录上设置共享锁(s)。如果另一个会话已经拥有排他锁,那么如果有多个其他会话尝试插入同一行,则使用共享锁可能会导致死锁。如果另一个会话删除了该行,就会发生这种情况。
- INSERT 加锁整个流程
- 首先对插入的间隙加插入意向锁(Insert Intension Locks)
- 如果该间隙已被加上了 GAP 锁或 Next-Key 锁,则加锁失败进入等待。
- 如果没有,则加锁成功,表示可以插入。
- 然后判断插入记录是否有唯一键,如果有,则进行唯一性约束检查
- 如果不存在相同键值,则完成插入
- 如果存在相同键值,则判断该键值是否加锁
- 如果没有锁,判断该记录是否被标记为删除。如果标记为删除,说明事务已经提交,还没来得及 purge,加 S 锁等待;
- 如果没有标记删除,则报 1062 duplicate key 错误。
- 如果有锁,说明该记录正在处理(新增、删除或更新),且事务还未提交,则加 S 锁等待(并不是简单的报错返回); [备注: 这里为什么要加S锁,从其他方面猜测可能是防止被其他事务删除。 至于什么时候释放S锁,应该是等待X锁释放后检查一次冲突, 如果还是有重复冲突则直接报错, 如果没有则继续尝试执行插入]
- 插入记录并对记录加 X 记录锁
- 提交事务
10.8 REPLACE INTO
- REPLACE INTO 时,如果没有唯一索引冲突,就像 INSERT一样。
- 如果出现唯一索引冲突,会在要替换的行上放置一个排他的 next-key 锁。