本文系转载自
转载仅为方便学习查看,一切权利属于原作者,本人只是做了整理和排版,如果带来不便请联系我删除。
本文主要讲了innodb对比myisam、锁、并发等内容。
原文:InnoDB,5项最佳实践,知其所以然?
知识点:MyISAM会直接存储总行数,InnoDB则不会,需要按行扫描。
潜台词是:对于select count(*) from t
不加where条件; 如果数据量大,MyISAM会瞬间返回,而InnoDB则会一行行扫描。
实践:数据量大的表,InnoDB不要轻易select count(*),性能消耗极大。
常见坑:只有查询全表的总行数,MyISAM才会直接返回结果,当加了where条件后,两种存储引擎的处理方式类似。
例如有用户信息表如下:
t_user(uid, uname, age, sex);
uid PK
age index
现在我们要查询未成年少女个数,sql如下:
select count(*) where age<18 and sex='F';
两种存储引擎的处理方式类似,都需要进行索引扫描。
不管哪种存储引擎,都要建立好索引。
知识点:MyISAM支持全文索引,InnoDB5.6之前不支持全文索引。
实践:不管哪种存储引擎,在数据量大并发量大的情况下,都不应该使用数据库自带的全文索引,会导致小量请求占用大量数据库资源,而要使用 索引外置的架构设计方法。
启示:大数据量+高并发量的业务场景,全文索引,MyISAM也不是最优之选。
知识点:MyISAM不支持事务,InnoDB支持事务。
实践:事务是选择InnoDB非常诱人的原因之一,它提供了commit,rollback,崩溃修复等能力。在系统异常崩溃时,MyISAM有一定几率造成文件损坏,这是非常烦的。但是,事务也非常耗性能,会影响吞吐量,建议只对一致性要求较高的业务使用复杂事务。
画外音:Can’t open file ‘XXX.MYI’. 碰到过么?
小技巧:MyISAM可以通过lock table表锁,来实现类似于事务的东西,但对数据库性能影响较大,强烈不推荐使用。
知识点:MyISAM不支持外键,InnoDB支持外键。
实践:不管哪种存储引擎,在数据量大并发量大的情况下,都不应该使用外键,而建议由应用程序保证完整性。
知识点:MyISAM只支持表锁,InnoDB可以支持行锁。
分析:
实践:网上常常说,select+insert的业务用MyISAM,因为MyISAM在文件尾部顺序增加记录速度极快。楼主的建议是,绝大部分业务是混合读写,只要数据量和并发量较大,一律使用InnoDB。
常见坑:
InnoDB的行锁是实现在索引上的,而不是锁在物理行记录上。潜台词是,如果访问没有命中索引,也无法使用行锁,将要退化为表锁。
画外音:Oracle的行锁实现机制不同。
例如有用户信息表如下,且无其他索引:
t_user(uid, uname, age, sex) innodb;
uid PK
那么有如下结论:
update t_user set age=10 where uid=1;
命中索引,行锁。
update t_user set age=10 where uid != 1;
未命中索引,表锁。
update t_user set age=10 where name='shenjian';
无索引,表锁。
在大数据量,高并发量的互联网业务场景下,对于MyISAM和InnoDB:
有where条件,count(*)两个存储引擎性能差不多;无时,MyISAM直接返回,InnoDB扫描全表
不要使用全文索引,应当使用索引外置的设计方案
事务(InnoDB特性)影响性能,强一致性要求才使用事务
不用外键(InnoDB特性),由应用程序来保证完整性
不命中索引,InnoDB也不能用行锁而是和MyISAM同样使用表锁
在大数据量,高并发量的互联网业务场景下,请使用InnoDB,他最吸引人的:
原文:InnoDB并发如此高,原因竟然在这?
原因
并发的任务对同一个临界资源进行操作,如果不采取措施,可能导致不一致,故必须进行并发控制(Concurrency Control)。
实现方式
通过并发控制保证数据一致性的常见手段有:
普通锁,被使用最多:
1. 操作数据前,锁住,实施互斥,不允许其他的并发任务操作;
2. 操作完成后,释放锁,让其他任务执行;
太过粗暴,连“读任务”也无法并行,即任务执行过程本质上是串行的。于是出现了以下锁:
可以看到,一旦写数据的任务没有完成,数据是不能被其他任务读取的,这对并发度有较大的影响。
画外音:对应到数据库,可以理解为,写事务没有提交,读相关数据的select也会被阻塞。
有没有可能,进一步提高并发呢?
也就是期望即使写任务没有完成,其他读任务也可能并发,这就引出了数据多版本MVCC。
数据多版本是一种能够进一步提高并发的方法,它的核心原理是:
可以看到,数据多版本,通过“读取旧版本数据”能够极大提高任务的并发度。
提高并发的演进思路,就在如此:
画外音:这个思路,比整篇文章的其他技术细节更重要,希望大家牢记。
对应到InnoDB上,具体是怎么玩的呢?
在进一步介绍InnoDB如何使用“读取旧版本数据”极大提高任务的并发度之前,有必要先介绍下redo日志,undo日志,回滚段(rollback segment)。
数据库事务提交后,必须将更新后的数据刷到磁盘上,以保证ACID特性。磁盘随机写性能较低,如果每次都刷盘,会极大影响数据库的吞吐量。
优化方式是,将修改行为先写到redo日志里(此时变成了顺序写),再定期将数据刷到磁盘上,极大提高了性能。
具体来说,redolog有两部分:
画外音:这里的架构设计方法是,随机写优化为顺序写,思路更重要。
假如某一时刻,数据库崩溃,还没来得及刷盘的数据,在数据库重启后,会replay redo日志里的内容,以保证已提交事务对数据产生的影响都刷到磁盘上。
start transaction;
select balance from bank where name="zhangsan";
// 生成 redo log balance=600
update bank set balance = balance - 400;
// 生成 redo log amount=400
update finance set amount = amount + 400;
事务和redo log流程如下
Buffer Pool
MySQL的表数据是存放在磁盘上的,因此想要存取的时候都要经历磁盘IO,然而即使是使用SSD磁盘IO也是非常消耗性能的。
为此,为了提升性能InnoDB提供了缓冲池(Buffer Pool),Buffer Pool中包含了Data Page
磁盘数据页的映射,可以当做缓存来使用:
Data Page
读取在放入缓冲池;如果还没来得及同步(上图红色操作)的时候宕机或断电了怎么办?这样会导致丢失部分已提交事务的修改信息?
所以引入了redo log来记录已成功提交事务的修改信息,并且会在提交事务时把redo log持久化到磁盘。
这样,如果有部分数据没有从Buffer Pool
同步到data page
就发生了故障宕机,则系统重启之后可读取redo log来恢复最新数据放到data page
,保证已提交事务持久性。
一句话,redo日志用于保障,已提交事务的持久性。
数据库事务未提交时,会将事务修改数据的镜像(即修改前的旧版本)存放到undo日志里,当事务回滚时,或者数据库奔溃时,可以利用undo日志,即旧版本数据,撤销未提交事务对数据库产生的影响。
事务提交后,可删除undo日志。
画外音:更细节的:
一句话,undo日志用于保障,未提交事务不会对数据库的ACID特性产生影响。
存储undo日志的地方,是回滚段。
undo日志和回滚段和InnoDB的MVCC密切相关,这里举个例子展开说明一下:
事务提交前会生成undo log,记录修改前的信息。
如果事务回滚,则可从undo log中恢复旧数据。
表结构:
t(id PK, name);
数据:
1, shenjian
2, zhangsan
3, lisi
事务启动但未提交
接着启动了一个事务并且事务处于未提交的状态:
start trx;
delete (1, shenjian);
update set(3, lisi) to (3, xxx);
insert (4, wangwu);
画外音:如果事务已提交,那么回滚段里的undo日志可以删除。
事务回滚
接下来,假如事务rollback,此时可以通过回滚段里的undo日志回滚:
从上图可以看到:
概述
InnoDB是高并发互联网场景最为推荐的存储引擎的根本原因就是其多版本并发控制(Multi Version Concurrency Control, MVCC)。MVCC就是通过“读取旧版本数据”来降低并发事务的锁冲突,提高任务的并发度。主要实现思想是通过数据多版本来做到读写分离,从而实现不加锁读,进而做到读写并行。
行锁,并发,事务回滚等多种特性都和MVCC相关。
核心问题:
InnoDB的内核,会对所有row数据增加三个内部属性:
MVCC实现细节可以见MySQL事务
回滚段里的数据,其实是历史数据的快照(snapshot),这些数据是不会被修改的,select可以肆无忌惮的并发读取他们。
快照读(Snapshot Read),这种一致性(这里的一致性是指,事务读取到的数据,要么是事务开始前就已经由其他已提交事务产生的数据;要么是本事务自身插入或者修改的数据)不加锁的读(Consistent Nonlocking Read
),就是InnoDB并发如此之高的核心原因之一。
MVCC处理高并发能力最强,但系统开销 比最大(较表锁、行级锁),这是最求高并发付出的代价。
除非显示加锁,普通的select语句都是快照读,例如:
select * from t where id>2;
这里的显示加锁,非快照读是指:
select * from t where id>2 lock in share mode;
select * from t where id>2 for update;
问题来了,这些显示加锁的读,是什么读?会加什么锁?和事务的隔离级别又有什么关系?
请看3.3.5章节快照读在RR和RC下有何差异?
画外音:本文的知识点均基于MySQL5.6。
原文:4种事务的隔离级别,InnoDB如何巧妙实现?
事务ACID特性实现如下(ACID详细可参考分布式-事务ACID章节):
整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。
如果事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
例如:银行转账,从 A 账户转 100 元至 B 账户,分为两个步骤:
从 A 账户取 100 元。
存入 100 元至 B 账户。
这两步要么都完成,要么都不完成。因为如果只完成第一步,第二步失败,钱会莫名其妙少了 100 元。
事务提交时的原子性是通过 redo log来实现的。如果事务提交后发生宕机,则机器重启后通过redo log将这一事务重新一起提交即可。
事务回滚时的原子性是通过 undo log 来实现的。如果事务未提交时,发生了某种异常行为,需要回滚该事务,则将undo log中的旧数据做逆操作恢复即可。
事务回滚时操作如下:
undo log记录了数据被修改前的信息以及新增和被删除的数据信息,根据undo log生成回滚语句,并执行即可。
数据库允许多个并发事务同时对数据进行读写和修改的能力,如果一个事务要访问的数据正在被另外一个事务修改,只要另外一个事务未提交,它所访问的数据就不受未提交事务的影响。
隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。
例如:现有有个交易是从 A 账户转 100 元至 B 账户,在这个交易事务还未完成的情况下,如果此时 B 查询自己的账户,是看不到新增加的 100 元的。
假设有InnoDB表:
t(id PK, name);
表中有三条记录:
1, shenjian
2, zhangsan
3, lisi
事务A,先执行,处于未提交的状态:
insert into t values(4, wangwu);
事务B,后执行,也未提交:
select * from t;
如果事务B能够读取到事务A操作的(4, wangwu)这条记录,事务A就对事务B产生了影响,这个影响叫做脏读
:读到了尚未提交事务操作的记录。
事务A,先执行:
select * from t where id=1;
结果集为:
1, shenjian
事务B,后执行,并且提交:
update t set name=xxoo where id=1;
commit;
事务A,再次执行相同的查询:
select * from t where id=1;
结果集为:
1, xxoo
这次是已提交事务B对事务A产生的影响,这个影响叫做不可重复读
,一个事务内相同的查询,得到了不同的结果。
事务A先执行:
select * from t where id>3;
结果集为:
NULL
事务B后执行,并且提交:
insert into t values(4, wangwu);
commit;
事务A首次查询了id>3的结果为NULL,于是想插入一条为4的记录:
insert into t values(4, xxoo);
结果集为:
Error : duplicate key!
事务A的内心OS是:你TM在逗我,查了id>3为空集,insert id=4告诉我PK冲突?这次是已提交事务B对事务A产生的影响,这个影响叫做“幻读”。
不可重复读和幻读到底有什么区别呢?
不可重复读是读取了其他事务更改的数据,针对update操作
解决:使用行级锁,锁定该行,事务A多次读取操作完成后才释放该锁,这个时候才允许其他事务更改刚才的数据。
幻读是读取了其他事务新增的数据,针对insert与delete操作
解决:使用表级锁,锁定整张表,事务A多次读取数据总量之后才释放该锁,这个时候才允许其他事务新增数据。
幻读和不可重复读都是指的一个事务范围内的操作受到其他事务的影响了。只不过幻读是重点在插入和删除,不可重复读重点在修改。
幻读重现步骤如下:
可以看到,并发的事务可能导致其他事务:
按照SQL92标准,InnoDB实现了四种不同事务的隔离级别:
他们与上述几种不正常读的关系如下,打钩代表会发生,打叉代表不会发生:
不同事务的隔离级别,实际上是一致性与并发性的一个权衡与折衷。
InnoDB使用不同的锁策略(Locking Strategy)来实现不同的隔离级别。
这种事务隔离级别下,select语句不加锁。
画外音:官方的说法是
SELECT statements are performed in a nonlocking fashion.
此时,可能读取到不一致的数据,即脏读
。
事务A未提交的数据,可以被事务B读取到。那么这里读到的数据就被视为脏数据。
这是并发最高,一致性最差的隔离级别。
读写可并行,性能高;但可能产生脏读、不可重复度、幻读。
脏读例子
公司发工资了,领导把5000元打到singo的账号上,但是该事务并未提交,而singo正好去查看账户,发现工资已经到账,是5000元整,非常高兴。可是不幸的是,领导发现发给singo的工资金额不对,是2000元,于是迅速回滚了事务,修改金额后,将事务提交,最后singo实际的工资只有 2000元,singo空欢喜一场。也就是说singo事务产生了对未提交的打钱事务的脏读。
事务A已提交的数据才能让其他事务读到
select读取时:
RR
中相同也是MVCC快照读,所以可避免读到未提交事务即脏读;记录锁
;RC可避免脏读
RC下其他事务的插入依然可以执行,就可能导致不可重复读和幻读。
具体来说,RC级别下的MVCC每次select
的时候新生成一个版本号,所以每次select的时候读的不是同一个快照副本,此时就出现不可重复读。
这是互联网最常用的隔离级别
不可重复读例子
singo拿着工资卡去消费,系统读取到卡里确实有2000元,而此时她的老婆也正好在网上转账,把singo工资卡的2000元转到另一账户,并在 singo之前提交了事务。
当singo扣款时,系统再次读取singo的工资卡时已经没有钱,扣款失败,singo十分纳闷,明明卡里有钱,为何…
出现上述情况,即我们所说的不可重复读 ,两个并发的事务,“事务A:singo消费”、“事务B:singo的老婆网上转账”,事务A事先读取了数据,事务B紧接了更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。
在一个事务内的多次读取的结果是一样的。这种级别下可以避免查询时脏读、不可重复读问题,但还是有可能遇到幻读的情况
这是InnoDB默认的隔离级别
select读取时:
select(select ... in share mode / select ... for update)
, update
, delete
等语句,它们的锁,依赖于它们是否在唯一索引(unique index)上使用了唯一的查询条件(unique search condition),或者范围查询条件(range-type search condition):记录锁(record lock)
,而不会封锁记录之间的间隔,即不会使用间隙锁(gap lock)
与临键锁(next-key lock)
,所以可避免不可重复读的情况。画外音:这一段有点绕,多读几遍。
幻读例子
singo的老婆工作在银行部门,她时常通过银行内部系统查看singo的信用卡消费记录。有一天,她查询到singo当月信用卡的总消费金额 (select sum(amount) from transaction where month = 本月)为80元,而singo此时正好在外面胡吃海塞后在收银台买单,消费1000元,即新增了一条1000元的消费记录(insert transaction … ),并提交了事务,随后singo的老婆将singo当月信用卡消费的明细打印到A4纸上,却发现消费总额为1080元,singo的老婆很诧异,以为出现了幻觉,幻读就这样产生了。
关于记录锁,间隙锁,临键锁的更多说明,详见InnoDB,select为啥会阻塞insert?
这种事务的隔离级别下,所有select语句都会被隐式的转化为select ... in share mode
,采用排他锁,所有事务全部串行执行。
这可能导致,如果有未提交的事务正在修改某些行,所有读取这些行的select都会被阻塞住。
画外音:官方的说法是
To force a plain SELECT to block if other transactions have modified the selected rows.
这是一致性最好的,但并发性最差的隔离级别。
只要有一个事务在执行,则其他事务全部排队。
不会发生任何误读情况。
注意:在互联网大数据量,高并发量的场景下,几乎不会使用RU和串行化两种隔离级别。
吞吐量太低,用户体验极差
原文:InnoDB,快照读,在RR和RC下有何差异?
快照读(Snapshot Read)
MySQL数据库,InnoDB存储引擎,为了提高并发,使用MVCC机制,在并发事务时,通过读取数据行的历史数据版本,不加锁,来提高并发的一种不加锁一致性读(Consistent Nonlocking Read)。
读提交(Read Committed)
可重复读(Repeated Read)
在读提交(RC),可重复读(RR)两个不同的事务的隔离级别下,快照读有什么不同呢?
先说结论:
画外音:可以看到
这些就能解答InnoDB的快照读,到底和什么相关?中的问题了。
InnoDB表:
t(id PK, name);
表中有三条记录:
1, shenjian
2, zhangsan
3, lisi
两个并发事务A,B执行的时间序列如下(A先于B开始,B先于A结束):
A1: start transaction;
B1: start transaction;
A2: select * from t;
B2: insert into t values (4, wangwu);
A3: select * from t;
B3: commit;
A4: select * from t;
仍然是上面的两个事务,只是A和B开始时间稍有不同(B先于A开始,B先于A结束):
B1: start transaction;
A1: start transaction;
A2: select * from t;
B2: insert into t values (4, wangwu);
A3: select * from t;
B3: commit;
A4: select * from t;
提问3:假设事务的隔离级别是可重复读RR,事务A中的三次查询,A2, A3, A4分别读到什么结果集?
回答:RR中
提问4:假设事务的隔离级别是读提交RC,A2, A3, A4的结果集又是什么呢?
回答:RC中
结论:事务的开始时间不一样,不会影响“快照读”的结果,所以结果集和case 1一样。
仍然是并发的事务A与B(A先于B开始,B先于A结束):
A1: start transaction;
B1: start transaction;
B2: insert into t values (4, wangwu);
B3: commit;
A2: select * from t;
提问5:假设事务的隔离级别是可重复读RR,事务A中的A2查询,结果集是什么?
回答:RR中
A2读到的是{1, 2, 3, 4}。
因为A2是事务A的第一个read,假设为时间T,它能读取到T之前提交事务写入的数据行,故结果集为{1, 2, 3, 4}。
提问6:假设事务的隔离级别是读提交RC,A2的结果集又是什么呢?
回答:RC中
A2读到的是{1, 2, 3, 4}
并发事务之间相互干扰,可能导致事务出现脏读
,不可重复度
,幻读
等问题
InnoDB实现了SQL92标准中的四种隔离级别:
或许有朋友问,为啥没提到insert?可以查阅InnoDB并发插入,居然使用意向锁?
事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
事务持久性由redo log实现。
详细参考 redo log
指在事务开始之前和事务结束以后,数据库数据的一致性约束没有被破坏,数据库总是从一个一致性的状态转移到另一个一致性的状态。
以转账案例为例,假设有五个账户,每个账户余额是100元,那么五个账户总额是500元,如果在这个5个账户之间同时发生多个转账,无论并发多少个,比如在A与B账户之间转账5元,在C与D账户之间转账10元,在B与E之间转账15元,五个账户总额也应该还是500元,这就是保护性和不变性。
由前面提到的AID特性共同保证。
下面举个例子:zhangsan 从银行卡转400到理财账户
start transaction;
select balance from bank where name=“zhangsan”;
//生成 重做日志 balance=600
update bank set balance = balance - 400;
// 生成重做日志 amount=400
update finance set amount = amount + 400;
commit;
如果执行完 update bank set balance = balance - 400;
之后发生异常了,银行卡的钱也不能平白无辜的减少,而是回滚到最初状态。
这个是原子性,由undo log保证。
如果事务提交之后,缓冲池还没同步到磁盘的时候宕机了,这也是不能接受的,应该在重启的时候恢复并持久化。否则用户看到界面提示转入理财金成功成功,结果理财账户却永远查不到买入的钱。
这个是持久性,由redo log保证。
如果有并发事务请求的时候也应该做好事务之间的可见性问题,避免造成脏读,不可重复读,幻读等。
在涉及并发的情况下往往在性能和一致性之间做平衡,做一定的取舍,所以隔离性也是对一致性的一种破坏。
隔离性使用各种锁和MVCC保证。运用的优化思想有读写分离和并行、读读并行。
总的来说,InnoDB共有七种类型的锁:
原文:插入InnoDB自增列,居然是表锁?
MySQL,InnoDB,默认的隔离级别(RR),假设有数据表:
t(id AUTO_INCREMENT, name);
数据表中有数据:
1, shenjian
2, zhangsan
3, lisi
事务A先执行,还未提交:
insert into t(name) values(xxx);
事务B后执行:
insert into t(name) values(ooo);
问:事务B会不会被阻塞?
InnoDB在RR隔离级别下,能解决幻读问题,上面这个案例中:
事务A先执行insert,会得到一条(4, xxx)的记录,由于是自增列,故不用显示指定id为4,InnoDB会自动增长,注意此时事务并未提交;
事务B后执行insert,假设不会被阻塞,那会得到一条(5, ooo)的记录;
此时,并未有什么不妥,但如果,
insert into t(name) values(xxoo);
会得到一条(6, xxoo)
的记录。
select * from t where id>3;
得到的结果是:
4, xxx
6, xxoo
画外音:不可能查询到5的记录,再RR的隔离级别下,不可能读取到还未提交的其他事务生成的数据。
咦,这对于事务A来说,就很奇怪了,对于AUTO_INCREMENT的列,连续插入了两条记录,一条是4,接下来一条变成了6,就像莫名其妙的幻影。
自增锁是一种特殊的表级别锁(table-level lock),专门针对事务插入AUTO_INCREMENT
类型的列。
最简单的场景,如果一个事务正在往表中插入记录,所有其他事务的插入必须等待,以便第一个事务插入的行,是连续的主键值。
画外音:官网是这么说的
An AUTO-INC lock is a special table-level lock taken by transactions inserting into tables with AUTO_INCREMENT columns. In the simplest case, if one transaction is inserting values into the table, any other transactions must wait to do their own inserts into that table, so that rows inserted by the first transaction receive consecutive primary key values.
与此同时,InnoDB提供了innodb_autoinc_lock_mode
配置,可以调节与改变该锁的模式与行为。
上面的案例,假设不是自增列,又会是什么样的情形呢?
表:
t(id unique PK, name);
数据表中有数据:
10, shenjian
20, zhangsan
30, lisi
事务A先执行,在10与20两条记录中插入了一行,还未提交:
insert into t values(11, xxx);
事务B后执行,也在10与20两条记录中插入了一行:
insert into t values(12, ooo);
这里,便不再使用自增锁,那:
原文:InnoDB并发插入,居然使用意向锁?
可以看到,一旦写数据的任务没有完成,数据是不能被其他任务读取的,这对并发度有较大的影响。
在InnoDB里当然也实现了标准的行级锁(row-level locking),共享/排它锁:
其兼容互斥表如下:
S | X | |
---|---|---|
S | 兼容 | 互斥 |
X | 互斥 | 互斥 |
即:
共享/排它锁的潜在问题是,不能充分的并行,解决思路是数据多版本,这里不再深入展开。
InnoDB支持多粒度锁(multiple granularity locking),它允许行级锁与表级锁共存,实际应用中,InnoDB使用的是意向锁
。
意向锁是指,未来的某个时刻,事务可能要加共享/排它锁了,先提前声明一个意向。
意向锁有这样一些特点:
select ... lock in share mode,要设置IS锁;
select ... for update,要设置IX锁;
意向锁协议(intention locking protocol):
由于意向锁仅仅表明意向,它其实是比较弱的锁,意向锁之间并不相互互斥,而是可以并行,其兼容互斥表如下:
IS | IX | |
---|---|---|
IS | 兼容 | 兼容 |
IX | 兼容 | 兼容 |
S | X | |
---|---|---|
IS | 兼容 | 互斥 |
IX | 互斥 | 互斥 |
画外音:排它锁是很强的锁,不与其他类型的锁兼容。这也很好理解,修改和删除某一行的时候,必须获得强锁,禁止这一行上的其他并发,以保障数据的一致性。
对已有数据行的修改与删除,必须加强互斥锁X锁,那对于数据的插入,是否还需要加这么强的锁,来实施互斥呢?插入意向锁,孕育而生。
插入意向锁,是间隙锁(Gap Locks)的一种(所以,也是实施在索引上的),它是专门针对insert操作的。
画外音:有点尴尬,间隙锁下一篇文章才会介绍,暂且理解为,它是一种实施在索引上,锁定索引某个区间范围的锁。
插入意向锁的玩法是:
多个事务,在同一个索引,同一个范围区间插入记录时,如果插入的位置不冲突,不会阻塞彼此。
画外音:官网的说法是
Insert Intention Lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap.
这样,之前挖坑的例子,就能够解答了。
在MySQL,InnoDB,RR下:
表:
t(id unique PK, name);
数据表中有数据:
10, shenjian
20, zhangsan
30, lisi
事务A先执行,在10与20两条记录中插入了一行,还未提交:
insert into t values(11, xxx);
事务B后执行,也在10与20两条记录中插入了一行:
insert into t values(12, ooo);
回答:虽然事务隔离级别是RR,虽然是同一个索引,虽然是同一个区间,但插入的记录并不冲突,且这里id不是自增字段,故这里:
假设不是插入并发,而是读写并发,又会是什么样的结果呢?
MySQL,InnoDB,默认的隔离级别(RR)。
表结构:
t(id unique PK, name);
数据表中有数据:
10, shenjian
20, zhangsan
30, lisi
事务A先执行,查询了一些记录,还未提交:
select * from t where id>10;
事务B后执行,在10与20两条记录中插入了一行:
insert into t values(11, xxx);
这里:
原文:InnoDB,select为啥会阻塞insert?
记录锁,它封锁索引记录,例如:
select * from t where id=1 for update;
它会在id=1的索引记录上加锁,以阻止其他事务插入,更新,删除id=1的这一行。
需要说明的是:
select * from t where id=1;
则是快照读(SnapShot Read),它并不加锁。
间隙锁,它封锁索引记录中的间隔,或者第一条索引记录之前的范围,又或者最后一条索引记录之后的范围。
依然是上面的例子,InnoDB,RR:
表结构:
t(id PK, name KEY, sex, flag);
表中有四条记录:
1, shenjian, m, A
3, zhangsan, m, A
5, lisi, m, A
9, wangwu, f, B
SQL语句
select * from t
where id between 8 and 15
for update;
上面这个SQL会封锁区间,以阻止其他事务id=10的记录插入。
画外音:
为什么要阻止id=10的记录插入?
如果能够插入成功,头一个事务执行相同的SQL语句,会发现结果集多出了一条记录,即幻影数据。
间隙锁的主要目的,就是为了防止其他事务在间隔中插入数据,以导致“不可重复读”。
如果把事务的隔离级别降级为读提交(Read Committed, RC),间隙锁则会自动失效。
临键锁,是记录锁与间隙锁的组合,它的封锁范围既包含索引记录,又包含索引区间。
更具体的,临键锁会封锁索引记录本身,以及索引记录之前的区间。
如果一个会话占有了索引记录R的共享/排他锁,其他会话不能立刻在R之前的区间插入新的索引记录。
画外音:原文是说
If one session has a shared or exclusive lock on record R in an index, another session cannot insert a new index record in the gap immediately before R in the index order.
依然是上面的例子,InnoDB,RR:
表结构:
t(id PK, name KEY, sex, flag);
表中有四条记录:
1, shenjian, m, A
3, zhangsan, m, A
5, lisi, m, A
9, wangwu, f, B
PK上潜在的临键锁为:
(-infinity, 1]
(1, 3]
(3, 5]
(5, 9]
(9, +infinity]
临键锁的主要目的,也是为了避免幻读(Phantom Read)。
如果把事务的隔离级别降级为RC,临键锁则也会失效。
各类语句使用到的锁如下(原文:别废话,各种SQL到底加了什么锁?):
加锁select主要是指:
select ... for update
select ... in share mode
举个栗子,假设有InnoDB表:
t(id PK, name);
表中有三条记录:
1, shenjian
2, zhangsan
3, lisi
SQL语句:
select * from t where id=1 for update;
以上SQL只会封锁记录,而不会封锁区间。
update t set name=xxx where id=1;
以上SQL也只加记录锁;
同样是写操作,insert和update与delete不同,它会用排它锁封锁被插入的索引记录,而不会封锁记录之前的范围。
同时,会在插入区间加插入意向锁(insert intention lock),但这个并不会真正封锁区间,也不会阻止相同区间的不同KEY插入。
原文:超赞,InnoDB调试死锁的方法!
近期写了不少InnoDB锁相关的文章,不少小伙伴问,如何在MySQL终端模拟并发事务,如何复现之前文章中的案例。今天,咱们一起动起手来,模拟并发事务的互斥与死锁。
安装MySQL服务端、客户端
安装能够模拟多个并发事务的终端
画外音:楼主使用的是MySQL5.6,官方客户端mysql,模拟并发终端用的SecureCRT。
要测试InnoDB的锁互斥,以及死锁,有几个会影响实验结果的配置务必要提前确认:
区间锁(间隙锁,临键锁)是InnoDB特有施加在索引记录区间的锁,MySQL5.6可以手动关闭区间锁,它由innodb_locks_unsafe_for_binlog参数控制:
- 设置为ON,表示关闭区间锁,此时一致性会被破坏(所以是unsafe)
- 设置为OFF,表示开启区间锁
MySQL5.6的默认值为OFF
,表示使用区间锁
可以这么查询该参数:
show global variables like "innodb_locks%";
MySQL默认把每一个单独的SQL语句作为一个事务,自动提交。
MySQL5.6的默认值为On
,表示事务自动提交。
可以这么查询事务自动提交的参数:
show global variables like "autocommit";
要模拟并发事务,需要修改事务自动提交这个选项,每个session要改为手动提交。
任何连上MySQL的session,都要手动执行以下命令以手动控制事务的提交:
set session autocommit=0;
如上图,需要把session的autocommit设置为OFF。
可以看到,修改session变量,并不影响global变量,全局其他的session仍然是ON。
画外音:session变量默认继承global变量,也可以单独修改。
不同事务的隔离级别,InnoDB的锁实现是不一样。
MySQL5.6的默认值为RR
,事务隔离级别为可重复读
可以这么查询事务的隔离级别:
show global variables like "tx_isolation";
可以这么设置事务的隔离级别:
set session transaction isolation level X;
X可取:
read uncommitted
read committed
repeatable read
serializable
InnoDB的行锁都是实现在索引上的,实验可以使用主键,建表时设定为innodb引擎:
create table t (
id int(10) primary key
)engine=innodb;
插入一些实验数据:
start transaction;
insert into t values(1);
insert into t values(3);
insert into t values(10);
commit;
注意:这是实验的初始状态,不同实验开始之初,都默认回到初始状态。
开启区间锁,RR的隔离级别下,上例会有:
(-infinity, 1)
(1, 3)
(3, 10)
(10, infinity)
这四个区间。
事务A删除某个区间内的一条不存在记录,获取到共享间隙锁,会阻止其他事务B在相应的区间插入数据,因为插入需要获取排他间隙锁。
session A:
set session autocommit=0;
start transaction;
delete from t where id=5;
session B:
set session autocommit=0;
start transaction;
insert into t values(0);
insert into t values(2);
insert into t values(12);
insert into t values(7);
事务B插入的值:0, 2, 12都不在(3, 10)区间内,能够成功插入,而7在(3, 10)这个区间内,会阻塞。
可以使用:
show engine innodb status;
来查看锁的情况。
如上图,可以看到(请把图放大):
insert into t values(7);
正在等待共享间隙锁的释放。
如果事务A提交或者回滚,事务B就能够获得相应的锁,以继续执行。
如果事务A一直不提交,事务B会一直等待,直到超时,超时后会显示:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
回到数据的初始状态,这次需要三个并发的session。
画外音:SecureCRT得开三个窗口了。
session A先执行:
set session autocommit=0;
start transaction;
insert into t values(7);
session B后执行:
set session autocommit=0;
start transaction;
insert into t values(7);
session C最后执行:
set session autocommit=0;
start transaction;
insert into t values(7);
三个事务都试图往表中插入一条为7的记录:
如果此时,session A执行:
rollback;
id=7排他锁释放。
则B,C会继续进行主键校验:
B会获取到id=7共享锁,主键未互斥;
C也会获取到id=7共享锁,主键未互斥;
B和C要想插入成功,必须获得id=7的排他锁,但由于双方都已经获取到id=7的共享锁,它们都无法获取到彼此的排他锁,死锁就出现了。
当然,InnoDB有死锁检测机制,B和C中的一个事务会插入成功,另一个事务会自动放弃:
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
共享排他锁,在并发量插入相同记录的情况下会出现,相应的案例比较容易分析。而并发的间隙锁死锁,是比较难定位的。
回到数据的初始状态,这次需要两个并发的session,其SQL执行序列如下:
A:set session autocommit=0;
A:start transaction;
A:delete from t where id=6;
B:set session autocommit=0;
B:start transaction;
B:delete from t where id=7;
A:insert into t values(5);
B:insert into t values(8);
A执行delete后,会获得(3, 10)的共享间隙锁。
B执行delete后,也会获得(3, 10)的共享间隙锁。
A执行insert后,希望获得(3, 10)的排他间隙锁,于是会阻塞。
B执行insert后,也希望获得(3, 10)的排他间隙锁,于是死锁出现。
仍然使用:
show engine innodb status;
来查看死锁的情况。
事务1占有什么锁,请求什么锁;事务2占有什么锁,请求什么锁,一清二楚(请把图放大)。
另外,检测到死锁后,事务2自动回滚了:
WE ROLL BACK TRANSACTION (2)
事务1将会执行成功。
说了很多,希望大家能起手来,这样对InnoDB锁的机制,以及锁的调试印象会更加深刻:
并发事务,间隙锁可能互斥
A删除不存在的记录,获取共享间隙锁;
B插入,必须获得排他间隙锁,故互斥;
并发插入相同记录,可能死锁(某一个回滚)
并发插入,可能出现间隙锁死锁(难排查)
show engine innodb status; 可以查看InnoDB的锁情况,也可以调试死锁