innodb的gap lock和change buffer

有别于oracle,除了read-commited外,innodb还实现了repeatable_readread_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,会话Bupdate tab1 where id <100commit,而slave会率先接受会话Bsql导致数据不一致;

个人推测这也是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

对于innodbnon-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 merge3种情况下触发

1 innodb_fast_down=0时关闭数据库时执行full purgeinsert 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/

请使用浏览器的分享功能分享到微信等