SQl标准定义的四个隔离级别为:
- reaad uncommited
- reaad commited
- repeatable read
- serializable
隔离性有多种实现方式,然而,最广为人知的实现方式就是加锁技术,因为其较容易为人所理解且能满足并发控制的两个准则。
- 当数据库系统中并发事物各自运行时,每个事务的运行不受其他事务的影响。
- 要求使用一种简单的算法或者开销较小的方式实现加锁技术。
MySQL(此处研究InnoDB引擎)中对于不同级别的隔离程度,可以采用不同粒度的锁实现,从而提高了系统的并发性。
InnoDB存储引擎默认的 支持隔离级别时repeatable read,但是与标准SQL不同的是,InnoDB存储引擎在repeatable read事务隔离级别下,使用Next-Key Lock锁的算法,因此避免了幻读的产生。所以,InnoDB存储引擎在默认的repeatable read的事务隔离级别下已经能完全保证事务的隔离性要求,即达到SQL标准的serializable隔离级别。
根据Jim Gray在Transaction Processing:Concepts and Techniques一书中指出,repeatable read级别与serializable级别隔离度的开销几乎是一样的,甚至serializable可能更优!因此在InnoDB存储引擎中选择repeatable read的事务隔离级别并不会有任何性能的损失。同样的,即使使用read committed的隔离级别,用户的性能也不会得到大幅度的提升。
一、InnoDB中锁的类型介绍:
1.1 行级锁(基于InnoDB引擎)
(1)共享锁(Shared Lock(S锁))
当一个事务A已经获得了行r的共享锁,那么另外的事务B可以立即获得行r的共享锁,因为读取并没有改变行r的数据,称这种情况为“锁兼容”。共享锁是为了让当前事务去读一行数据,而读不会改变数据,所以各个事务之间可以并发读取数据。
设置共享锁:select * from user where id = 1 LOCK IN SHARE MODE;
(2)排他锁(Exclusive Locks(X锁))
兼容性:该记录行加了X锁的记录,不允许其他事务再向此表加S锁或者X锁,对于 insert、update、delete操作,InnoDB会自动给其加上排他锁,对于select操作需要手动设置排他锁。既涉及到修改数据表中的数据时,排他锁就不允许其他事务对此数据表进行访问(为什么是不让访问这个数据表?看下文的意向排他锁)。
设置排他锁:select * from user where id = 1 FOR UPDATE;
(3)S锁与X锁的兼容性
当事务A对某个数据范围(行或表)上了“某锁”后,另一个事务B是否能在这个数据范围上“某锁”
线程A/线程B | X | S |
---|---|---|
X | 不兼容 | 不兼容 |
S | 不兼容 | 兼容 |
(4)行级锁说明
可先看1.3。
行级锁是对该行记录中的索引进行加锁,并且在加行锁之前已取得数据表中相应的意向锁(下文介绍),无论此事务加了何种行级锁,其他事务仍可以对该数据表的其他行数据进行任意操作而不会被阻塞。其对表添加意向锁主要是阻塞其他事务再对表添加一些表锁(如下段所示),尤其是表级别的排他锁(表独占写锁),故能完整维护表独占锁的语义(可修改数据表内的任意行数据)。
由于InnoDB预设是Row-Level Lock,所以只有明确指定主键(索引),MySQL才会执行Row lock (只锁住被选取的记录行) ,否则MySQL将会执行Table Lock (将整个表给锁住)。即只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!(通过非主键索引检索数据时,例如在student表中,id是每个学生的编号,设为主键,但检索条件为学生名字,如:select * from student where name=‘Li Hua’ for update,此时不满足上述行级锁加锁条件,则给整张表加锁。)
对于上述出现的对表加锁的情况,在此点内说明,就不于表级锁中阐述了。InnoDB中仅提供了意向共享锁(IS)、意向排他锁(IX)以及自增锁(AI),对于此处的表锁交由MySQL Server处理。 MySQL表级锁有两种模式:表共享读锁(Table Read Lock)和表独占写锁(Table Write Lock)。什么意思呢,就是说对MyISAM表进行读操作时,它不会阻塞其他用户对同一表的读请求,但会阻塞 对同一表的写操作。
说明:此处我没弄清楚没有明确主键(索引)时到底是使用行级排他锁住了表的所有行(每行都有一个行排他锁),还是把控制权交给MySQL Server来使用整个表的表独占写锁,这两种方法截然不同,但呈现效果是一样的,姑且把这种锁定效果称为表级排他锁。
举个例子: 假设有个表单products ,里面有id跟name二个栏位,id是主键。
例1: (明确指定主键,并且有此笔资料,row lock)
SELECT * FROM products WHERE id=‘3’ FOR UPDATE;
SELECT * FROM products WHERE id=‘3’ and type=1 FOR UPDATE;
例2: (明确指定主键,若查无此笔资料,无lock)
SELECT * FROM products WHERE id=’-1’ FOR UPDATE;
例3: (无主键,table lock)
SELECT * FROM products WHERE name=‘Mouse’ FOR UPDATE;
例4: (主键不明确,table lock)
SELECT * FROM products WHERE id<>‘3’ FOR UPDATE;
例5: (主键不明确,table lock)
SELECT * FROM products WHERE id LIKE ‘3’ FOR UPDATE;
(5)一些典型情况的实验
接下来看一些典型情况:
情况1:
事务A对id=1的行加排他锁(同时表自动被加意向排他锁):
select * from user where id=1 for update;
事务A创建完毕后先不提交事务,事务B此时对id=1的行加排他锁或共享锁
select * from user where id=1 for update;
select * from user where id=1 lock in share mode;
事务B查询失败,事务A排他锁的存在使事务B阻塞。
情况2:
事务A对id=1的行加排他锁(同时已取得表的意向排他锁):
select * from user where id=1 for update;
事务A创建完毕后先不提交事务,事务B此时对id=2的行加排他锁或共享锁
select * from user where id=2 for update;
或 select * from user where id=2 lock in share mode;
事务B查询成功,事务之间不同行的行级锁不会互相阻塞。
(行级共享锁比较简单,这里不再提起)
情况3:
事务A对id=1的行加排他锁(同时已取得表的意向排他锁):
select * from user where id=1 for update;
事务A创建完毕后先不提交事务,事务B对数据表加表级排他锁及共享锁(不使用索引即可,Mysql server提供表级锁排他锁及共享锁):
select * from user where name='lihua' for update;
或 select * from user where name='lihua' lock in share mode;
或 select * from user for update;
或 select * from user lock in share mode;
事务B查询失败,行级排他锁会在表上加一个意向排他锁,其他事务再向表上加锁时被阻塞。在事务锁定表来修改数据时,会其他事务对该表的所有并发读写操作。
情况4:
事务A对id=1的行加共享锁(同时已取得表的意向共享锁):
select * from user where id=1 lock in share mode;
事务A创建完毕后先不提交事务,事务B对数据表加表级排他锁及共享锁(不使用索引即可,Mysql server提供表级锁排他锁及共享锁):
select * from user where name='lihua' for update;
或 select * from user where name='lihua' lock in share mode;
或 select * from user for update;
或 select * from user lock in share mode;
事务B中关于排他锁的操作失败,共享锁的操作成功。表级共享锁可阻塞其他事务修改行数据,对于其他事务进行并发读取还是可以的。
由此亦可知:当先在事务A中向数据表加表级排他锁时,事务B的任何操作(无论是行级锁还是表级锁都会被阻塞);当先在数据表加表级共享锁时,事务B的任何排他锁操作失败,共享锁操作成功。
情况5:
事务A对加锁(无论何种锁):
select * from user where id=1 for update;
或 select * from user where id=1 lock in share mode;
或 select * from user where name='lihua' for update;
或 select * from user where name='lihua' lock in share mode;
或 select * from user for update;
或 select * from user lock in share mode;
事务A创建完毕后先不提交事务,事务B对数据表不加锁查询
select * from user where id=1 ;
或 select * from user where id=1;
或 select * from user where name='lihua' ;
或 select * from user where name='lihua';
或 select * from user ;
或 select * from user;
事务B操作均成功。参照1.2。
1.2 无锁
对于共享锁大家可能很好理解,就是多个事务只能读数据不能改数据,对于排他锁大家的理解可能就有些差别,我当初就犯了一个错误,以为排他锁锁住一行数据后,其他事务就不能读取和修改该行数据,其实不是这样的。排他锁指的是一个事务在一行数据加上排他锁后,其他事务不能再在其上加其他的锁。mysql InnoDB引擎默认的修改数据语句,update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型,所以加过排他锁的数据行在其他事务中是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制。
Tips:
对于select 语句(没有指定加锁),innodb不会加任何锁,也就是可以多个并发去进行select的操作,不会有任何的锁冲突,因为根本没有锁。
对于insert,update,delete操作,innodb会自动给涉及到的数据加排他锁,只有查询select需要我们手动设置排他锁。
1.3表级锁
(1)意向共享锁
表示事务准备给数据行加入共享锁,也就是说一个数据行加共享锁前必须先取得该表的 IS 锁。如果需要对记录 A 加共享锁,那么此时 InnoDB 会先找到这张表,对该表加意向共享锁之后,再对记录 A 添加共享锁。
当一个事务对表加了意向排他锁时,另外一个事务在加锁前就会通过该表的意向排他锁知道前面已经有事务在对该表进行独占操作,从而等待。
Tips:
Q1:为什么意向锁是表级锁呢?
答:当我们需要加一个排他锁时,需要根据意向锁去判断表中有没有数据行被锁定(行锁);
- 如果意向锁是行锁,则需要遍历每一行数据去确认;
- 如果意向锁是表锁,则只需要判断一次即可知道有没数据行被锁定,提升性能。
Q2:意向锁怎么支持表锁和行锁并存?
- 首先明确并存的概念是指数据库同时支持表、行锁,而不是任何情况都支持一个表中同时有一个事务A持有行锁、又有一个事务B持有表锁,因为表一旦被上了一个表级的写锁,肯定不能再上一个行级的锁。
- 如果事务A对某一行上锁,其他事务就不可能修改这一行。这与“事务B锁住整个表就能修改表中的任意一行”形成了冲突。所以,没有意向锁的时候,让行锁与表锁共存,就会带来很多问题。于是有了意向锁的出现,如q1的答案中,数据库不需要在检查每一行数据是否有锁,而是直接判断一次意向锁是否存在即可,能提升很多性能。
(2)意向排他锁
类似上面,表示事务准备给数据行加入排他锁,也就是说事务在给一个数据行加排他锁前必须先取得该表的 IX 锁。如果需要对记录 A 加排他锁,那么此时 InnoDB 会先找到这张表,对该表加意向排他锁之后,再对记录 A 添加排他锁
(3)自增锁(AUTO-INC Locks)
自增锁是事务插入到有自增列的表中而获得的一种特殊的表级锁。如果一个事务正在向表中插入值,那么任何其他事务都必须等待,保证第一个事务插入的行是连续的自增值。
(4)表级锁的兼容性表
事务A/事务B | 意向共享 | 意向排他 | 表级共享 | 表级排他 | 自增锁 |
---|---|---|---|---|---|
意向共享 | 兼容 | 兼容 | 兼容 | 不兼容 | 兼容 |
意向排他 | 兼容 | 兼容 | 不兼容 | 不兼容 | 兼容 |
表级共享 | 兼容 | 不兼容 | 兼容 | 不兼容 | 不兼容 |
表级排他 | 不兼容 | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
自增锁 | 兼容 | 兼容 | 不兼容 | 不兼容 | 不兼容 |
意向锁都是互相兼容的,因为其是因为行级别的锁产生的,各事务行级锁只要不在同一行上就不会产生阻塞,故由其产生的意向锁也是兼容的。其他锁的兼容性可从上分析得到。
1.4 行级锁的锁介绍
(1)记录锁(record Lock)
- 记录锁, 仅仅锁住索引记录的一行,在单条索引记录上加锁。
- record lock锁住的永远是索引,而非记录本身,即使该表上没有任何索引,那么innodb会在后台创建一个隐藏的聚集主键索引,那么锁住的就是这个隐藏的聚集主键索引。
记录锁是索引记录上的锁,例如:SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE;
会阻止其他事务对c1=10的数据行进行更新、删除等操作。
所以说当一条sql没有走任何索引时,那么将会在每一条聚合索引后面加X锁,这个类似于表锁,但原理上和表锁应该是完全不同的。
(2)间隙锁(gap Lock)
间隙锁是一个在索引记录之间的间隙上的锁。
- 区间锁, 仅仅锁住一个索引区间(开区间,不包括双端端点)。
- 在索引记录之间的间隙中加锁,或者是在某一条索引记录之前或者之后加锁,并不包括该索引记录本身。比如在 1、2、3中,间隙锁的可能值有 (∞, 1),(1, 2),(2, ∞)
- 间隙锁可用于防止幻读,保证索引间的不会被插入数据
一个间隙可能跨越单个索引值、多个索引值,甚至为空。
对于使用唯一索引 来搜索唯一行的语句,只加记录锁不加间隙锁(这并不包括组合唯一索引)。
(3)临键锁(Next-key Lock)
Next-Key Locks是行锁与间隙锁的组合。当InnoDB扫描索引记录的时候,会首先对选中的索引记录加上记录锁(Record Lock),然后再对索引记录两边的间隙加上间隙锁(Gap Lock)。
- record lock + gap lock, 左开右闭区间
- 默认情况下,innodb使用next-key locks来锁定记录。select … for update
- 但当查询的索引含有唯一属性的时候,Next-Key Lock会进行优化,将其降级为Record Lock,即仅锁住索引本身,不是范围
- Next-Key Lock在不同的场景中会退化
1.5 其他概念
锁冲突:例如说事务A将某几行上锁后,事务B又对其上锁,锁不能共存否则会出现锁冲突。(但是共享锁可以共存,共享锁和排它锁不能共存,排它锁和排他锁也不可以)
死锁:例如说两个事务,事务A锁住了1~5
行,同时事务B锁住了6~10
行,此时事务A请求锁住6~10
行,就会阻塞直到事务B施放6~10
行的锁,而随后事务B又请求锁住1~5
行,事务B也阻塞直到事务A释放1~5
行的锁。死锁发生时,会产生Deadlock错误。
二、锁的内存实现
三、隐式锁和显式锁
Lock 是一种悲观的顺序化机制。它假设很可能发生冲突,因此在操作数据时,就加锁。如果冲突的可能性很小,多数的锁都是不必要的。而且InnoDB 的一个锁结构的开销是比较大的。 或者说InnoDB 锁一条记录的开销,与锁一个页面中所有记录的开销是一样的。故InnoDB实现了一个延迟加锁的机制,来减少加锁的数量,在代码中称为隐式锁(Implicit Lock),通过 Implicit 方式加锁,极大的减轻了锁模块开销。
隐式锁加锁流程
- InnoDB的每条记录中都一个隐含的trx_id字段,这个字段存在于簇索引的B+Tree中。
- 在操作一条记录前,首先根据记录中的trx_id检查对应事务是否是活动事务(未提交或回滚)。如果是活动的事务,首先将隐式锁转换为显式锁,创建显示锁结构,并加入到lock hash table中;如果事务非活跃,则按照该语句的正常语法流程进行。
- 检查两个事务间是否有锁冲突,如果有冲突,创建锁(应该是后访问该行记录(称为新事务)的事务创建了一个类自旋锁吧,自旋锁不断尝试对该行记录加锁,自旋锁加锁成功,说明旧事务结束,新事务被唤醒;自旋锁加锁失败,则旧事务还未被提交或回滚,新的事务继续阻塞等待),并设置为waiting状态。如果没有冲突不加锁,跳到E。
- 等待加锁成功,被唤醒,或者超时。
- 写数据,并将自己的trx_id写入trx_id字段。Page Lock可以保证操作的正确性。
隐式锁的特点
-
只有在很可能发生冲突时才加锁,减少了锁的数量。
-
隐式锁是针对被修改的B+Tree记录,因此都是Record类型的锁。不可能是Gap或Next-Key类型。
隐式锁的使用
-
INSERT操作只加隐式锁,不需要显示加锁。
-
UPDATE,DELETE在查询时,直接对查询用的Index和主键使用显示锁,其他索引上使用隐式锁。
理论上说,可以对主键使用隐式锁的。提前使用显示锁应该是为了减少死锁的可能性。
INSERT,UPDATE,DELETE对B+Tree们的操作都是从主键的B+Tree开始,因此对主键加锁可以有效的阻止死锁。
非聚集索引的隐式锁
前边说了, trx_id只存在于主键上,那么辅助索引上如何来实现隐式索引呢?
显然是要通过辅助索引中的主键值,在主键B+Tree上进行二次查找。这个开销是很大的。
InnoDB对这个过程有一个优化:
A. 每个页上有一个MAX_TRX_ID,每次修改辅助索引的记录时,都会更新这个最大事务ID。
B. 当判断是否要将隐式锁变为显式锁时,先将页面的max_trx_id和事务列表的最小trx_id比较。如果max_trx_id比事务列表的最小trx_id还小,那么就不需要转换为显示锁了。