锁是数据库区别与文件系统的一个关键特性,锁机制用于管理对共享资源的并发访问。
InnoDB使用的锁类型,分别有:html
InnoDB实现了两种标准的行级锁:共享锁(S)和排他锁(X)java
共享锁:容许持有该锁的事务读取行记录。若是事务 T1 拥有记录 r 的 S 锁,事务 T2 对记录 r 加锁请求:若想要加 S 锁,能立刻得到;若想要得到 X 锁,则请求会阻塞。mysql
排他锁:容许持有该锁的事务更新或删除行记录。若是事务 T1 拥有记录 r 的 X 锁,事务 T2 对记录 r 加锁请求:不管想获取 r 的 S 锁或 X 锁都会被阻塞。算法
S 锁和 X 锁都是行级锁。sql
InnoDB 支持多粒度的锁,容许一行记录同时持有兼容的行锁和表锁。意向锁是表级锁,代表一个事务以后要获取表中某些行的 S 锁或 X 锁。数据库
InnoDB中使用了两种意向锁并发
例如:性能
SELECT ... LOCK IN SHARE MODE
,设置了 IS 锁SELECT ... FOR UPDATE
,设置了 IX 锁意向锁协议以下所示:大数据
这些规则能够总结为下面的图表(横向表示一个事务已经获取了对应的锁,纵向表示另一个事务想要获取对应的锁):优化
IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突
X | IX | S | IS | |
---|---|---|---|---|
X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
IX | 不兼容 | 兼容 | 不兼容 | 兼容 |
S | 不兼容 | 不兼容 | 兼容 | 兼容 |
IS | 不兼容 | 兼容 | 兼容 | 兼容 |
当请求的锁与已持有的锁兼容时,则加锁成功;若是冲突的话,事务将会等待已有的冲突的锁释放
IX 和 IS 锁的主要目的是代表:某个请求正在或者将要锁定一行记录。意向锁的做用:意向锁是在添加行锁以前添加。当再向一个表添加表级 X 锁的时候
意向锁使用 SHOW ENGINE INNODB STATUS
查看当前锁请求的信息:
TABLE LOCK table `test`.`t` trx id 10080 lock mode IX
InnoDB中,对每一个含有自增加值的表都有一个自增加计数器(aito-increment counter)。当对含有自增加计数器的表进行插入操做时,这个计数器会被初始化。执行以下语句会得到自增加的值
SELECT MAX(auto_inc_col) FROM t FOR UPDATE;
插入操做会依据这个自增加的计数器值加1赋予到自增加列。这种实现方式是AUTO_INC Locking。这种锁采用了一种特殊的表锁机制,为提升插入的性能,锁不是在一个事务完成后释放,而是在完成对自增加值插入的SQL语句后当即释放。虽然AUTO-INC Locking必定方式提高了并发插入的效率,但仍是存在性能上的一些问题:
InnoDB提供了一种轻量级互斥量的自增加实现机制,大大提升了自增加值插入的性能。提供参数innodb_autoinc_lock_mode来控制自增加锁使用的算法,默认值为1。他容许你在可预测的自增加值和最大化并发插入操做之间进行权衡。
插入类型的分类:
插入类型 | 说明 |
---|---|
insert-like | 指全部的插入语句,例如:insert、replace、insert ... select、replace... select、load data |
simple inserts | 指再插入前就肯定插入行数的语句。例如:insert、replace等。注意:simple inserts不包含 insert ... on duplicate key update 这类sql语句 |
bulk inserts | 指在插入前不能肯定获得插入行数的语句,例如:insert ... select、 replace ... select、load data |
mixed-mode inserts | 指插入中有一部分的值是自增加的,一部分是肯定的。例如:insert into t1(c1, c2) values (1, 'a'), (NULL, 'b'), (5, 'c'), (NULL,'d'); 也能够指 insert ... on duplicate key update 这类sql语句 |
innodb_autoinc_lock_mode 在不一样设置下对自增加的影响:
MySQL 5.1.22版本以前自增加的实现方式,经过表锁的AUTO-INC Locking方式
对于『simple inserts』,该值会用互斥量(mutex)对内存中的计数器进行累加操做。对于『bulk inserts』会用传统的AUTO-INC Locking方式。这种配置下,若是不考虑回滚,自增加列的增加仍是连续的。须要注意的是:若是已经使用AUTO-INC Locking方式去产生自增加的值,而此时须要『simple inserts』操做时,还须要等待AUTO-INC Locking的释放
对于全部『insert-like』自增加的产生都是经过互斥量,而不是AUTO-INC Locking方式。这是性能最高的方式。但会带来一些问题:
所以,使用这种方式,任何状况下都须要使用row-base replication,这样才能保证最大并发性能和replication的主从数据的一致 |
InnoDB存储引擎行锁的算法
行锁是加在索引记录上的锁,例如:SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE
,会阻止其余事务插入、更新或删除 t.c1 = 10 的记录
行锁老是在索引记录上面加锁,即便一张表没有设置任何索引,InnoDB会建立一个隐藏的聚簇索引,而后在这个索引上加上行锁。
行锁使用 SHOW ENGINE INNODB STATUS
的输出以下:
RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t` trx id 10078 lock_mode X locks rec but not gap Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0 0: len 4; hex 8000000a; asc ;; 1: len 6; hex 00000000274f; asc 'O;; 2: len 7; hex b60000019d0110; asc ;;
间隙锁是加在索引记录间隙之间的锁,或者在第一条索引记录以前、最后一条索引记录以后的区间上加的锁。例如:SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;
这条语句阻止其余的事务插入一条 t.c1 = 15 的记录,由于在10-20的范围值都已经被加上了锁。
间隙锁只在RR隔离级别中使用。若是一条sql使用了惟一索引(包括主键索引),那么不会使用到间隙锁
例如:id 列是惟一索引,下面的语句只会在 id = 100 行上面使用Record Lock,而不会关心别的事务是否在上述的间隙中插入数据。若是 id 列没有索引或者不是惟一索引,这个语句会在上述的间隙上加锁。
SELECT * FROM child WHERE id = 100 FOR UPDATE;
Next-Key Lock是结合了Gap Lock 和 Record Lock的一种锁算法。
当扫描表的索引时,InnoDB以这种形式实现行级的锁:遇到匹配的的索引记录,在上面加上对应的 S 锁或 X 锁。所以,行级锁其实是索引记录锁。若是一个事务拥有索引上记录 r 的一个 S 锁或 X 锁,另外的事务没法当即在 r 记录索引顺序以前的间隙上插入一条新的记录。
假设有一个索引包含值:10,11,13和20。下列的间隔上均可能加上一个Next-Key 锁(左开右闭)
(negative infinity, 10] (10, 11] (11, 13] (13, 20] (20, positive infinity)
在最后一个区间中,Next-Key锁 锁定了索引中的最大值到 正无穷。
默认状况下,InnoDB启用 RR 事务隔离级别。此时,InnoDB在查找和扫描索引时会使用 Next-Key 锁,其设计的目的是为了解决『幻读』的出现。
当查询的列是惟一索引状况下,InnoDB会对Next-Key Lock进行优化,降级为Record Lock,即只锁住索引自己,而不是范围。
next-key 锁 使用 SHOW ENGINE INNODB STATUS
输出以下:
RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t` trx id 10080 lock_mode X Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0 0: len 4; hex 8000000a; asc ;; 1: len 6; hex 00000000274f; asc 'O;; 2: len 7; hex b60000019d0110; asc ;;
插入意向锁是一种在数据行插入前设置的gap锁。这种锁用于在多事务插入同一索引间隙时,若是这些事务不是往这段gap的同一位置插入数据,那么就不用互相等待。假若有4和7两个索引记录值。不一样的事务尝试插入5和6的值。在不一样事务获取分别的 X 锁以前,他们都得到了4到7范围的插入意向锁,可是他们无需互相等待,由于5和6这两行不冲突。
例如:客户端A和B,在插入记录获取互斥锁以前,事务正在获取插入意向锁。
客户端A建立了一个表,包含90和102两条索引记录,而后去设置一个互斥锁在大于100的全部索引记录上。这个互斥锁包含了在102记录前的gap锁。
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 开启一个事务在这段gap上插入新纪录,这个事务在等待获取互斥锁以前,获取了一把插入意向锁。
mysql> START TRANSACTION; mysql> INSERT INTO child (id) VALUES (101);
插入意向锁 使用 SHOW ENGINE INNODB STATUS
输出以下:
RECORD LOCKS space id 31 page no 3 n bits 72 index `PRIMARY` of table `test`.`child` trx id 8731 lock_mode X locks gap before rec insert intention waiting Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; info bits 0 0: len 4; hex 80000066; asc f;; 1: len 6; hex 000000002215; asc " ;; 2: len 7; hex 9000000172011c; asc r ;;...
给定两个SQL来分析InnoDB下加锁的过程:
SQL1:select * from t1 where id = 10; SQL2:delete * from t1 where id = 10;
事务隔离级别为默认隔离级别Repeatable Read。而对于id不一样的索引类型,会有不一样的结论。(总结自何登成大神的 MySQL 加锁处理分析)
SQL1:在RC和RR下,由于MVCC并发控制,select操做不须要加锁,采用快照读。读取记录的可见版本(多是历史版本)
针对SQL2:以下分不一样状况
将主键上,id=10的记录加上 X 锁
id不是主键,而是一个惟一的二级索引,主键是name列。加锁步骤以下:
聚簇索引加锁的缘由:若是并发的一个SQL是经过主键索引来更新:update t1 set id = 100 where name = 'd';
此时,若是delete语句没有将主键索引上的记录加锁,那么并发的update就会感知不到delete语句的存在。违背同一条记录的更新/删除须要串行执行的约束。
加锁步骤以下:
幻读解决:
这幅图中多了个GAP锁,并非加到记录上的,而是加在两个记录之间的位置。GAP 锁就是 RR 隔离级别相对于 RC 隔离级别,不会出现幻读的关键。GAP锁保证两次当前读以前,其余的事务不会插入新的知足条件的记录并提交。
所谓幻读,就是同一个事务,连续作两次当前读 (例如:select * from t1 where id = 10 for update;
),那么这两次当前读返回的是彻底相同的记录 (记录数量一致,记录自己也一致),第二次的当前读,不会比第一次返回更多的记录 (幻象)。
如图中所示:考虑到B+树索引的有序性,有哪些位置能够插入新的知足条件的项 (id = 10):
所以,不只将知足条件的记录锁上 (X锁),同时还经过GAP锁,将可能插入知足条件记录的3个GAP给锁上,保证后续的Insert不能插入新的id=10的记录,也就杜绝了同一事务的第二次当前读,出现幻象的状况。
当id是惟一索引时,则不须要加GAP锁。由于惟一索引可以保证惟一性,对于where id = 10 的查询,最多只能返回一条记录,并且新的 id= 10 的记录,必定不会插入进来。
当id无索引时,只能进行全表扫描,加锁步骤:
若是表中有上千万条记录,这种状况是很恐怖的。这个状况下,MySQL也作了一些优化,就是所谓的semi-consistent read。semi-consistent read开启的状况下,对于不知足查询条件的记录,MySQL会提早放锁。针对上面的这个用例,就是除了记录[d,10],[g,10]以外,全部的记录锁都会被释放,同时不加GAP锁
死锁避免的一些办法: