什么是锁?
锁是数据库系统区别于文件系统的一个关键特性。锁机制用于管理对共享资源的并发访问,提供数据的完整性和一致性。
MySQL 不同的存储引擎支持不同的锁机制,MyISAM 和MEMORY 存储引擎采用的是表级锁(talbe-level locking);
BDB存储引擎采用的是页面锁(page-level locking),但是也支持表级锁,InnoDB存储引擎即支持行级锁(row-level locking),也支持表级锁,但是默认情况下是采用行级锁。
- 表级锁:开销小,加锁快;不会出现死锁;锁定颗粒度大,发生锁冲突的概率最高,并发度低。
- 行级锁:开销大,加锁慢;会出现死锁;锁定颗粒度小,发生锁冲突的概率最低,并发度也最高。
- 页面锁:开销和加锁时间介于表锁和行锁之间;会出现死锁;锁定颗粒度介于表锁和行锁之间,并发度一般。
2PL 两阶段锁定协议
InnoDB 采用的是两阶段锁定协议,在事务的执行过程中,只有 COMMIT 和 ROLLBACK 是解锁阶段,其余过程都是加锁阶段,并且所有的锁都会在同一时刻释放。即同一个事务内不管需要对多少个数据项加锁,那么所有的加锁操作都只能在同一个阶段完成,在这个阶段内,不允许对已加锁的数据项进行解锁操作,即加锁和解锁操作不能交叉进行(同一个事务内)。主要用于单机事务中的一致性与隔离性。
MySQL表级锁
MySQL表级锁有两种模式:表共享读锁(Tbale Read Lock)和表独占写锁(Table Write Lock)。
可见,对MyISAM 表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求。对于写操作,则会阻塞其他用户对同一表的读和写操作。
如下图:当一个线程获得对一个表的写锁后,只有持有锁的线程可以对表进行更新操作,其他线程的读写操作都会等待,直到锁被释放为止。
MyISAM在执行查询语句(SELECT)之前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE,DELECT,INSERT等)前,会自动给涉及的表加写锁,这个过程不需要用户干预。
InnoDB 的行锁
InnoDB实现了以下两种类型的行锁:
- 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排它锁。
- 排它锁(X):允许获得排它锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排它锁。
另外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。
- 意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先去的该表的IS锁。
- 意向排它锁(IX):事务打算给数据行加行排它锁,事务在给一个数据行加排它锁前必须先获得该表的IX锁。
如果一个事务请求的锁模式与当前的锁兼容,InnoDB就会将请求的锁授予该事务,反之,如果两者不兼容,该事务就要等待锁释放。意向锁是InnoDB自动加的,不需要用户干预,对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及的数据集加排它锁(X);对于普通的SELECT语句,InnoDB不会加任何锁。
事务可以通过以下语句显示给记录集加共享锁或排它锁:
- 共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE.
- 排它锁(X):SELECT * FROM table_name WHERE ... FORUPDATE。
用SELECT ... IN SHARE MODE获得共享锁,主要用在需要对数据依存关系时来确认某行记录是否存在,并确保没有人对这个记录进行UPDATE 和 DELETE 操作。但是如果当前事务也需要对该记录进行更新操作,则很有可能造成死锁,对于锁定行记录后需要进行更新操作的应用,应该使用SELECT ...FOR UPDATE 方式获得排它锁。
行锁的三种实现
InnoDB行锁是通过给索引上的索引项加锁来实现的,如果没有索引,InnoDB将通过隐藏的聚簇索引来对记录加锁。
- Record Lock: 单个行记录上的锁。
- Gap Lock:间隙锁,锁定一个范围,但不包括记录本身,防止间隔中被其他事务插入。
- Next-Key Lock: Gap Lock + Record Lock,锁定一个范围,并且锁定记录本身,防止幻读。
InnoDB这种行锁实现特点意味着:如果不通过索引条件检索数据,那么InnoDB将对表中的所有记录加锁,实际效果跟表锁一样。
几个加锁例子:
从上面例子可以看出session1只给一行加了排它锁,但是session2在请求其他行的排他锁时,却出现了锁等待。原因就是在没有索引的情况下,InnoDB会对所有的记录都加锁。
2)InnoDB存储引擎的表在使用索引时使用行锁的例子
3)InnoDB存储引擎使用相同索引键的阻塞例子
由于MySQL 的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果使用相同的索引键,是会出现锁冲突的。
4)InnoDB存储引擎使用不同索引键的阻塞例子
当表中有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。
5)即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,他就不会使用索引,这种情况下InnoDB也会对所有记录加锁。
Next-Key 锁
当我们用范围条件而不是相等条件检索数据,并请求共享或排它锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个间隙加锁,这种锁机制就是所谓的Next-Key锁。
InnoDB使用Next-Key锁的目的,一方面是为了防止幻读,满足相关隔离级别的要求。另一方面,是为了满足其回复和复制的需要。很显然在使用范围条件检索并锁定记录时,InnoDB这种加锁机制会阻塞符合条件范围内键值的并发插入,这往往会造成严重的锁等待。
特别说明:InnoDB除了通过范围条件加锁时使用Next-Key锁外,如果使用相等条件请求给你一个不存在的记录加锁,InnoDB也会使用Next-Key锁。
各种SQL到底加了什么锁?
一、普通select
(1)在读未提交(Read Uncommitted),读提交(Read Committed, RC),可重复读(Repeated Read, RR)这三种事务隔离级别下,普通select使用快照读(snpashot read),不加锁,并发非常高;
(2)在串行化(Serializable)这种事务的隔离级别下,普通select会升级为select ... in share mode;
二、加锁select
加锁select主要是指:
-
select ... for update
-
select ... in share mode
(1)如果,在唯一索引(unique index)上使用唯一的查询条件(unique search condition),会使用记录锁(record lock),而不会封锁记录之间的间隔,即不会使用间隙锁(gap lock)与临键锁(next-key lock);
举个栗子,假设有InnoDB表:
t(id PK, name);
表中有三条记录:
1, shenjian
2, zhangsan
3, lisi
SQL语句:
select * from t where id=1 for update;
只会封锁记录,而不会封锁区间。
(2)其他的查询条件和索引条件,InnoDB会封锁被扫描的索引范围,并使用间隙锁与临键锁,避免索引范围区间插入记录;
三、update与delete
(1)和加锁select类似,如果在唯一索引上使用唯一的查询条件来update/delete,例如:
update t set name=xxx where id=1;
也只加记录锁;
(2)否则,符合查询条件的索引记录之前,都会加排他临键锁(exclusive next-key lock),来封锁索引记录与之前的区间;
(3)尤其需要特殊说明的是,如果update的是聚集索引(clustered index)记录,则对应的普通索引(secondary index)记录也会被隐式加锁,这是由InnoDB索引的实现机制决定的:普通索引存储PK的值,检索普通索引本质上要二次扫描聚集索引。
四、insert
同样是写操作,insert和update与delete不同,它会用排它锁封锁被插入的索引记录,而不会封锁记录之前的范围。
同时,会在插入区间加插入意向锁(insert intention lock),但这个并不会真正封锁区间,也不会阻止相同区间的不同KEY插入。