在数据库事务特性ACID中,锁对应其中的I(Isolation 隔离性)。
1. 锁类型
存储引擎实现了两种标准的行级锁
- 共享锁 (S lock)
- 排他锁 (X lock)
简单理解,共享锁就是只读锁,排他锁就是读写锁。和java线程内的读写锁的兼容性是一样的。只有SS兼容,其他均不兼容。
2. 锁粒度
锁的申请是分级申请。表 -> 页 -> 行。在表或页上申请的锁被称为意向锁。意向锁也分共享锁和排他锁。
- 共享意向锁 (IS lock)
- 排他意向锁 (IX lock)
锁兼容性可概括为:意向锁之间相互兼容,S之间互相兼容。
通过锁粒度划分,有效减小或避免了表锁/页锁之间的等待。
3. 一致性非锁定读
一致性非锁定读指的是,通过快照,读取操作越过行上的锁读取数据。
然而,即便不申请锁,读取到的数据的一致性还是能得到保障的——在读取时,仍然只能读到事务提交后的状态的数据。
目的是为了提高并发读取的性能。
在不同的事物隔离级别上,快照的定义也不一样。read commit级别,快照是最新的其他线程的提交事务后的快照。repeatable read级别下,快照选取的是本事务开启时的快照。
在serialiable事务隔离级别下,InnoDB默认会为每条select语句加上lock in share mode,即不再支持一致性非锁定读了。
4. 一致性锁定读
读操作时,申请锁。语法层面上:
- select ... for update; # 申请X锁
- select ... lock in share mode; # 申请S锁
5. 自增长主键与锁
自增长主键生成时会申请锁,这个锁相当于一个表锁。但在实现上,为了提高性能,并不是等到事务提交才会释放,而是数据一旦插入即释放。
考虑事务回滚,自增长主键不一定是连续的。
6. 锁算法
锁算法对于理解InnoDB解决幻读问题非常关键。锁算法分3种:
- 行锁 (record lock)
- 间隙锁(gap lock)
- Next-Key Lock (行锁与间隙锁的综合,还有一种变种 Previous-Key Lock,区别是锁定的索引区间,一个是左开右闭,一个是左闭右开)。
间隙锁和Next-Key锁的本质就是,根据索引,把索引范围当锁定对象申请锁。这种思想让解决幻读(重复读)变得十分容易。
比如: select * from user where id between 10 and 20 for update; 锁定的对象至少是id在10到20之间的所有数据,所以当想要有另一个事务在10-20之间插入或删除数据时,将导致申请X锁失败,就不会发生幻读。
7. 锁升级
innoDB的锁使用bitmap算法,即对一个表上的数据按bitmap算法标记锁定还是非锁定,所以,其锁开销非常小,没必要当行锁多时升级为表锁。
8. 锁问题
通过锁解决了事务的隔离性,提高系统并发性能,但会有其他潜在问题。锁带来的问题被限制在三个问题上:脏读,幻读(不可重复读),写入丢失。
脏读,就是读到未提交的数据。read uncommited事务隔离级别下,会产生脏读。
幻读,在一个事务下两次读到不同的数据集。read commit事务隔离级别会读到幻读数据。而InnoDB的默认事务隔离级别repeatabl read下通过next-key lock算法可以避免幻读。
更新丢失,在当前数据库隔离级别上并不会出现数据库意义上的更新丢失,但是在应用程序级别会出现更新丢失。
9. 死锁
和大家熟悉的线程死锁问题一样。两个线程持有对方的要申请的锁就发生了死锁。
超时等待当然也可以解决死锁问题。
InnoDB采用一种更主动的方式——死锁检查线程来发现死锁。算法就是,建立一个事务之间的等待图,当图中发现环(回路)时,即发生死锁。
发生死锁后,根据操作权重,其中一个回退事务。