这里说的锁是指事务级别的对行记录/表进行加/解锁。
事务的开始--加锁,事务的提交/回滚--解锁。
和我们通常说的多线程对共享资源的锁是不一样的。
1. 锁的类型
1. 行级锁
我们知道InnoDB支持行级锁。即以下两种:
- 共享锁(S Lock):允许事务读取某一行的数据
-
排他锁(X Lock):允许事务写某一行的数据
S锁和S锁是可以兼容的,S和X、X和X是不兼容的。
2. 表级锁
InnoDB也有表级锁,当遇到如下SQL就会加表级锁
ALTER TABLE, DROP TABLE, LOCK TABLES
LOCK TABLE my_tabl_name READ; 用读锁锁表,会阻塞其他事务修改表数据。
LOCK TABLE my_table_name WRITE; 用写锁锁表,会阻塞其他事务读和写。
即表级锁也有S锁和X锁。
意向锁
InnoDB中有一种表级锁叫意向锁。
InnoDB有一个这样的规定:在加行级锁之前自动会加意向锁。
- 意向共享锁(IS Lock):事务“想要”读取某几行数据。
- 意向排他锁(IX Lock):事务“想要”写某几行数据。
即在加S Lock时,会先加IS Lock。在加X Lock时,会先加IX Lock。
意向锁只是表达一个意愿,表达后面我将要做什么。
那意向锁的作用是什么呢?
考虑场景:我们使用LOCK TABLE语句希望对表A加读锁,这时我们应该先要判断是否有其他事务对表A进行写。
- 没有意向锁
做法是遍历表A的所有行级锁,看是否有X锁。
显然这样的方式太耗时。 - 有了意向锁
做法判断一下表A是否有IX就可以了。就可以判断表A此时有没有在写的事务。
总结一下意向锁的作用:就是为了提高锁定表级数据的效率。
所以我们再来看一下意向锁和表锁之间的兼容性:
- 意向锁之间都是兼容的
- 表中的S和X都指的是表级锁。意向锁和行级锁不存在兼容性问题,都是兼容的。
2. 一致性非锁定读
读取数据时,如果读取的行已被加X锁,则无需等待,读取的是该行的快照版本。这种技术称为多版本控制(MVVC)。
- 快照版本可以有多个,是由undo段来实现,而undo段用于回滚,所以快照版本无需额外的开销。
- 读取快照版本不会加任何锁,因为没有事务会对历史版本进行修改。
不同事务隔离下不同的行为
READ COMMITTED
- 使用的是一致性非锁定读。
- 快照版本是该行的最新一条快照版本。
REPEATABLE READ(默认级别)
- 使用的是一致性非锁定读。
- 快照版本是事务开始时的版本。
这个行为也决定了RR为什么解决了不可重复读的问题。
解释一下上面两个快照版本的区别
事务A:读行r | --------------- | 读行r
事务B:--------| 写r commit |
主要是看事务A第二次读行r的结果
- READ COMMITTED:读的是事务B提交后的快照
- REPEATABLE READ:读的是事务A开始时读的快照。
3. 一致性锁定读
意思就是用户可以显示地指定读操作一定要加锁。
select ...... for update :这条select语句加X锁
select ......lock in share mode :这条select语句加S锁
4. 自增长与锁
自增长一般我们都会用,如何保证并发下插入的自增长都是1?
- InnoDB使用一种AUTO-INC Locking,它是一个表锁。
不同的是:这个锁的释放是在插入语句执行结束后就释放,而不是事务结束。所以这个效率就会还不错了。
所以并不是全部的锁都是在事务结束后才释放。 - 自增长的列一定要加索引且是索引的第一列(如果索引是组合索引),否则MySQL会报错。
5. 行锁的3种算法
Record Lock、Gap Lock和Next-Key Lock
- Record Lock:单个行记录上的锁。
- Gap Lock:间隙锁,锁定一个范围,不包含记录本身
- Next-Key Lock:Record Lock+Gap Lock,锁定一个范围且包含记录本身。
以上说的锁都是锁的索引。如果没有索引InnoDB会自动建一个rowId索引。
举个例子:
表A只有一个字段id,id为主键。此时表中有四行记录1, 3, 5, 7
对于一个查询语句而言:
Begin
select * from A where id > 2 for update;--加for update的目的是生成X锁。
--注意此时事务并没有提交
- Record Lock:会锁住这个sql语句扫描到的符合条件的所有索引。即3,5,7会被锁住,即此时其他事务无法对id=3、5、7的行进行查询或修改。但可以插入id为4、6、8等行记录。
- Gap Lock:首先会根据所有的索引值生成Gap: (-无穷,1),(1, 3),(3,5),(5,7),(7,+无穷)。然后会锁住根据sql语句扫描到的符合条件的所有gap,不包含索引值本身。即此时其他事务可以修改记录3、5、7但不可以插入4、6、8等。
- Next-Key Lock:也是会先生成Gap:(-无穷,1],(1, 3],(3,5],(5,7],(7,+无穷)。注意这里的Gap是左开右闭的,即包含了扫描到的索引本身。所以此时会锁住所有大于1的范围,即其他事务无法写入/修改和读取所有大于1的记录。
Next-Key Lock
Next-Key Lock是InnoDB在默认隔离级别(REPEATABLE READ)下对于行查询默认的算法。设计的目的为了解决幻读问题。
幻读
幻读:相同两个SQL查询,由于两次SQL查询期间有其他事务的操作,导致执行的结果不同。
- 幻读:第二次读和第一次读比较:多了一些行或少了一些行(其他事务插入/删除了这些行记录)
- 不可重复读:第二次读和第一次读比较:一些行的值不一样(其他事务修改了这些行记录)
从上面我们可以看到Next-Key Lock解决幻读的具体过程。其实是for update+Next-Key Lock才可以解决幻读现象。
这里我们总结一下原因:
- for update:决定了读必须加锁
- Next-Key Lock:决定了加的锁是Gap+Record Lock。
- 查询SQL的条件:决定了Gap Lock的范围。
降级
Next-Key Lock在一些情况下可以降级为Record Lock。
Begin
select * from A where id = 3 for update;
select * from A where id in (1,3) for update;
当查询的字段是唯一索引,且搜索的条件也是唯一的时,此时Next-Key Lock就会降级为Record Lock。原因是根据这个条件查询,不会出现因为其他事务insert导致同一个事务内的两次查询,记录不一样。
- REPEATABLE READ:使用next-key lock来解决这个问题,即锁住的是一个范围。
- READ COMMITTED:使用Record Lock,所以无法避免幻读现象。
一个死锁问题-蛮有意思的
- 在时间点4时,会话A需要等待会话B的S锁结束,所以这里会阻塞。
- 时间点5时,会话A需要等待会话B释放S锁,会话B需要等待会话A释放S锁,这样就形成了循环等待,造成了死锁。
InnoDB会检测出这种死锁条件,直接抛出异常。
6. 锁的问题
1. 脏读
脏读是指一个事务中读到了其他事务未提交的数据。
一般不会出现,除非隔离级别设为:READ UNCOMMITTED,其他隔离级别都不会出现。
2. 丢失更新
比如说有一个这样的逻辑:
[1] select `account` from a where `name` = 'kobe';
....
[2] update a
set `account` = '[1]查询结果 + 100' where `name` = `kobe`;
- 首先查询kobe的账户余额,然后根据账户余额去更新这个账户。
- 出现的问题:在[1]和[2]之间其他事务对kobe的账户做了改动,在执行[2]时,其实kobe的账户并不是[1]查到的结果了。
- 比如:kobe账户总共1万,在执行[1]时查到1万。此时其他事务将账户减去9千,还剩一千,然后执行[2],认为账户余额还是1万,则把账户设为1万+1百=1万1了。
- 解决办法:使用一致性锁定读,即在[1]读的时候加X锁,则其他事务就无法进行修改和读取,这样[2]使用的余额就是数据库中最新的值。
[1] select `account` from a where `name` = 'kobe' for update;
....
[2] update a
set `account` = '[1]查询结果 + 100' where `name` = `kobe`;
7. 数据库隔离级别与读问题
1. 解释一下
这个是数据库系统的规范,即任何具体的数据库都必须满足的特性。只是不同的数据库有自己的对这种规范的具体实现方式。
2. Lost updates
- 即一个事务修改行r,另一个事务也可以修改行r,这样后一个事务相当于把前一个事务修改的数据覆盖掉了。即前一个事务修改的数据“Lost”。
- 所有的数据库隔离级别都不会发生这种事,即行r被一个事务修改的时候,另一个事务是不可以修改的。
- 上节说的“丢失更新”是业务级别上的,并不是数据库事务级别上的。
3. Dirty reads
- 这个上面已经说过了,RC和RR都不会发生。RC通过名字就可以看出来,read已经committed的数据。
- 在InnoDB中,RC由于一致性非限定读,读的是最新的快照版本,所有读的一定是已经committed的数据。
4. Non-repeatable reads
- RR不会发生,通过名字就可以看出来,repeatable read。
- 在InnoDB中,RR由于一致性非限定读,读的是事务开始的快照版本,所以是repeatable read的。
- 所以跟具体使用的哪种锁(Next-Key Lock)没有太大关系。其实个人感觉Next-Key Lock并没有太大意义,可重复读是靠MVVC解决的,而它只是在特定情况下能够解决幻读问题,这个特定情况又是那么特定。
5. Phantoms
- 由图可以看出,只有Serializable才不会出现。
- 在InnoDB中,从上面我们可以看到RR的Next-Key Lock说可以解决幻读现象,那与这里是否矛盾呢?
- 其实并不矛盾,因为上面说RR的Next-Key Lock+for update才可以解决幻读现象,只是单纯的Next-Key Lock并不能解决幻读现象。
8. 阻塞
当一个事务等待另一个事务commit时,我们称为阻塞。
默认情况下,当一个事务阻塞到一定时间时(默认50s),会抛出异常。注意此时此事务并没有把之前的操作进行commit或者roll back,此时处于一种非常危险的状态。所以一定要设置当事务超时时,是commit还是roll back。
9. 死锁
回忆下死锁的知识:
数据库中的死锁是指:多个事务竞争同一资源锁,而出现互相等待,若无外力则无法进行下去。
解决死锁的办法
- 事务超时机制
当出现死锁时,事务阻塞时间超过某个值,则让这个事务roll back释放资源,这样其他事务就可以继续进行下去。
缺点:roll back的事务永远是先阻塞的事务,如果先阻塞的事务的undo太大,则代价很大。简单地说就是无法控制应该让哪个事务roll back。 -
wait for graph
维护一张有向图,节点就是事务,节点之间的连线说明事务A需要等待事务B释放资源。类似于下图:
当图中出现回路时,则说明有死锁。图中的t1和t2就是一个回路。
当任意一个事务申请锁时,都回去更新这张图,如果存在回路则挑undo量最小的事务进行roll back。
死锁的示例
死锁的场景还是很简单的,就是A等待B,B等待A。就会出现死锁。下面这个例子很简单,看一看。