脏读(Dirty Read): A 看到 B 进行中更新的数据,并以此为根据继续执行相关的操作;B 回滚,导致 A 操作的是脏数据。
不可重复读(Non-repeatable Read): A 先查询一次数据,然后 B 更新之并提交,A 再次查询,得到和上一次不同的查询结果。
幻读(Phantom Read): A 查询一批数据,B 插入或删除了某些记录并提交,A 再次查询,发现结果集中出现了上次没有的记录,或者上次有的记录消失了。
第二类丢失更新 (覆盖丢失): A 和 B 更新同一条记录并提交,后提交的数据将覆盖先提交的,通常这是没问题的,但是在某些情况下,如在程序中自增自减、程序中的读-改-全量更新,就会出现并发问题。这类问题更像是应用层面的,不属于DB范畴。
read uncommited 最弱,事务的任何动作对其他事务都是立即可见的。存在脏读、不可重复读、幻读问题(除了回滚丢失,其他的并发问题都有)。
read commited 只能读到其他事务已提交的数据,中间状态的数据则看不到,解决了脏读问题。
repeatable read (InnoDB的默认隔离级别) 根据标准的SQL规范,该级别解决了不可重复读的问题,保证在一个事务内,对同一条记录的重复读都是一致的。
InnoDB 的 Repeatable Read 通过 MVCC 和 间隙锁 机制额外解决了幻读问题。
serial 最高,所有读写都是串行的。
Serializable隔离级别,影响的是SQL1:select * from t1 where id = 10; 这条SQL,在RC,RR隔离级别下,都是快照读,不加锁。但是在Serializable隔离级别,SQL1会加读锁,也就是说快照读不复存在,MVCC并发控制降级为Lock-Based CC
InnoDB 对事务隔离级别的实现依赖两个手段:锁、MVCC(多版本控制协议)。MVCC可以认为是对锁机制的优化,让普通select避免加锁,同时还能有事务隔离级别的语义保证。
2PL (二阶段锁)
事务开始加锁,事务结束解锁
MVCC,Multi-Version Concurrency Control,为一条记录维护多个不同的snapshot,并记录各snapshot对应的版本号(事务ID),每个事务可以读到的snapshot是受限的,从而隔离其他事务的并发动作。
MVCC并发控制中,读操作分为两类:快照读 (snapshot read)与当前读 (current read)。前者读取的是记录的snapshot(有可能是历史版本),不用加锁;后者读取的是记录的最新版本,且会加上锁,保证其他事务不会并发修改这条记录。
在说到如何实现前先引入两个概念:
系统版本号:一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
事务版本号:事务开始时的系统版本号。
InnoDB
中 MVCC
的实现方式为:每一行记录都有两个隐藏列:DATA_TRX_ID
、DATA_ROLL_PTR
(如果没有主键,则还会多一个隐藏的主键列)。
记录最近更新这条行记录的事务 ID
,大小为 6
个字节
表示指向该行回滚段(rollback segment)
的指针,大小为 7
个字节,InnoDB
便是通过这个指针找到之前版本的数据。该行记录上所有旧版本,在 undo
中都通过链表的形式组织。
行标识(隐藏单调自增 ID
),大小为 6
字节,如果表没有主键,InnoDB
会自动生成一个隐藏主键,因此会出现这个列。另外,每条记录的头信息(record header
)里都有一个专门的 bit
(deleted_flag
)来表示当前记录是否已经被删除。
还是以上文 MVCC
的例子,事务 A
对值 x
进行更新之后,该行即产生一个新版本和旧版本。假设之前插入该行的事务 ID
为 100
,事务 A
的 ID
为 200
,该行的隐藏主键为 1
。
####
DB_ROW_ID = 1
的这行记录加排他锁undo log
中,DB_TRX_ID
和 DB_ROLL_PTR
都不动DATA_TRX_ID
为修改记录的事务 ID
,将 DATA_ROLL_PTR
指向刚刚拷贝到 undo log
链中的旧版本记录,这样就能通过 DB_ROLL_PTR
找到这条记录的历史版本。如果对同一行记录执行连续的 UPDATE
,Undo Log
会组成一个链表,遍历这个链表可以看到这条记录的变迁redo log
,包括 undo log
中的修改那么 INSERT
和 DELETE
会怎么做呢?其实相比 UPDATE
这二者很简单,INSERT
会产生一条新纪录,它的 DATA_TRX_ID
为当前插入记录的事务 ID
;DELETE
某条记录时可看成是一种特殊的 UPDATE
,其实是软删,真正执行删除操作会在 commit
时,DATA_TRX_ID
则记录下删除该记录的事务 ID
。
快照读:
普通的select均为快照读,不用加锁; 当前读:
select… lock in shared mode: 读锁 select… for update: 写锁 DML(insert/delete/update):写锁 MVCC 只工作在RC & RR两个隔离级别下,Read Uncommited 直接读数据;Serializable 所有读都是当前读。
在RR级别下,快照读只能读取本事务开始之前的snapshot,反复读同一条记录,不会看到其他事务对它的更新动作;反复执行同一条查询,不会看到其他事务插入的新记录,也不会丢失其他事务删除的记录(删除并非立刻物理删除)。可以看到,RR级别下,普通的select没有不可重复读和幻读的问题。
快照读:简单的select操作,属于快照读,不加锁。(当然,也有例外,下面会分析) select * from table where ?;
当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。
select * from table where ? lock in share mode;
select * from table where ? for update;
insert into table values (…);
update table set ? where ?;
delete from table where ?;
所有以上的语句,都属于当前读,读取记录的最新版本。并且,读取之后,还需要保证其他并发事务不能修改当前记录,对读取记录加锁。其中,除了第一条语句,对读取记录加S锁 (共享锁)外,其他的操作,都加的是X锁 (排它锁)。
在RC级别下,快照读读取的是记录最新的snapshot,可以看到其他事务已提交的内容。
innoDB 是一致性非锁定读,如果读取的行被锁住了,innoDB会去读行的一个快照数据。
事务的隔离级别默认REPEATABLE-READ
InnoDB是一个支持行锁的存储引擎,锁的类型有:共享锁(S)、排他锁(X)、意向共享(IS)、意向排他(IX)。为了提供更好的并发,InnoDB提供了非锁定读:不需要等待访问行上的锁释放,读取行的一个快照。该方法是通过InnoDB的一个特性:MVCC来实现的。
#define LOCK_TABLE 16 /* table lock */
#define LOCK_REC 32 /* record lock */
/* Precise modes */
#define LOCK_ORDINARY 0
#define LOCK_GAP 512
#define LOCK_REC_NOT_GAP 1024
#define LOCK_INSERT_INTENTION 2048
记录锁]是最简单的行锁,并没有什么好说的。譬如下面的 SQL 语句(id 为主键):
mysql> UPDATE accounts SET level = 100 WHERE id = 5;
这条 SQL 语句就会在 id = 5 这条记录上加上记录锁,防止其他事务对 id = 5 这条记录进行修改或删除。记录锁永远都是加在索引上的,就算一个表没有建索引,数据库也会隐式的创建一个索引。如果 WHERE 条件中指定的列是个二级索引,那么记录锁不仅会加在这个二级索引上,还会加在这个二级索引所对应的聚簇索引上。
注意,如果 SQL 语句无法使用索引时会走主索引实现全表扫描,这个时候 MySQL 会给整张表的所有数据行加记录锁。如果一个 WHERE 条件无法通过索引快速过滤,存储引擎层面就会将所有记录加锁后返回,再由 MySQL Server 层进行过滤。不过在实际使用过程中,MySQL 做了一些改进,在 MySQL Server 层进行过滤的时候,如果发现不满足,会调用 unlock_row 方法,把不满足条件的记录释放锁(显然这违背了二段锁协议)。这样做,保证了最后只会持有满足条件记录上的锁,但是每条记录的加锁操作还是不能省略的。可见在没有索引时,不仅会消耗大量的锁资源,增加数据库的开销,而且极大的降低了数据库的并发性能,所以说,更新操作一定要记得走索引。
还是看上面的那个例子,如果 id = 5 这条记录不存在,这个 SQL 语句还会加锁吗?答案是可能有,这取决于数据库的隔离级别。
还记得我们在上一篇博客中介绍的数据库并发过程中可能存在的问题吗?其中有一个问题叫做 幻读,指的是在同一个事务中同一条 SQL 语句连续两次读取出来的结果集不一样。在 read committed 隔离级别很明显存在幻读问题,在 repeatable read 级别下,标准的 SQL 规范中也是存在幻读问题的,但是在 MySQL 的实现中,使用了间隙锁的技术避免了幻读。
间隙锁是一种加在两个索引之间的锁,或者加在第一个索引之前,或最后一个索引之后的间隙。有时候又称为范围锁(Range Locks),这个范围可以跨一个索引记录,多个索引记录,甚至是空的。使用间隙锁可以防止其他事务在这个范围内插入或修改记录,保证两次读取这个范围内的记录不会变,从而不会出现幻读现象。很显然,间隙锁会增加数据库的开销,虽然解决了幻读问题,但是数据库的并发性一样受到了影响,所以在选择数据库的隔离级别时,要注意权衡性能和并发性,根据实际情况考虑是否需要使用间隙锁,大多数情况下使用 read committed 隔离级别就足够了,对很多应用程序来说,幻读也不是什么大问题。
回到这个例子,这个 SQL 语句在 RC 隔离级别不会加任何锁,在 RR 隔离级别会在 id = 5 前后两个索引之间加上间隙锁。
值得注意的是,间隙锁和间隙锁之间是互不冲突的,间隙锁唯一的作用就是为了防止其他事务的插入,所以加间隙 S 锁和加间隙 X 锁没有任何区别。
下面我们针对大部分的SQL类型分析是如何加锁的,假设事务隔离级别为可重复读。
/* Basic lock modes */
enum lock_mode {
LOCK_IS = 0, /* intention shared */
LOCK_IX, /* intention exclusive */
LOCK_S, /* shared */
LOCK_X, /* exclusive */
LOCK_AUTO_INC, /* locks the auto-inc counter of a table in an exclusive mode*/
...
};
共享锁又称读锁,是读取操作创建的锁。其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。
如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。
SELECT ... LOCK IN SHARE MODE;
排他锁又称写锁,如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。
SELECT ... FOR UPDATE;
insert、update、delete
有索引的列加上行锁,会产生Gap锁,会在对应行,和左右两边的gap都加上锁
插入意向锁是一种特殊的间隙锁(所以有的地方把它简写成 II GAP),这个锁表示插入的意向,只有在 INSERT 的时候才会有这个锁。注意,这个锁虽然也叫意向锁,但是和上面介绍的表级意向锁是两个完全不同的概念,不要搞混淆了。插入意向锁和插入意向锁之间互不冲突,所以可以在同一个间隙中有多个事务同时插入不同索引的记录。譬如在上面的例子中,id = 1 和 id = 5 之间如果有两个事务要同时分别插入 id = 2 和 id = 3 是没问题的,虽然两个事务都会在 id = 1 和 id = 5 之间加上插入意向锁,但是不会冲突。
插入意向锁只会和间隙锁或 Next-key 锁冲突,正如上面所说,间隙锁唯一的作用就是防止其他事务插入记录造成幻读,那么间隙锁是如何防止幻读的呢?正是由于在执行 INSERT 语句时需要加插入意向锁,而插入意向锁和间隙锁冲突,从而阻止了插入操作的执行。
第一行表示已有的锁,第一列表示要加的锁
select .. from
不加任何类型的锁
select…from lock in share mode 在扫描到的任何索引记录上加共享的(shared)next-key lock,还有主键聚集索引加排它锁
select..from for update 在扫描到的任何索引记录上加排它的next-key lock,还有主键聚集索引加排它锁
update..where delete from..where 在扫描到的任何索引记录上加next-key lock,还有主键聚集索引加排它锁
insert into.. 简单的insert会在insert的行对应的索引记录上加一个排它锁,这是一个record lock,并没有gap,所以并不会阻塞其他session在gap间隙里插入记录。不过在insert操作之前,还会加一种锁,官方文档称它为insertion intention gap lock,也就是意向的gap锁。这个意向gap锁的作用就是预示着当多事务并发插入相同的gap空隙时,只要插入的记录不是gap间隙中的相同位置,则无需等待其他session就可完成,这样就使得insert操作无须加真正的gap lock。想象一下,如果一个表有一个索引idx_test,表中有记录1和8,那么每个事务都可以在2和7之间插入任何记录,只会对当前插入的记录加record lock,并不会阻塞其他session插入与自己不同的记录,因为他们并没有任何冲突。
假设发生了一个唯一键冲突错误,那么将会在重复的索引记录上加读锁。当有多个session同时插入相同的行记录时,如果另外一个session已经获得改行的排它锁,那么将会导致死锁。
IS
IX
意向锁是表级锁,其设计目的主要是为了在一个事务中揭示下一行将要被请求锁的类型。InnoDB中的两个表锁:
意向共享锁(IS):表示事务准备给数据行加入共享锁,也就是说一个数据行加共享锁前必须先取得该表的IS锁
意向排他锁(IX):类似上面,表示事务准备给数据行加入排他锁,说明事务在一个数据行加排他锁前必须先取得该表的IX锁。
意向锁是InnoDB自动加的,不需要用户干预。
意向锁是表级锁,但表示事务试图读或写某一行记录,而不是整个表。所以意向锁之间不会产生冲突,真正的冲突在加行锁时检查。
在给一行记录加锁前,首先要给该表加意向锁。也就是要同时加表意向锁和行锁。
AUTO_INC 锁又叫自增锁(一般简写成 AI 锁),它是一种特殊类型的表锁,当插入的表中有自增列(AUTO_INCREMENT)的时候可能会遇到。当插入表中有自增列时,数据库需要自动生成自增值,在生成之前,它会先为该表加 AUTO_INC 表锁,其他事务的插入操作阻塞,这样保证生成的自增值肯定是唯一的。AUTO_INC 锁具有如下特点:
显然,AUTO_INC 表锁会导致并发插入的效率降低,为了提高插入的并发性,MySQL 从 5.1.22 版本开始,引入了一种可选的轻量级锁(mutex)机制来代替 AUTO_INC 锁,我们可以通过参数 innodb_autoinc_lock_mode
控制分配自增值时的并发策略。参数 innodb_autoinc_lock_mode
可以取下列值:
Simple inserts、Bulk inserts、Mixed-mode inserts
。通过分析 INSERT 语句可以明确知道插入数量的叫做 Simple inserts
,譬如最经常使用的 INSERT INTO table VALUE(1,2) 或 INSERT INTO table VALUES(1,2), (3,4);通过分析 INSERT 语句无法确定插入数量的叫做 Bulk inserts
,譬如 INSERT INTO table SELECT 或 LOAD DATA 等;还有一种是不确定是否需要分配自增值的,譬如 INSERT INTO table VALUES(1,’a’), (NULL,’b’), (5, ‘C’), (NULL, ‘d’) 或 INSERT … ON DUPLICATE KEY UPDATE,这种叫做 Mixed-mode inserts
。读锁,又称共享锁(Share locks,简称 S 锁),加了读锁的记录,所有的事务都可以读取,但是不能修改,并且可同时有多个事务对记录加读锁
写锁,又称排他锁(Exclusive locks,简称 X 锁),或独占锁,对记录加了排他锁之后,只有拥有该锁的事务可以读取和修改,其他事务都不可以读取和修改,并且同一时间只能有一个事务加写锁。(注意:这里说的读都是当前读,快照读是无需加锁的,记录上无论有没有锁,都可以快照读)
表锁锁定了整张表,而行锁是锁定表中的某条记录,它们俩锁定的范围有交集,因此表锁和行锁之间是有冲突的。譬如某个表有 10000 条记录,其中有一条记录加了 X 锁,如果这个时候系统需要对该表加表锁,为了判断是否能加这个表锁,系统需要遍历表中的所有 10000 条记录,看看是不是某条记录被加锁,如果有锁,则不允许加表锁,显然这是很低效的一种方法,为了方便检测表锁和行锁的冲突,从而引入了意向锁。
意向锁为表级锁,也可分为读意向锁(IS 锁)和写意向锁(IX 锁)。当事务试图读或写某一条记录时,会先在表上加上意向锁,然后才在要操作的记录上加上读锁或写锁。这样判断表中是否有记录加锁就很简单了,只要看下表上是否有意向锁就行了。意向锁之间是不会产生冲突的,也不和 AUTO_INC 表锁冲突,它只会阻塞表级读锁或表级写锁,另外,意向锁也不会和行锁冲突,行锁只会和行锁冲突。
mysql InnoDB
引擎默认的修改数据语句,update,delete,insert
都会自动给涉及到的数据加上排他锁,select
语句默认不会加任何锁类型。
死锁的发生与否,并不在于事务中有多少条SQL语句,死锁的关键在于:两个(或以上)的Session加锁的顺序不一致。而使用本文上面提到的,分析MySQL每条SQL语句的加锁规则,分析出每条语句的加锁顺序,然后检查多个并发SQL间是否存在以相反的顺序加锁的情况,就可以分析出各种潜在的死锁情况,也可以分析出线上死锁发生的原因。
判断索引范围
http://blog.csdn.net/cug_jiang126com/article/details/50596729
http://novoland.github.io/%E6%95%B0%E6%8D%AE%E5%BA%93/2015/08/17/InnoDB%20%E9%94%81.html
http://hedengcheng.com/?p=771#_Toc374698307
https://www.aneasystone.com/archives/2017/11/solving-dead-locks-two.html
https://zhuanlan.zhihu.com/p/64576887