InnoDB是一个多线程并发的存储引擎,内部的读写都是使用多线程来实现的,所以InnoDB内部实现了一个比较高效的并发同步机制。InnoDB并没有直接使用系统提供的锁同步结构(latch),而是对其进行封装实现优化,同时兼容系统的锁。
测试数据
DROP TABLE IF EXISTS account;
CREATE TABLE IF NOT EXISTS account(
id INT(11) unsigned NOT NULL AUTO_INCREMENT,
code INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY(id),
KEY(code)
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO account(id, code) VALUES(10,10),(20,20),(30,30),(40,40),(50,50),(60,60),(70,70)
注意:id为主键索引,code为普通索引(非唯一键二级索引)不要使用唯一索引。
查看锁状态
查看锁状态必须手动开启多个事务,某些锁的状态的查看必须在锁处于waiting
状态才能在MySQL引擎日志中查看到。
命令:显示最近几个事务的状态、查询、写入情况等信息,当出现死锁时会给出最近死锁的明细。
mysql> SHOW ENGINE innodb STATUS;
------------
TRANSACTIONS
------------
Trx id counter 12BA05
Purge done for trx's n:o < 12BA03 undo n:o < 0
History list length 944
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 0, not started
MySQL thread id 8, OS thread handle 0x51d8, query id 75 localhost 127.0.0.1 root
SHOW ENGINE innodb STATUS
---TRANSACTION 12BA04, not started
MySQL thread id 7, OS thread handle 0x575c, query id 72 localhost 127.0.0.1 root
---TRANSACTION 0, not started
MySQL thread id 5, OS thread handle 0x6980, query id 65 localhost 127.0.0.1 root
S锁和X锁
InnoDB存储引擎实现了两种标准行级锁,一种是S锁(Shared Locks,S锁,乐观锁,共享锁),一种是X锁(Exclusive Locks, X锁,互斥锁,排它锁,独占锁)。S锁允许当前持有该锁的事务读取行,X锁允许当前持有该锁的事务更新或删除行。
行锁 | 名称 | 描述 |
---|---|---|
S锁 | 乐观锁,共享锁 | 允许当前持有S锁的事务读取行 |
X锁 | 互斥锁,排它锁,独占锁 | 允许当前持有X锁的事务更新或删除行 |
如果某事务持有某行上的S锁,则其他事务可以同时持有该行的S锁,但不能对该行添加X锁。如果某事务持有某行上的X锁,则其他事务不能持有该行的X锁,必须等待该行的X锁释放。
如果某事务在某行上保持S锁,则另一个事务对该行上锁请求的处理方式可分为两种情况:另一事务可以同时持有S锁,另一事务若想在该行上获取X锁必须等待其他事务对该行添加的S锁或X锁的释放。
意向锁(Intention Locks)
InnoDB存储引擎支持多种粒度的锁,允许行级锁和表级锁的共存。
例如:为指定的表添加X锁
LOCK TABLES tblname WRITE
InnoDB使用意向锁(Intention Locks)实现多个粒度级别的锁定。意向锁(Intention Lock)是表级锁,表示表中的行锁需要的锁的类型。意向锁可以分为意向共享锁(IS锁)和意向排它锁(IX锁), IS锁表示当前事务意图在表中的行上设置S锁。IX锁表示当前事务意图在表中的行上设置X锁。
意向锁 | 名称 | 描述 |
---|---|---|
IS锁 | 意向共享锁 | 当前事务意图在表中的行上设置S锁 |
IX锁 | 意向排它锁 | 当前事务意图在表中的行上设置X锁 |
例如:获取IS锁
SELECT ... LOCK IN SHARE MODE;
例如:获取IX锁
SELECT ... FOR UPDATE;
注意,事务要获取某个表上的S锁和X锁之前,必须先分别获取对应的IS锁和IX锁。
InnoDB锁的兼容矩阵
如果不同事务之间的锁兼容则当前加锁事务可以持有锁,如果有冲突则会等待其他事务的锁释放。如果一个事务请求锁时,请求的锁与已经持有的锁冲突而无法获取时,相互等待就可能会产生死锁。意向锁不会阻止除了全表锁定请求之外的任何锁请求。意向锁的主要目的是显示事务正在锁定某行或正意图锁定某行。
兼容性 | X锁 | IX锁 | S锁 | IS锁 |
---|---|---|---|---|
X锁 | N | N | N | N |
IX锁 | N | Y | N | Y |
S锁 | N | N | Y | Y |
IS锁 | N | Y | Y | Y |
记录锁(Record Lock)
- 记录锁是对索引记录的锁定,记录锁分为两种模式S模式和X模式。
- 记录锁始终只锁定索引,即使表没有建立索引,InnoDB也会创建一个隐藏的聚簇索引(隐藏的递增主键索引),并使用此索引进行记录锁定。
例如:防止任何其他事务插入、更新、删除id为10的行
SELECT id FROM account WHERE id=10 FOR UPDATE;
查看记录所
mysql> SHOW ENGINE innodb STATUS;
事务1:开启事务更新数据后不提交
mysql> START TRANSACTION;
mysql> UPDATE account SET id=100 WHERE id=10;
查看记录锁
mysql> SHOW ENGINE innodb STATUS;
------------
TRANSACTIONS
------------
Trx id counter 12BA07
Purge done for trx's n:o < 12BA03 undo n:o < 0
History list length 944
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 12BA04, not started
MySQL thread id 7, OS thread handle 0x575c, query id 72 localhost 127.0.0.1 root
---TRANSACTION 0, not started
MySQL thread id 5, OS thread handle 0x6980, query id 65 localhost 127.0.0.1 root
---TRANSACTION 12BA06, ACTIVE 44 sec
3 lock struct(s), heap size 320, 2 row lock(s), undo log entries 2
MySQL thread id 8, OS thread handle 0x51d8, query id 87 localhost 127.0.0.1 root
SHOW ENGINE innodb STATUS
UPDATE语句会为id为1的行添加X锁
---TRANSACTION 12BA06, ACTIVE 44 sec
3 lock struct(s), heap size 320, 2 row lock(s), undo log entries 2
MySQL thread id 8, OS thread handle 0x51d8, query id 87 localhost 127.0.0.1 root
SHOW ENGINE innodb STATUS
注意:X锁广义上是一种抽象意义的排它锁,即锁一般分为X模式和S模式。狭义上是指行或索引上的锁。记录锁是索引上的锁,为了不修改数据,使用SELECT ... FOR UPDATE
语句加锁行为和UPDATE
、DELETE
是一样的。INSERT
加锁机制则较为复杂。
事务2:事务1保持原状不提交或回滚,此时开启第二个事务。
mysql> START TRANSACTION;
Query OK, 0 rows affected
mysql> UPATE account SET id=200 WHERE id=10;
1205 - Lock wait timeout exceeded; try restarting transaction
事务2执行UPDATE时SQL语句的执行是被阻塞的
此时查看存储引擎状态
mysql> SHOW ENGINE innodb STATUS;
------------
TRANSACTIONS
------------
Trx id counter 12BA0D
Purge done for trx's n:o < 12BA08 undo n:o < 0
History list length 945
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 12BA0A, not started
MySQL thread id 10, OS thread handle 0x575c, query id 113 localhost 127.0.0.1 root
---TRANSACTION 0, not started
MySQL thread id 5, OS thread handle 0x6980, query id 107 localhost 127.0.0.1 root
---TRANSACTION 12BA0C, ACTIVE 22 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 320, 1 row lock(s)
MySQL thread id 8, OS thread handle 0x51d8, query id 120 localhost 127.0.0.1 root Updating
UPDATE lock_test SET id=200 WHERE id=1
------- TRX HAS BEEN WAITING 22 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 2768 n bits 80 index `PRIMARY` of table `test`.`lock_test` trx id 12BA0C lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 32
0: len 4; hex 00000001; asc ;;
1: len 6; hex 00000012ba0b; asc ;;
2: len 7; hex 0a0000013c1e12; asc < ;;
3: len 4; hex 80000001; asc ;;
间隙锁(Gap Locks)
间隙锁是用来解决幻读的一大手段,通过这种手段,可以没有必要将事务隔离级别调整到序列化这个最严格的级别,而导致并发量大大下降。
间隙锁作用在索引记录之间的间隙,或作用在第一个索引之前最后一个索引之后的间隙,不包括索引本身。
间隙锁的间隙可以跨越零个、单个或多个索引值。间隙锁是性能和并发权衡的产物,只存在于部分事务隔离级别。
例如:唯一索引可以锁定一行,所以无需间隙锁锁定。如果列没有索引或具有非唯一索引,则该语句会锁定当前索引前的间隙。
SELECT * FROM account WHERE id=10;
例如:阻止其他事务插入code为1到10之间的数字,无论这个数字是否存在。
SELECT code FROM account WHERE code BETWEEN 1 AND 10 FOR UPDATE;
在同一个间隙上不同事务可以持有兼容或冲突表中冲突的两个锁,比如事务1现在持有一个间隙S锁,事务2可同时在同一个间隙上持有间隙X锁。允许冲突的锁在间隙上锁定的原因是,若从索引中清除一条记录则由不同事务在这条索引记录上的加间隙锁的动作必须被合并。
InnoDB中的间隙锁的唯一目的是防止其他事务插入间隙,间隙锁是可以共存的,一个事务占用的间隙锁不会阻止另一个事务获取同一个间隙上的间隙锁。如果事务隔离级别修改为RC则间隙锁会被禁用。
查看间隙锁
按官方文档WHERE子句查询条件是唯一键且指定了值时只有记录锁没有间隙锁,如果WHERE语句制定了范围间隙锁是存在的。当指定非唯一键索引时间隙锁的位置会锁定当前索引以及索引之前的间隙。制定了非唯一键索引,间隙锁仍然会存在。
例如:
开启事务1,锁定一条非唯一的普通索引记录。
mysql> START TRANSACTION;
锁定code=70的索引,同时锁定code<70的间隙。
mysql> SELECT * FROM account WHERE code=70 FOR UPDATE;
开启事务2,在code=70之前的间隙中插入一条数据。
mysql> START TRANSACTION;
插入时执行被阻塞
mysql> INSERT INTO account(69, 69);
此时查看引擎状态
mysql> SHOW ENGINE innodb STATUS;
Next-key 锁
Next-key锁实际上是Record锁和Gap锁的组合,Next-key锁是在下一个索引记录本身和索引之前的间隙添加S锁或X锁。默认情况下,InnoDB存储引擎的事务隔离级别为RR,系统参数innodb_locks_unsafe_for_binlog
的值为false
。InnoDB使用Next-key锁对索引进行扫描和搜索,这样就读取不到幻象行,避免了幻读的发生。幻读是指在同一事务下,连续执行两次同样的SQL语句,第二次的SQL语句可能会返回之前不存在的行。当查询的索引是唯一索引时,Next-key锁会进行优化,降级为Record锁,此时Next-key锁仅仅作用在索引本身,而不会作用于间隙和下一个索引上。
自增锁(AUTO-INC Locks)
自增锁是事务插入时自增列上特殊的表级别的锁,最简单的情况是如果一个事务正在向表中插入值,则任何其他事务必须等待,以便第一个事务插入的行接收连续的主键值。一般会将主键设置为AUTO_INCREMENT
的列,默认情况下此字段的值为0,InnoDB会在AUTO_INCREMENT
修饰下的数据列所关联的索引末尾设置独占锁。在访问自增计数器时,InnoDB使用自增锁,但锁定仅持续到当前SQL语句的末尾,而非整个事务的结束,毕竟自增锁是表级别的锁,若长期锁定会大大降低数据库的性能。由于是表锁,使用期间其他会话是无法插入表中。
InnoDB锁优化
- 尽可能让所有数据检索都通过索引来完成,从而避免InnoDB因为无法通过索引键加锁而升级为表级锁定。
- 合理涉及索引,让InnoDB在索引键上加锁时尽可能准确,尽可能缩小锁定范围,避免造成不必要的锁定而影响其它Query的执行。
- 尽量控制事务的大小以减少锁定的资源量和锁定时间长度