1.lock
Innodb用共享锁(shared locks)和独占锁(exclusive locks),又称S锁跟X锁,实现了标准的行锁。
共享锁允许事务持有锁去读取一行
独占锁允许事务持有锁去更新或者删除一行
如果一个事务T1在r行上持有一个S锁。现在存在一个事务T2,也要对r行进行操作。
如果T2需要一个S锁,Innodb会立即授权给它,最终,T1,T2都会对r行持有S锁。
如果T2需要一个X锁,Innodb不会立即授权给它。
如果事务T1在r行上持有X锁,那么来自不同事务T2对r行的任何锁的请求都不会授权,只能等待T1释放r行的锁。
意向锁(intention lock)
意向锁的主要作用是表明有事务锁住了某一行,或者即将锁住某一行。
Innodb支持多粒度锁,允许行锁跟表锁共存。意向锁是表级别的锁,表明稍后对表中的行需要哪种锁(S锁或者X锁)
意向共享锁(IS锁)表明事务将会在表中的各行上添加一个共享锁。
意向独占锁(IX锁)表明事务将会在表中的各行上添加一个独占锁。
SELECT …. LOCK IN SHARE MODE 会添加一个IS锁。
SELECT … FRO UPDATE 会添加一个IX锁。
在事务获得一个表中某一行的共享锁之前,必须要获得一个表上的IS锁或者更强的锁。
在事务获得一个表中某一行的独占锁之前,必须要获得一个表的IX锁。
| X | IX | S | IS | |
|---|---|---|---|---|
| X | Conflict | Conflict | Conflict | Conflict |
| IX | Conflict | Compatible | Conflict | Compatible |
| S | Conflict | Conflict | Compatible | Compatible |
| IS | Conflict | Compatible | Compatible | Compatible |
当事务持有某个对象的锁时,只要不冲突就可以获得需要的锁,如果锁的类型冲突了,需要等待前一个事务释放锁资源。
记录锁(record lock)
记录锁锁住的是索引记录,即使表中没有创建索引,因为Innodb会隐式创建一个索引。
例如:
select c1 from t where c1=10 for update
就会阻止其他事务对t.c1进行更改,删除,插入。
间隙锁(gap lock)
针对某一个范围的索引记录加锁,例如:
SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE
当使用唯一索引去查找值时,不会使用gap lock:
SELECT * FROM child WHERE id = 100;
如果id列的索引值不是唯一的,那么会使用gap 锁。
next-key 锁
next-key锁结合了记录锁跟间隙锁。
Innodb执行行级锁定的方式是,当它搜索或扫描表索引时,它会对遇到的索引记录设置共享或独占锁。因此行锁通常是记录锁。一个在索引记录上的next-key锁也会影响这个记录之前的一部分记录。如果一个事务已经有了对R行这个索引记录的锁,共享锁或者独占锁,另一个事务不能在紧靠索引顺序的R之前的间隙中插入新的索引记录。
插入意向锁
插入意向锁是插入行之前由插入操作设置的一种间隙锁。如果多个事务插入的位置不在同一个位置,则他们不需要等待对方。假设现在有索引记录,4,7。现在有两个事务要分别插入5,6。每个事务在插入的行上都会获得独占锁,他们不会因为冲突而阻塞。
客户端A创建了一张表,插入了两个索引记录,90,102。然后开启了一个事务,获得了一个id大于100的独占锁,包含一个间隙锁,id小于102
mysql> CREATE TABLE child (id int(11) NOT NULL, PRIMARY KEY(id)) ENGINE=InnoDB;
mysql> INSERT INTO child (id) values (90),(102);
mysql> START TRANSACTION;
mysql> SELECT * FROM child WHERE id > 100 FOR UPDATE;
+-----+
| id |
+-----+
| 102 |
+-----+
客户端B开启了一个事务,要去在间隙中插入一个101索引记录,在等待独占锁时,客户端B会获得一个插入意向锁
mysql> START TRANSACTION;
mysql> INSERT INTO child (id) VALUES (101);
自动增长锁(auto-inc lock)
对于auto-increment的列使用的是auto-inc lock,auto-inc lock是一个表级锁。
innodb_autoinc_lock_mode变量决定了使用自增值时的锁定模式,枚举值是0,1,2,默认是1。0代表传统,1代表连续,2代表交错。
当innodb_autoinc_lock_mode=0时,此锁通常保持在语句末尾(而不是事务末尾),以确保按可预测和可重复的顺序为给定的INSERT语句序列分配自动增量值,并确保任何给定语句分配的自动增量值是连续的。
CREATE TABLE t1 (
c1 INT(11) NOT NULL AUTO_INCREMENT,
c2 VARCHAR(10) DEFAULT NULL,
PRIMARY KEY (c1)
) ENGINE=InnoDB;
Tx1: INSERT INTO t1 (c2) SELECT 1000 rows from another table ...
Tx2: INSERT INTO t1 (c2) VALUES ('xxx');
如果是0,那么事务1的自增列是连续的,事务2的自增列的值比事务1的大还是小,取决于哪个事务先执行。
如果是1,结果跟0一样。
如果是2,那么事务1跟事务2可以同时进行,没有谁等待谁,但是使用基于语句到复制,或者恢复时这是不安全的。
在主从复制中,当使用了基于语句的复制时,innodb_autoinc_lock_mode需要设置成0,或者1。如果设置成2,就不能保证主跟从的自增列的值是一样的。如果使用了row-based或者mixed-based时,所有的值都是安全的。因为row-based不区分语句执行的顺序。
关于自增值的丢失,一旦产生了自增值,是不能rollback的。如果事务进行了回退,那么自增值就会产生间隙,就是不连续了。
在所有的锁定模式中,当给自增列赋值0或者NULL时,innodb会视为没有给定值,会分配一个新的值给自增列。
在所有的锁定模式中,分配一个负值,或者值超过了定义的最大值,这都是没有定义的。
Innodb表的数据字典会有一个自动增长的计数器,每当分配一个值时,这个计数器就会加一。这个计数器存在内存中。当服务器重启时,当要插入包含自增的语句时,会执行下面这么一条语句来获取当前的自增值。
SELECT MAX(ai_col) FROM table_name FOR UPDATE;
auto_increment_increment决定了自增的间隔,默认是1。
auto_increment_offset决定了自增的起始位置,默认是1。
2.transaction
事务的隔离级别
读未提交(read uncommited)
这种级别下,语句将不使用锁,将不能保证读一致性,会出现脏读。
读已提交(read commited)
每个读之前都会产生自己的快照。这个隔离级别中,Innodb只会锁住索引记录,从而允许在锁定的记录旁边自由插入新记录。间隙锁只会在检查外键和重复键检查时使用。因为禁用间隙锁,所以可能会发生幻读(其他会话可能在间隙中插入新的行)。
在binlog_format=row时,才支持读已提交,statement不支持读已提交,如果是mixed,则日志自动使用row-based记录。
对于UPDATE跟DELETE语句,Innodb只会锁住需要更改的行。
对于UPDATE语句,如果已经锁定了某一行,Innodb会提供半一致性读,会返回最近提交的版本给MYSQL,MYSQL会判断改变的行是否满足where条件,如果满足,会重新读一遍行的数据,然后加锁,或者等待加锁。
可重复读(repeatable read)
相同事务中会建立一个快照来保证一致性读。如果唯一查询条件中使用了唯一索引,那么将使用记录锁来锁定对应的行。对于其他情况,将使用间隙锁与next-key锁来锁定对应的行。
串行化(serializable)
这个级别与可重复读类似,如果autocommit被禁用了,它会将select语句转换成select.. lock in share mode,这样select语句就是它自己的事务,这会阻塞其他想要修改它的语句。
如果想要改变事务的隔离级别可以使用 SET TRANSACTION 语句。如果想改变默认的隔离级别可以在选项文件中或者命令行使用—transaction-isolation选项。
举例说明读已提交跟可重复度的区别:
现在创建一张表,没有显示创建索引,会创建一个隐式索引
CREATE TABLE t (a INT NOT NULL, b INT) ENGINE = InnoDB;
INSERT INTO t VALUES (1,2),(2,3),(3,2),(4,3),(5,2);
COMMIT;
现在有两个事务:
# Session A
START TRANSACTION;
UPDATE t SET b = 5 WHERE b = 3;
# Session B
UPDATE t SET b = 4 WHERE b = 2;
当隔离级别是RR时:第一个事务会锁定所有的行,但不会释放不需要更改的行。事务二会等待事务一结束。
Tx1:
x-lock(1,2); retain x-lock
x-lock(2,3); update(2,3) to (2,5); retain x-lock
x-lock(3,2); retain x-lock
x-lock(4,3); update(4,3) to (4,5); retain x-lock
x-lock(5,2); retain x-lock
Tx2:
x-lock(1,2); block and wait for first UPDATE to commit or roll back
当隔离级别是读已提交时:只会在需要更改的行上加锁,释放表中那些不需要更改行的锁。对于第二个事务,Innodb提供半一致性读,会锁定满足where条件的行。
Tx1:
x-lock(1,2); unlock(1,2)
x-lock(2,3); update(2,3) to (2,5); retain x-lock
x-lock(3,2); unlock(3,2)
x-lock(4,3); update(4,3) to (4,5); retain x-lock
x-lock(5,2); unlock(5,2)
Tx2:
x-lock(1,2); update(1,2) to (1,4); retain x-lock
x-lock(2,3); unlock(2,3)
x-lock(3,2); update(3,2) to (3,4); retain x-lock
x-lock(4,3); unlock(4,3)
x-lock(5,2); update(5,2) to (5,4); retain x-lock
当where条件中包含索引列时,只有索引列会被加记录锁。下面这个例子,session B将会阻塞。因为他们想更改同一个索引上的值。
CREATE TABLE t (a INT NOT NULL, b INT, c INT, INDEX (b)) ENGINE = InnoDB;
INSERT INTO t VALUES (1,2,3),(2,2,4);
COMMIT;
# Session A
START TRANSACTION;
UPDATE t SET b = 3 WHERE b = 2 AND c = 3;
# Session B
UPDATE t SET b = 4 WHERE b = 2 AND c = 4;
多版本并发控制(multi-version concurrentcy control)
会话A只有在会话B插入并提交之后,且在会话A提交之后,快照的时间点才会向前。
Session A Session B
SET autocommit=0; SET autocommit=0;
time
| SELECT * FROM t;
| empty set
| INSERT INTO t VALUES (1, 2);
|
v SELECT * FROM t;
empty set
COMMIT;
SELECT * FROM t;
empty set
COMMIT;
SELECT * FROM t;
---------------------
| 1 | 2 |
---------------------
幻读(phantom rows)
两次select会出现不同的结果。Innodb使用next-key锁来保证select不会出现幻读。next-key是记录锁与间隙锁的结合版。如果一个会话有R行的共享锁或者独占锁,那么另一个会话将不能在R行记录的间隙中插入新的行。
死锁
两个事务同时等待对方释放自己所需要的资源,就会造成死锁。死锁的可能性不受隔离级别的影响,因为隔离级别影响的是读操作,但是死锁是因为写操作造成的。
innodb_deadlock_detect,这个变量通常是禁用的,但是默认是开启的。在一个高并发的系统中,当有大量的线程等待同一个锁时,死锁检测会降低mysql的性能。所以通常设置innodb_lock_wait_timeout变量而不是innodb_deadlock_detect变量。
innodb_lock_wait_timeout,一个事务多少秒后会放弃等待它所需要的锁,默认是50s。
查看最近的死锁,可以使用SHOW ENGINE INNODB STATUS语句。还可以启用innodb_print_all_deadlock将死锁记录到错误日志中。
如果监控死锁的LATEST DETECTED DEADLOCK这一段中出现了以下信息:
“TOO DEEP OR LONG SEARCH IN THE LOCK TABLE WAITS-FOR GRAPH, WE WILL ROLL BACK FOLLOWING TRANSACTION,”
这表明等待的事务达到了200个,这些事务将会被当成死锁来处理。
如果处理死锁以及降低死锁的可能性
1/经常查看SHOW ENGINE INNODB STATUS
2/如果死锁多了,应该将它们记录到错误日志
3/如果因为死锁而回退的语句,再次执行即可
4/保持每个事务短而小,减少各个事务之间的碰撞
5/不要让事务长时间保持未提交状态,当事务结束时要立即提交,减少事务间的碰撞
6/当使用lock read时(select … for update),(select … lock in share mode)时,应降低隔离级别到READ COMMITED
7/增加索引,减少锁定
