以下都是在innodb存储引擎中
1:innodb中的锁
1.1:锁的类型
行级锁
- 共享锁(s lock):允许事务读一行数据
- 排他锁(x lock):允许事务删除或更新一行数据
行级锁的兼容情况: 对于同一行来说,只有两个共享锁之间是兼容的。这种情况称为锁兼容,即是当一个事务获得了行r的共享锁之后,另一个事务还可以获得该行的共享锁。其他情况均不兼容。
| x | S | |
|---|---|---|
| x | 不兼容 | 不兼容 |
| s | 兼容 | 兼容 |
不兼容的例子:当一个事务获得某一行的共享锁之后,另一个事务要修改该行的数据并需要获得排他锁,这个时候必须要等待前一事务完成后释放该行上的共享锁,这个事务才能获得该行的排他锁,这种情况称为锁不兼容。
表级锁(意向锁)
为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。
- 意向共享锁(is like):事务想要获得一张表中的某些行的共享锁,需先要获得该表的意向共享锁。
- 意向排他锁(is lock):事务想要获得一张表中的某些行的排他锁,需要先获得该表的意向排他锁。
1.2:一致性非锁定读
什么是一致性非锁定读
一致性非锁定读是存储引擎通过多版本控制的方式来读取当前执行时间数据库中的数据
简单来说就是比如某一事物对某行正在进行删除或者更新操作,并且对该行加了排他锁。如果这时另一个事务想要读取该行的数据,那么可以不用等待之前的事物结束释放锁。相反的,innodb会去读取该行的一个快照数据返回。
不用考虑锁兼容的,即是该行被加锁了,还是可以读
快照数据是指该行之前版本的数据。该实现是通过undo段来实现的。undo段时用于事务回滚的,因此快照数据是没有任何开销的。此外,读取快照数据是不需要上锁的,因为没有事务需要对历史数据进行修改操作。
这样一致性的非锁定读提高了数据库的并发性。在innodb中,这是默认的读取方式,即读取不会占用和等待表上的锁,要注意的是,在不同的事物隔离级别下,读取的方式不同,并不是在所有的事物隔离级别下使用的都是一致性非锁定读,而且即使都是使用一致性非锁定读,但是对于快照数据的定义也不同。
使用该方式的隔离级别:
因为有事默认的模式,所以数据库查询语句默认并不会加锁
在事物隔离级别Read committed(读提交)和repeatable read(可重复读)中,采用的是一致性非锁定读,但是这个量级别中对于快照数据的定义是不一样的。
- 提交读中:读取的是被锁定行的最新一份快照数据
- 可重复读中:读取的数据是读取事物开始时的快照数据版本。(本事务的快照数据的最新版本)
例子:
表:学生表(ID,name,age)
数据:1,小明,18
现在我们开启一个事务A:执行以下操作,查询小明的年龄,这时是返回18.
接着,我们再开启一个事务B。在这个事务中,我们更新小明的年龄为19,但是事务没有被提交。这时也就是对该行加了一个排他锁。
我们现在考虑的事务隔离级别是提交读和可重复读,现在我们继续回到事务A中,我们再次查询姓名为小明的学生的年龄。这两个事务隔离下,都会返回18.因为B事务并没有提交,数据库中该行只有一个版本的数据,该版本当前即是最新版本数据,也是A事务开始时的版本数据,所以这两种事务隔离级别下都是返回18 。
之后我们提交B事务。这时候该行数据就会更新一个版本了。
这时我们再返回到A事务中进行查询,同样是查询名字为小明的同学的年龄,这时再提交读的事物隔离级别下,因为是读取最新版本的数据,所以会返回19(即读提交不可重复读的性质);而在可重复读的事物隔离级别下,因为读取的快照数据应该是事务开始时的版本数据,所以还是返回19(即可重复读性质)
1.3:一致性锁定读
默认配置下,即提交读的事物隔离级别下,查询使用一致性非锁定读。
但是在某些情况下,我们想要显示的对查询进行加锁,这就要数据库支持加锁语句,即使时对于select的只读操作。
innidb存储引擎支持两种对查询语句的一致性锁定读(9显示加锁)操作:
- select ....for update:对于读取的记录行加一个排他锁。其他事务就不能再加锁了。
- select ......lock in share mode:对于读取的行记录加一个共享锁。其他事务还可以加共享锁,排他锁就不锁兼容了。
这两个语句必须在事务中,事务结束了锁也就释放了。
而且就算使用该语句给行加了一个排他锁,也丝毫不影响一致性非锁定读。
1.4:自增长与锁
自增长锁这个文章优点不是很清楚:
- 释放这个自增长表锁是执行完就释放,而不是事务结束。
- 现在的默认模式有了互斥量算法
在mysql5.1.22之前。
每个含有自增长的列的数据库都会有一个自增长计数器。插入操作会依据这个计数器的值再加一然后赋值给自增长的列。这个实现方式称为AUTO-INC lock(表锁)。采用一种特殊的表锁机制,为了提高插入的性能,并不是事务提交后才释放锁,而是在自增长列插入的sql语句完成后就释放锁,以提高并发插入的能力。但是还是有许多不足。必须等待前一个插入的完成才能插入下一个(虽然不用等待事务完成),而且如果一个事务中插入数据非常大量,另一个事务中的插入会被阻塞。
在mysql5.1.22之后。
提供了一种轻量级互斥量的自增长实现机制,这种机制大大了提高自增长的性能。
并且mysql可以设置自增长模式,在默认情况下:
- 插入前能确定插入行数的,使用互斥量
- 插入前不能确认插入行数的,还是使用AUTO-INC lock(表锁)
一个问题?不按顺序插入呢?
使用间隙锁原理了。
1.5:外键和锁
对于外键值的拆入或者更新,要先检查父表,即select父表。但是对于父表的select操作,不使用一致性非锁定读的方式,因为这样会发生数据不一致的问题,因此此时使用select ...lock in share mode方式,即显示对父表加一个锁,是一致性锁定读。
这样如果父表上被加了X锁,那么子表中与父表有关的相应外键操作就会被阻塞。
比如在A事务中对父表中id为3的进行修改(删除),这是父表中该记录被加了一个X锁,还不提交事务。这时在事务B中,我们在表中,我们对一行记录的外键等于3的要更新,这时会在父表中查询id=3的记录,这时要加s锁,但是发现以及已经有一个x锁了,B事务这里就被阻塞了,只有A事务提交了,B事务才能继续执行下去。如果使用的时一致性非锁定读,在A事务没有提交情况下,一位id=3的记录还在,就做了更新,但是A事务提交后,id=3的记录实际上是已经删除了的,就造成不一致。
如果对于父表的查询使用一致性非锁定读,就不会发生阻塞,但是会发生数据不一致的问题,所以外键这里,对于父表的查询,使用的是一致性锁定读。
2:行锁的三种算法
InnoDB通过给索引项加锁来实现行锁,如果没有索引,则通过隐藏的聚簇索引来对记录加锁。如果操作不通过索引条件检索数据,InnoDB 则对表中的所有记录加锁,实际效果就和表锁一样。
InnoDB存储引擎有三种行锁算法,其分别是:
- record lock(记录锁):单个行记录上的锁。提交读使用这种算法
- Gap Lock(间隙锁):锁定一个范围,但不会包含记录本身
- Next-Key Lock:Gap Lock+Record Lock,锁定一个范围,并且包含记录本身。可重复读下使用这种算法。
行锁锁住的是索引而不是记录!,查询(修改)条件没有索引的话,只能锁住全表
next-key lock也是innodb默认的算法。
例如一个索引有10、11、13、20这四个值,那么next-key loking的区间为:
(-无穷11]、(11,13]、(13,20]、(20,+无穷)
之呢是左开右闭
当查询的索引含有唯一属性时,innodb存储引擎会对next-key lock进行降级,降级到record lock。(所以主键索引肯定是使用的record lock)
也就是说对于主键索引,最终使用的时record lock算法对索引记录加锁。
对于辅助索引来说,默认加上next-key lock后并不会降级(除非这个辅助索引唯一吧)。特别要注意的是,innodb存储引擎还会对辅助索引下一个键值加上gap lock。例如当一个辅助索引b 有值1 3 6,那么在使用b=3进行查询的时候,显示加锁next-key lock,即对辅助索引范围为(1,3】加锁了,但是还会对(3,6)这个范围也加一个gap lock。而且还要锁住这些辅助索引对应的主键索引,如果b=3对应的书签即主键索引为5,那么还要对主键索引为5进行加锁,这时候加的锁就是record lock了。
总结: 查询条件使用到索引时:
- 对主键索引加锁:record lock算法
- 对非主键索引加锁:先对非主键索引加锁(next-key lock + Gap lock),然后要对对应的主键索引加锁(record lock)
查询条件没有使用到索引,就会锁住整张表
3:解决幻像(phantom problem)-next-key lock可以解决
幻象是不是就是不可重复读呢?innodb存储引擎上说的是,把这两个问题归为一类,但是其他地方却不是.
一文彻底读懂MySQL事务的四大隔离级别 - Jay_huaxiao - 博客园 (cnblogs.com)
看这篇文章确实有可能发生幻读(在一致性非锁定读的情况下),中间使用了uopdate语句的话。
如果使用一致性锁定读的情况下,必然可以解决幻象的问题。(上面例子的情况下因为另一事务就不可能获取锁执行插入操作)
所以在可重复读情况下,只有一致性非锁定读可能发生幻象问题
幻读: 同一事务下,两虚两次执行两次同样的sql语句可能导致不同的结果,第二次sql语句可能返回和之前不存在的行(行增加)
幻读与不可重复读的区别:(按照网上的理论来说) 不可重复读是指在一个事务内多次读取同一个数据返回的结果不一样。(记录数一样)
next-key lock解决了幻读。(而不是可重复读级别下)
在可重复读级别下:(按照主流理论)
- 读的时候:
- 一致性锁定读:通过next-key-lock一定可以解决幻象问题,中间不会有修改(当前读)
- 一致性非锁定读:在可重复读下通过mvc无法解决幻象问题,比如中间使用了update语句,虽然一般不可能出现这种使用方式。
4:锁问题
4.1:脏读
脏数据: 脏数据和脏页是完全不同的概念。脏页是缓冲池中修改的页,但是还没有刷新到磁盘中,即缓冲池中的页数据和磁盘中的不一致。脏数据是指事务对缓冲池中行记录的修改,并且还没有被提交。
脏读: 读到了脏数据。
即是一个事务可以读取到另一个事务中未提交的数据,显然违反了数据库的隔离性。
在未提交读这个事务隔离级别下可能会发生脏读
在另一个事务没有提交的情况下,可以读到其数据的变化
4.2:不可重复读
不可重复读是指在一个事务内多次读取同一个数据返回的结果不一样。(数据条数一样)
在一个事务中查询一个数据,而当这个事务还没有结束时,历一个事务修改了这个数据,并且提交了,那么这个事务再此查询这个数据时,两次返回的内容就不一样,着就是不可重复读。
不可重复读与脏读的区别: 脏读时读到了未提交的脏数据,而不可重复读是,提交的数据在另一事务前后两次读会发现不一致。
在innodb中,通过版本控制可以解决快照读(非锁定一致性读)的不可重复读问题,而当前读(update,insert)的不可重复读(幻读)问题是靠next_key lock解决的。(上面也有将)
4.3:丢失更新
- 事务A将行记录R进行修改,但是事务A没有提交
- 与此同时,事务B将记录R又进行修改,事务B也不提交
- 事务A提交
- 事务B提交 可能你以为这样A的跟新就被B 覆盖了,其实不会
其实这种丢失跟新不会发生,因为任何事务隔离级别下,跟新操作都会加锁,这里A事务进行更新就会对行R加锁,A事务提交前B事务并不会更新,会被阻塞。
还有逻辑意义的一种丢失更新,这种并不是因为数据库的问题:
- 事务A查询数据,给用户user1显示
- 事务B页查询这个数据,给用户user2显示
- user1 修改这个数据,并且提交
- user2修改这个数据,并且提交
显然这个过程中user1 的跟新“丢失”了。
要解决这个问题就要让事务在这种情况下的操作变成串行化。 在步骤一与步骤二加一个排他锁,这样步骤二事务B的读取就会被阻塞,直到1,3执行完成,即事务A结束,才能执行事务B。
5:阻塞
因为不同锁之间的兼容性关系,在有些时刻一个事务中的锁要等待另一个事务中的锁释放它所占用的资源,这就是阻塞。阻塞并不是一件坏事,其是为了确保事务可以并发且正常的运行。
控制等待时间与超时是否回滚得到参数(默认不回滚)
注意阻塞超时后,事务不会主动提交或者回滚,所以需要确定是回滚还是提交。抛出异常后输入commit 或者rollback。默认是不回滚的。 建议把超时后设置为回滚。
6:死锁
死锁是指两个个或者两个以上的事务在执行过程中,因为争夺锁资源而造成的一种相互等待的现象
7:锁升级
锁升级是指把锁粒度降低
innodb存储引擎中没有锁升级。