有别于oracle,除了read-commited外,innodb还实现了repeatable_read,read_uncommited以及serializable,并且默认为repeatable_read。
因此除了行级锁,innodb还提供了gap lock,以避免repeatable_read模式下出现phantom read(幻像读)
先看一个案例,当第2个会话提交后,同一条sql在会话1两次运行返回结果不一样,显然违背了repeatable_read
transaction1> START TRANSACTION;
transaction1> SELECT * FROM t WHERE i > 20 FOR UPDATE;
+------+
| i |
+------+
| 21 |
| 25 |
| 30 |
+------+
transaction2> START TRANSACTION;
transaction2> INSERT INTO t VALUES(26);
transaction2> COMMIT;
transaction1> select * from t where i > 20 FOR UPDATE;
+------+
| i |
+------+
| 21 |
| 25 |
| 26 |
| 30 |
+------+
而gap lock解决了此问题,
什么是gap lock
This is a lock on a gap between index records, or a lock on the gap before the first or after the last index record.
Gap locking is not needed for statements that lock rows using a unique index to search for a unique row
SELECT * FROM child WHERE id = 100;
如果id为unique索引,则不申请gap lock;否则会为其之前的记录添加gap lock;
它与行级锁一起构成了next-key lock;
当innodb访问索引并为记录R施加行级锁时,捎带R之前的记录加上gap lock,阻止其他会话向此区域insert;
innodb锁还有一个致命的弱点,update/delete会为所有扫描过的行添加行锁(不管其是否满足where过滤条件),会伴随相应的gap锁产生,从而阻塞向gap区间的insert操作;
全表扫描则会锁定所有行,如果使用二级索引且为排他锁,innodb还会为其相应的聚集索引记录加锁;
而select ... for update和 select ... lock in share mode则会释放不满足where条件的行锁,但union操作或许是个例外;
innodb索引与锁
先从存储引擎进行过滤,然后是mysql服务器级别,如果行r没有被引擎而是服务器过滤的,则该行依旧会被引擎锁定;
begin
select actor_id from actor where actor_id < 5 and actor <> 1 for update;
--如果actor_id列没有索引,则锁定全表;如果有索引,则锁定小于5的所有记录(包括1)gap lock,因为执行计划为索引范围扫描,而服务器没有告诉存储引擎消除第一行的where条件;
type--range
key--primary
extra--using where; using index --这里的using where表明mysql在引擎返回行之后使用了where过滤条件
还有一种insertion intention gap lock,由insert在实际插入行之前获取,只要不插入同一位置,就不会相互阻塞;
并发插入同一位置则会引发duplicate-key错误,相应记录会被添加共享锁,如果该记录已有排他锁则极易引发死锁;
CREATE TABLE t1 (i INT, PRIMARY KEY (i)) ENGINE = InnoDB;
案例1
Session 1:
START TRANSACTION;
INSERT INTO t1 VALUES(1);
Session 2:
START TRANSACTION;
INSERT INTO t1 VALUES(1);
Session 3:
START TRANSACTION;
INSERT INTO t1 VALUES(1);
Session 1:
ROLLBACK;
会话1获取了排他锁,2和3引发duplicate-key则申请了共享锁;当1回滚释放排他锁后,2和3都获取了共享锁,此时却都不能获取排他锁(彼此阻塞)从而形成死锁;
innodb_locks_unsafe_for_binlog:
用于控制是否开启gap lock,默认为0启动,1则禁止;
但是外键和duplicate-key检查一直使用gap lock无法禁用;
5.1之前replication只支持statement复制,如果此参数设为1,
假定主库会话A执行delete * from tab1 where id > 100,如果没有gap lock,会话B可update tab1 where id <100并commit,而slave会率先接受会话B的sql导致数据不一致;
个人推测这也是unsafe_for_binlog名字的由来;
调用set [global|session] transaction isolation level read committed 与innodb_locks_unsafe_for_binlog=1效果一样,但是操作更灵活,可动态设置;
Full purge
Innodb表执行delete/update时候选行被标记为已删除,但不会立即从表上移出,因为有可能被其他session引用(repeateable_read);
Innodb专门有一个purge thread负责此类工作,其周期性的进行垃圾回收,会判断这些行是否可以实际删除,如果可以则从表中移除;
5.6起引入参数innodb_purge_threads可配置多个线程
Innodb执行slow shutdown(innodb_fast_down=0)时会执行full purge
insert buffer
对于innodb的non-unique二级索引,对其执行insert/delete/update操作时,先检查该索引页是否位于buffer pool中,如果是则直接修改,否则先将改动记录于change buffer中
此功能以前(5.5.4之前)仅用于insert,故又称insert buffer;
受innodb_change_buffering限制,候选值包括:inserts/deletes/purges/changes/all/none
而insert buffer merge在3种情况下触发
1 innodb_fast_down=0时关闭数据库时执行full purge和insert buffer merge
2 sql访问到包含unmerged记录的页,则需要先merge然后才能被访问
3 master进程自动执行,通常是在系统压力比较低的时候;如果系统压力降低了且脏页也被清除,但IO突然活动频繁且持续了几个小时,可能就是此操作在作祟
可通过show innodb status查看相应信息
-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
-------------------------------------
Ibuf: size 7545, free list len 3790, seg size 11336,
8075308 inserts, 7540969 merged recs, 2246304 merges
Size单位为页(默认16k)
Seg size--总大小,约莫11336*16/1024=180M
Free list—空闲页的数量,即不包含unmerged记录
Inserts—自启动以来向insert buffer的插入次数
Merged recs—自启动以来已经merged的记录数,由此判定insert buffer现存有8075308 – 7540969 =534339条记录;启动时可能存有unmerged记录,所以即便其大于inserts也属正常
Merges—merge操作执行次数,每次merge执行的记录数7540969/2246304=3条,理论上insert buffer的引入节省了3倍的IO
上例中,假定1秒执行100个记录合并,也需要5343秒完成
Innodb认为insert buffer适用所有场景因此无法人工控制,但是SSD显然不在此列。
http://www.mysqlperformanceblog.com/2009/01/13/some-little-known-facts-about-innodb-insert-buffer/