锁
锁
数据库系统使用锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性。
锁的粒度
锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高。但锁的各种操作(获取锁、释放锁、检查锁状态)都会增加系统开销。因此锁的粒度越小,系统开销就越大。因此在选择锁的粒度时,需要在锁开销和并发程度做平衡。
MySQL数据库根据锁的粒度把锁分为表级锁和行级锁:
- 表级锁:对当前操作的整张表加锁。锁的粒度大,系统开销小,加锁快,不会出现死锁。但发生锁冲突的概率高,并发程度低。
- 行级锁:只对当前操作的行进行枷锁。锁的粒度小,系统开销大,加锁慢,会出现死锁。但能大大减少数据库操作的冲突,并发度高。
InnoDB支持行级锁和表级锁,默认为行级锁。而MyISAM存储引擎支持表级锁。
锁的类型
InnoDB实现了以下两种标准的行级锁:
- 共享锁(S Lock):也叫读锁。如果一个事务获得数据对象A的S锁,就可以对A进行读操作,但是不能进行写操作,加锁期间其他事务也可以获得A的S锁并读取它。但任何事务都不能获取数据上的X锁,直到A已释放所有共享锁。
- 排他锁(X Lock):也叫写锁。如果一个事务获得数据对象A的X锁,就可以对A进行读和写操作。加X锁期间其他事务不能对A加任何锁,因此叫排他锁。
锁的兼容性可以理解为:同一个数据对象可否同时获得两个不同的锁:
为了支持在更细粒度上进行加锁,InnoDB支持意向锁。意向锁将锁定的对象分为多个层次。如果需要对页上的记录r进行上X锁,那么分别需要对数据库A、表、页上意向锁IX,最后对记录r上X锁。
当一个事务需要给自己需要的某个资源加锁的时候,如果遇到一个共享锁正锁定着自己需要的资源的时候,自己可以再加一个共享锁,不过不能加排他锁。但是,如果遇到自己需要锁定的资源已经被一个排他锁占有之后,则只能等待该锁定释放资源之后自己才能获取锁定资源并添加自己的锁定。
而意向锁的作用就是当一个事务在需要获取资源锁定的时候,如果遇到自己需要的资源已经被排他锁占用的时候,该事务可以需要锁定行的表上面添加一个合适的意向锁。如果自己需要一个共享锁,那么就在表上面添加一个意向共享锁。而如果自己需要的是某行(或者某些行)上面添加一个排他锁的话,则先在表上面添加一个意向排他锁。意向共享锁可以同时并存多个,但是意向排他锁同时只能有一个存在。
意向锁是一种表锁,分为如下两种:
- 意向共享锁(IS Lock):表示事务准备给数据行记入共享锁,一个事务在获得某个数据行对象的 S 锁之前,必须先获得表的 IS 锁或者更强的锁。
- 意向共享锁(IX Lock):表示事务准备给数据行加入排他锁,一个事务在获得某个数据行对象的 X 锁之前,必须先获得表的 IX 锁。
注意:
- 任意 IS/IX 锁之间都是兼容的,因为它们只表示想要对表加锁,而不是真正加锁。
- IX,IS是表级锁,不会和行级的X,S锁发生冲突,只会和表级的X,S发生冲突。
锁问题
通过锁机制实现了事物的隔离性要求,使事物可以并发地工作。但在并发情况下,多个事物同时对同一事物进行操作,会带来以下几种并发不一致问题:
1.丢失更新
即一个事务的更新操作会被另一个事务的更新操作所覆盖。例如;事务T1先将行记录r更新为v1,事务T2再将记录r更新为v2(当前两事务均为提交)。随后事务T1先提交,事务T2后提交。则最终记录r即为v2,事务T2的修改覆盖了事务T1的修改。
2.脏读
要理解脏读,首先要理解脏数据。
脏数据是指事物对缓冲池中行记录的修改,并且还没有提交(commit)。(它和脏页不同,后者是在缓冲池中已经修改,但还没有刷新到硬盘中的页)
脏读指的是在不同的事物下,当前事务可以读到另外一个事物未提交的数据。
例如:事物1修改了数据但并未提交,事物2随后读取了这个数据,之后如果事物1回滚了此次修改,事物2读到的就是不正确的数据。
3.不可重复读
不可重复读是指在一个事物内多次读取同一数据集合,在这个事物还没有结束时,另一个事物也访问了该数据集合并做了一些修改。因此,由于第二个事物的修改,第一个事物两次读取到的数据可能是不一致的。
例如:事物1先读取一次数据,之后事物2对数据进行了修改并提交,这样事物1再次读取这个数据时,读取结果就和第一次不同。
不可重复读和脏读的区别:
- 脏读读到的是未提交的数据,不可重复读读到的是已提交的数据
- 脏读违反了数据库事物的隔离性,不可重复读违反了数据库事物的一致性。
4.幻读
幻读和不和重复读类似,是指当一个事务T1读取了几行数据后,另一个并发事务T2插入了一些数据,因此在之后的查询中,事务T1就会发现多了一些原本不存在的记录。
幻读和不可重复读的区别:
- 幻读的关注点在于增删,比如多次读取一条记录发现记录增多或减少了。
- 不可重复读的关注点在于修改,比如多次读取一条记录发现其中某些列的值被修改。
产生并发不一致问题的主要原因是破坏了事务的隔离性,数据库系统提供了多种事务的隔离级别供用户解决并发一致性问题。
锁算法
InnoDB存储引擎有三种行锁的算法,分别是:
- Record Lock:锁定一个记录上的索引,而不是记录本身。
- Gap Lock:间隙锁,锁定索引间的间隙(一个范围),但不包含索引本身(为了阻止多个事务将记录插入到同一范围内,而这会导致幻读问题的产生)
- Next-Key Lock: Record Lock和Gap Lock的结合。不仅锁定一个记录上的索引,也锁定索引之间的间隙。(InnoDB存储引擎使用这个机制来避免幻读,在可重复读(REPEATABLE READ)隔离级别下,使用 MVCC + Next-Key Locks 可以解决幻读问题。)
例如索引包含以下值:10,11,13,20。采用Next-Key Lock。那么将锁定如下区间:
(-∞,10]
[10,11)
[11,13)
[13,20)
[20,+∞]
死锁
当两个事务都需要获得对方持有的锁,导致双方都在等待,这就产生了死锁。发生死锁后,InnoDB一般都可以检测到,并使一个事务释放锁回退,另一个则可以获取锁完成事务
多版本并发控制(MVCC)
一致性非锁定读是指InnoDB存储引擎通过行多版本控制的方式(multi versioning)的方式来读取当前执行时间数据库中行的数据。具体为:如果读取的行正在执行DELETE或UPDATE操作,这时读取操作不会因此去等待该行上锁的释放。相反地、InnoDB存储引擎会去读取行的一个快照数据。
快照数据其实就是当前行数据之前的历史版本(通过undo来实现,快照数据不需要上锁,因为没有事物需要对历史数据进行修改操作),每行数据可能有多个快照数据(多个历史版本),一般称这种技术为行多版本技术,由此带来的并发控制,称为多版本并发控制。
脏读和不可重复读最根本的原因是事务读取到其它事务未提交的修改。在事务进行读取操作时,为了解决脏读和不可重复读问题,MVCC 规定只能读取已经提交的快照(历史版本)。
MVCC可以实现提交读READ COMMITTED和可重复读REPEATABLE READ两种隔离级别。
- 在READ COMMITTED隔离级别下,总是读取行的最新版本,如果行被锁定了(被另一个事物使用),则读取该行版本的一个快照。
- 而对于REPEATABLE READ的事物隔离级别,总是读取事务开始时的行数据版本。
而未提交读隔离级别总是读取最新的数据行,要求很低,无需使用 MVCC。可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。
- MVCC的SELECT操作读的是快照中的数据,不需要进行加锁操作。
- MVCC的对于数据库进行修改的操作(INSERT、UPDATE、DELETE)需要读取最新的数据,因此需要进行加锁操作。
- 此外,在进行SELETE操作时,可以强制指定进行加锁操作。
1
2
3
4//对读取的行记录加一个X锁,其他事物就不能对该行加上任何锁
SELETE...for update
//对读取的记录加一个S锁,其他事物可以向被锁定的行加S锁,但如果加X锁,则会被阻塞。
SELETE...lock in share mode参考
- 姜承尧. MySQL 技术内幕: InnoDB 存储引擎 第2版[M]. 机械工业出版社, 2013.
- MySQL锁机制
- Cyc2018:数据库系统原理
- JavaGuide;MySQL