事务隔离级别
什么是事务
WHAT
事务(Transaction)是一组SQL组成的执行单元(Unit),是数据库并发控制和恢复回滚的基本单位。一个事务中可能包含多个SQL,要么都失败,要么都成功。
最典型的业务场景就是转账,比如张三给李四转账,简化步骤如下:查询张三余额、张三账户减去转账金额、将张三最新余额写入账户、查询李四余额、李四账户加上转账余额、将李四最新余额写入账户。这是一个完整的事务,里面包含了多个SQL,要么都成功,转账完成;要么任何一个环节不成功,转账就失败(比如张三余额扣成功了,李四余额没加成功,张三亏了。再比如,张三余额没扣成功,李四余额加成功了,银行亏了。这年代,都不“傻”,这种事情任何一方都不愿意发生,除了李四)。
如果要满足上述要求,事务就具备了以下四个基本特性:
原子性(Atomicity):每个事务都是一个整体,不可分隔。事务的原子性确保动作要么全部都成功,要么全部都失败。
一致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。
隔离性(Isolation):多个用户并发访问操作数据库时,一个用户的事务操作不能被其它的用户事务所干扰,也就是多个并发事务之间操作是隔离的。
持久性(Durability):一个事务一旦提交成功,那么对数据库数据的修改是持久化存储的。
这就是大家耳熟能详的事务ACID特性。
MySQL 8.0,对表的DDL可以支持原子性,但是并不支持事务,比如RENAME表名,要么都成功,要么都失败。举例:DROP TABLE a,b;比如b表不存在,会报错,那么对a表的操作也会回滚,也就是a表也不会被删除。
原子性不代表是事务,原子性不是事务的全部,原子性只是事务的一部分特征。
事务隔离级别
ISOLATION
在某一时刻,不可能只有张三给李四转账这一笔交易,肯定会有很多人同时产生很多笔交易。在关系型数据库中,进行并发操作的同时还需要满足事务隔离性的要求。
事务的隔离性要求每个读/写事务对象对其他事务的操作对象能相互分离,即该事务提交前对其他事务都不可见。若违反了数据库的隔离性要求,从而可能导致问题的产生。
但是令人惊讶的是,大部分数据库系统都没有提供真正的隔离性,最初或许是因为系统实现者并没有真正理解这些问题,如今这些问题已明,但是数据库实现者在正确性和性能之间做了妥协。ISO和ANSI SQL标准制定了4种事务隔离级别的标准,但是很少有数据库厂商完全遵循这些标准,比如Oracle就不支持READ UNCOMMITTED和REPEATABLE READ的事务隔离级别。好在一点,MySQL都支持。
SQL标准定义的四个隔离级别为:
READ UNCOMMITTED(读未提交);
READ COMMITTED(读已提交);
REPEATABLE READ(可重复读);
SERIALIZABLE(序列化/串行化);
事务隔离级别从上到下依次升高(即隔离级别越来越严格),因为兼容了数据的准确性和性能,“越宽松”的事务隔离级别就会出现以下问题:
脏读(Dirty Read):当前事务可以查看到其他事务正在进行但未提交的数据。
幻读(Phantom Read):当前事务可以查看到其他事务已提交新插入的数据。
不可重复读(Unrepeatable Read):其他事务修改了当前事务查到的数据,并提交,当前事务再次查询时,查询到的是其他事务提交修改后的数据。
如图所示(即不同隔离级别下可以解决上述问题的情况):
锁(Lock)简介
什么是锁
WHAT
锁(Lock)是实现并发操作的前提。在数据库系统中,与锁互为等价的概念有:并发控制(Concurrency Control)、序列化(Serializability)、隔离性(Isolation)。通俗来讲,锁就是用来实现事务一致性和隔离性的一种常用技术。
最简单的加锁技术就是对每个要访问的对象加上一个锁,当事务访问一个对象,数据库自动请求并加上一个锁,在事务结束后释放,若该请求对象上已经被其他事务持有锁,则该事务需要等待对象上的锁释放。由此可见,锁是串行的机制,用来保证同一时刻一个对象仅能被一个事务访问。
通过多粒度(fine granular)锁可以用来提高数据库系统的并发性。例如,MyISAM存储引擎就是表锁设计,并发情况下,读没有影响,但是插入的性能就会差一些。InnoDB存储引擎锁的实现和Oracle数据库非常类似,提供做一致性的非锁定读、行级锁支持。这里说的表锁、行锁就是锁的两种不同粒度。再比如Microsoft SQL Server也可以支持到行锁级别,但是当行锁超过5000个以后可能会升级成页(Page)锁、表锁,所以SQL Server还会涉及到锁升级的问题。
什么是latch
WHAT
latch,闩(shuān)锁,直译过来就是锁,但latch的作用是用于控制内存中的数据结构并发访问的(通俗来讲,就是保护内存中数据结构完整性的)。与数据库的锁(Lock)不同,数据库中的锁对象不是内存结构,锁住的是一行一行的记录,而latch锁住的是并发资源的对象,也称作临界区。并且二者持续的时间不一样,Lock是贯穿整个事务,事务提交了,Lock才会释放。其实任何系统中都有latch,无处不在,所以大家一定要区分清楚两者(是没有太大关系的)。
InnoDB存储引擎中的锁
共享锁和排他锁
S LOCK & X LOCK
InnoDB存储引擎支持两种标准的行级锁:
共享锁(Shared Lock → S Lock):允许持有该锁的事务读取一行数据。
排他锁(Exclusive Lock → X Lock):允许持有该锁的事务更新或删除一行数据 。
如果事务T1在行记录r上持有共享(S)锁,另外一个事务T2可以立即获得该行记录r的共享(S)锁,因为读取数据的操作并没有改变行记录r本身,这种情况称之为锁兼容(Lock Compatible,√)。
如果另一个事务T3想获取行记录r的排他(X)锁,则其必须等待事务T1、T2释放行记录r的共享(S)锁,这种情况就称之为锁不兼容(Lock Conflict,×)。
针对上面描述的情况,我们用一张兼容性矩阵来说明:(X:排他锁,S:共享锁)
意向锁
INTENTION LOCK
InnoDB存储引擎支持多粒度锁定(multiple granularity locking),允许行锁和表锁共存。为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持一种额外的锁方式,称之为意向锁(Intention Lock)。意向锁是表级别(table-level)的锁,设计的主要目的就是将锁定的对象分为多个层次,在一个事务中揭示下一层级将被请求的锁类型。目前有两种类型的意向锁:
意向共享锁(Intention Shared Lock → IS Lock):事务想要获得一张表中某几行数据的共享(S)锁。
意向排他锁(Intention Exclusive Lock → IX Lock):事务想要获得一张表中某几行数据的排他(X)锁。
在事务可以获取表中行的共享锁之前,它必须首先获取表上的IS锁或更高层级的锁。
在事务可以获取表中行的排他锁之前,它必须首先获取表上的IX锁。
因为InnoDB存储引擎支持的是行级别的锁,因此意向锁不会阻塞除全表扫描以外的任何请求。故得到了表级别(table-level)意向锁与行级锁的兼容型矩阵:(IS:意向共享锁,IX:意向排他锁,X:排他锁,S:共享锁)
由图可得,所有的意向锁之间都是兼容的,无论是意向共享锁(IS Lock)与自身之间、意向排他锁(IX Lock)与自身之间,还是意向共享锁(IS Lock)间与意向排他锁(IX Lock)间,所有事务间均不会被阻塞,都是兼容的。
记录锁
RECORD LOCK
记录锁(Record Lock)是锁在索引记录上的。比如,SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE;防止任何其他事务插入、更新或删除t.c1值为10的行。记录锁就是锁定索引记录,即使定义的表没有索引,这种情况下,InnoDB会创建一个隐藏的聚簇索引,并使用此索引进行记录锁定(还记得MySQL之InnoDB记录结构一文中那个row_id吗,就是它(如果表结构定义了PRIMARY KEY,PK就是row_id,否则,会生成一个6字节长度的虚拟列row_id来组织B+树),还有B+树“索引即数据,数据即索引”的数据结构,大家没有忘记吧,如果忘记,请移步至MySQL之B+树索引 )。
记录锁显示如下:
mysql> SHOW ENGINE INNODB STATUS\G.........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 gapRecord 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 LOCK
间隙锁(Gap Lock)是锁在索引记录之间“间隙”上的锁,或者锁在第一个索引记录之前或者最后一个索引记录之后。例如,SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;防止其他事务在t.c1列中插入15的值,无论列中是否已经存在这个值,都不会被插入,因为范围内所有现有值之间的间隙都被锁定,间隙锁的锁定范围都为开区间(10,20)。
Gap Lock的特点:
间隙可能跨越单个索引值、多个索引值,甚至为空。
间隙锁是性能和并发权衡的一部分,用于某些事务隔离级别,而不是其他级别。
唯一索引上定位某条记录加锁时,不需要间隙锁(但是不包括搜索条件包含多列唯一索引某些列的情况,这种情况下,还是会发生间隙锁)。
如果id字段没有索引或非唯一的索引,该语句确实会锁定记录前面的间隙。
这里还值得注意的是,不同的事务可以在间隙上持有冲突的锁。例如,事务A可以在一个间隙上持有一个共享间隙锁(Gap S-lock),而事务B在同一个间隙上持有一个排他间隙锁(Gap X-lock)。允许冲突间隙锁的原因是,如果从索引中清除记录,则必须合并不同事务在记录上持有的间隙锁。
InnoDB存储引擎中的间隙锁是“纯粹的抑制”,这意味着它们的唯一目的是防止其他事务的数据插入到当前事务的间隙中。间隙锁可以共存。一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用间隙锁。共享(Gap S-lock)和排他间隙锁(Gap X-lock)之间没有区别。它们彼此不冲突,并且执行相同的功能。
可以显式禁用间隙锁定。如果事务隔离级别更改为READ COMMITTED或启用innodb_locks_unsafe_for_binlog系统变量(现已弃用),这种情况下,不会产生间隙锁。但在这种情况下,间隙锁不会用于搜索和索引扫描,仅用于外键约束检查和重复键检查(官档的这段描述在后面文章分析加锁原理时会有大用,这里大家先着重关注一下这句话)。
使用READ COMMITTED隔离级别或启用innodb_locks_unsafe_for_binlog还有其他影响。在MySQL评估WHERE条件后,匹配不到的行记录锁将被释放。对于UPDATE语句,InnoDB会采取“半一致性”(semi-consistent)读取,以便将最新提交的版本返回给MySQL,以便MySQL可以确定该行是否UPDATE的WHERE条件匹配。
临键锁
NEXT-KEY LOCK
临键锁(Next-key Lock)是索引记录上的记录锁和索引记录之前的间隙锁的组合。
InnoDB执行行级锁定的方式是,当它搜索或扫描表索引时,它会在它遇到的索引记录上设置共享(S)或排他(X)锁。因此,行级锁(row-level)实际上是索引记录锁。索引记录上的Next-key锁也会影响该索引记录之前的“间隙”。也就是说,Next-key 锁是索引记录锁加上索引记录前面的间隙上的间隙锁。如果一个会话在索引中的记录R上具有共享(S)或排他(X)锁,则另一个会话不能在索引顺序中R之前的间隙中插入新的索引记录。
假设索引包含值10、11、13和20。此索引Next-key锁涵盖以下区间(通常是左闭右开):
(-∞/infimum, 10](10, 11](11, 13](13, 20](20, +∞/supremum)
插入意向锁
INSERT INTENTION LOCK
自增锁
AUTO-INC LOCK
自增锁(AUTO-INC Lock)是插入带有AUTO_INCREMENT列的表的事务所采用的特殊表(table-level)级锁。在最简单的情况下,如果一个事务正在向表中插入值,则任何其他事务都必须等待自己向该表中插入,以便第一个事务插入的行接收连续的主键值。
innodb_autoinc_lock_mode
变量控制用于控制自增锁的算法。它允许您选择如何在可预测的自动增量值序列和插入操作的最大并发性之间进行权衡。
空间索引的谓词锁
PREDICATE LOCK
MySQL Server层的锁
元数据锁
METADATA LOCK
元数据锁(Metadata Lock,MDL)是MySQL 5.5版本引入的,用于解决或者保证DDL操作与DML等操作之间的一致性。在MySQL之Online DDL一文中,我们有过简单的介绍。怎么理解保证DDL操作和DML操作之间一致性这句话呢?举个栗子🌰,比如正在使用mysqldump做备份的过程中,我对一张表(原始5个字段)加了一个字段,备份出来的数据前1000行是5列,后面的数据是6列,这样的备份肯定是不行的。若没有MDL锁的保护,就会出现这样的问题。
InnoDB存储引擎层已经有IS、IX这样的表(table-level)级别锁了,应该可以实现上述情况的并发控制。但是MySQL的架构是分层设计,存储引擎层作为插件(Plugin)的方式接入到MySQL的Server层,所以,MDL锁是在Server层中实现的。
小结
READ UNCOMMITTED(读未提交); READ COMMITTED(读已提交); REPEATABLE READ(可重复读); SERIALIZABLE(序列化/串行化);
同时,我们还介绍了锁的概念、latch、MySQL Server层的元数据(MDL)锁,还有最重要的InnoDB存储引擎中的8种锁:
共享锁和排他锁(Shared Lock & Exclusive Lock):两者属于行(row-level)级别锁;
意向锁(Intention Lock):意向锁又分为意向排他锁(Intention Exclusive Lock)和意向共享锁(Intention Shared Lock),两者属于表(table-level)级别锁;锁的兼容型矩阵如下:
记录锁(Record Lock):行(row-level)级别锁,锁在索引记录上;
间隙锁(Gap Lock):锁在索引记录之间“间隙”上的锁,锁定范围是开区间;
临键锁(Nexy-key Lock):锁在当前记录和记录前间隙上的锁,锁定范围是左开右闭,“supremum”最后一个伪记录是开区间;
插入意向锁(Insert Intention Lock):在插入行之前由INSERT操作设置的间隙锁;
自增锁(AUTO-INC Lock):插入带有AUTO_INCREMENT列的表的事务所采用的特殊表(table-level)级锁。
谓词锁(Predicate Lock):未找到相关资料,很少使用,不再赘述。
今天的内容主要是以概念为主,大家了解概念即可,可能有比较晦涩的字眼,不要着急,后面的文章会对每一种锁进行详细讲解和说明,讲解过程中,也会提及、补充今天的内容,所以大家对这今天的这部分内容有一个印象就好了。
今天的内容就到这里,每天进步一点点,我们下篇见!~
参考资料
https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html
https://dev.mysql.com/doc/refman/5.7/en/performance-schema-metadata-locks-table.html
姜承尧《MySQL内核:InnoDB存储引擎 卷I》
InsideMySQL公众号-姜承尧-《深入理解MySQL的MDL元数据锁》
扫描二维码关注
获取更多精彩
GrowthDBA
end