欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

InnoDB中事务隔离级别和锁的关系

程序员文章站 2022-06-07 13:06:41
...

1.锁的分类(一次*或两段锁)

a.一次*

就是在方法的开始阶段,已经预先知道要用到那些数据,在方法开始之前就将这些数据用锁锁住,在方法执行完成之后,再全部解锁。这种方式可以有效的避免死锁,但是这种*方式在数据库中并不可用,因为在事务开始阶段,并不会提前预知要使用数据库中的哪些数据。因此数据库遵循两段锁协议。
b. 两段锁

就是将事务分为加锁阶段和解锁阶段两个阶段。

加锁阶断:在这个阶段,可以进行加锁操作。在对事务进行"读"的操作之前,需要申请S锁(共享锁),对事务进行"写"的操作之前,需要申请X锁(排它锁)。在这里要注意的是,如果事务加上S锁,其他事务可以继续加共享锁,但不能加排它锁;但是如果事务加上X锁,其他事务则不能加其他任何类型的锁。如果事务加锁不成功,则事务进入wait状态,直至枷锁成功为止。
   

解锁阶断:当事务提交之后,就会释放锁,此时事务进入解锁阶断,在这个阶段只能进行解锁操作,不能再进行枷锁操作。
    两段锁无法避免死锁,但是却可以保证事务的并发操作是串行化进行的。

2.Mysql中锁的种类

a.表锁:对整张表进行加锁
b.行锁:锁住数据行,对有限的数据行进行加锁,对其他数据不限制。
在这里要注意的是如果查询条件中存在没有建索引的字段,Mysql则会给数据库中整张表的所有数据行加行锁,这时存储引擎会将所有的数据记录加锁返回给MysqlServer层进行过滤,在MysqlServer层过滤的时候,如果发现有不满足过滤条件的记录,则会调用unlock_row方法,将不满足条件的记录所加的锁释放掉,当然这种做法是违背了"两段锁协议"的。

3.事务的隔离级别

隔离级别

脏读

不可重复读

幻读

读未提交(ReadUncommitted)

可能

可能

可能

读已提交(ReadCommitted)

不可能

可能

可能

可重复读( Repeateable Read)

不可能

不可能

可能

可串行化(Serializable)

不可能

不可能

不可能

a.读未提交(RU):就是可能会读取到其他会话中未提交事务的数据。
RU级别中,无论是读的操作还是写的操作,都不会进行加锁操作
b.读已提交(RC):只能读到已经提交事务的数据。Oracle默认的隔离级别为该级别。
RC级别中,数据的读取操作不会进行加锁,但是数据的写入、修改和删除操作是需要加锁的。
c.可重复读(RR):指同一个事务的多个实例在并发读取数据时看到的数据行是一致的。InnoDB引擎默认为该级别。
RR级别中, 数据的读取操作不会进行加锁,但是数据的写入、修改和删除操作是需要加锁的。
d.可串行化:每次读取的时候事务都要获取表级别的S锁,每次写入的时候都会加写锁(X锁),读写操作会相互阻塞。
下面我们通过事例来说明隔离级别和表锁的关系:
(1)创建数据库表branch,并插入三条测试数据,如下所示
CREATE TABLE `branch` (
  `id` tinyint(4) NOT NULL AUTO_INCREMENT,
  `branch_name` varchar(20) NOT NULL,
  `director_id` smallint(4) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
– ----------------------------
– Records of branch
– ----------------------------
INSERT INTO `branch` VALUES ('1', '软件一部', '7135');
INSERT INTO `branch` VALUES ('2', '软件二部', '7136');
INSERT INTO `branch` VALUES ('3', '软件三部', '7137');
(2)查询表中的数据,如下所示:
InnoDB中事务隔离级别和锁的关系
            
    
    博客分类: database  
(3)RU级别
由于Mysql默认是提交事务的,在这里,我们需要将Mysql设置为不自动提交事务,如下图所示:
InnoDB中事务隔离级别和锁的关系
            
    
    博客分类: database  
由于Mysql中InnoDB默认的级别是RR级别,我们需要将session开启成RU级别,如下图所示:
InnoDB中事务隔离级别和锁的关系
            
    
    博客分类: database  
接着,我们开启两个事务,左边为事务A右边为事务B, 如下图所示:
InnoDB中事务隔离级别和锁的关系
            
    
    博客分类: database  
由上图可以看出,由于RU级别中对读和写操作均不加锁,事务B可以查询出事务A未提交的数据,造成"脏读"现象。
(4)RC级别
接着我们将session开启成RC级别,如下图所示:
InnoDB中事务隔离级别和锁的关系
            
    
    博客分类: database  
接着我们和(3)一样,开启两个事务, 左边为事务A右边为事务B ,再次执行(3)的操作,结果如下图所示:
InnoDB中事务隔离级别和锁的关系
            
    
    博客分类: database  
     从上图中可以看出RC级别中,虽然读的操作不加锁,但是对数据的写入操作加行锁,因此在事务B执行insert操作提交事务之前,事务A是不会查询出事务B未提交的那一行数据的,不会产生RU级别的"脏读"现象。
     但事务B会出现不可重复读的现象,接着我们展现RC模式,如下图所示:
InnoDB中事务隔离级别和锁的关系
            
    
    博客分类: database  
从上图中可以看出,事务A两次读取到的结果是不一样的,这时由于在RC级别中,读的操作不加锁,因此在事务A没有提交之前,事务B可以进行写的操作。在表中id是主键,事务B中会对id=5的记录加X锁,在事务B提交之后,事务A再次读取数据,就会和第一次查询的结果不同,造成了"不可重复读"的现象。
(5)RR级别
我们先将session开启成RR级别,如下图所示:
InnoDB中事务隔离级别和锁的关系
            
    
    博客分类: database  
接着我们和(4)一样,开启两个事务,左边为事务A右边为事务B ,再次执行(4)的操作,结果如下图所示:
InnoDB中事务隔离级别和锁的关系
            
    
    博客分类: database  
从上图中可以看出,事务A两次读取到的结果是相同的。
如果使用锁机制来解决幻读的话,就是由于在RR级别中,id为主键,读的操作也会加行锁,因此在事务A没有提交之前,其他事务没法修改这些数据直到事务A提交成功之后,其他事务才可以修改这些数据,因此RR级别可以避免"不可重复读"的现象。
但在Mysql的InnoDB中,是通过多版本并发控制(MVCC)机制来避免幻读,相当于我们读取到的数据是历史数据,我们将这种读取历史数据的方式称为"快照读"。
我们再展现 在RR级别"幻读"模式,如下图所示:
InnoDB中事务隔离级别和锁的关系
            
    
    博客分类: database  
在这里,并没有出现预期的现象,事务A在提交之前并没有读取到事务B插入的数据,并没有出现"幻读"现象,可见在Mysql的RR级别中是解决了"幻读"这个问题的,至于怎么解决的我们后面再摸索。
下面我们先简单了解一下"不可重复读"和"幻读"的区别。

4.不可重复读和幻读的区别

不可重复读是指如果事务A读取了某一行数据,事务A未提交,如果事务B修改了这一行数据并且提交,事务A再去重复读取这条记录时,会发现两次读取的同一行数据记录发生了变化。
幻读是指事务A读取了某一个范围内的数据,事务A未提交,事务B在这个范围内插入一条记录,事务A再去重复读取这个范围内的数据时,发现再次读取到的数据记录数比之前多。
不可重复读的重点在于update和delete操作,而幻读的重点在于insert操作。就拿上面的例子来说,如果只是纯粹的用锁机制来实现这两种隔离级别的话,在sql执行事务A的select操作之后,就对这些数据进行加锁,在此期间,其他事务拿不到该行锁,因此对于事务B中的update操作,只能等待事务A将锁释放之后才能对相应的数据行加锁。这样就可以实现可重复读,但是这种方法却无法锁住即将要insert的数据,因此就会出现"幻读"的现象。
因此,"幻读"现象是不能通过行锁来避免的,必须通过Serializable隔离级别来避免,也就是读的操作用"读锁",写的操作用"写锁","读锁"和"写锁"互为互斥锁,这么做就可以有效地避免"脏读"、"不可重复读"、"幻读"等问题,但是会极大地降低数据库的并发能力。
前面所说的都是使用悲观锁来解决"不可重复读"和"幻读" 这两个问题的,但是Mysql、Oracle、Postgresql等一些成熟的关系型数据库为了性能上的考虑,都会采用以乐观锁为基础的多版本并发控制(MVCC)来避免这两个问题。
接下来,我们来简单的了解一下悲观锁和乐观锁。

5.悲观锁和乐观锁

a.悲观锁:在整个数据的处理过程中,将数据处于锁定的状态,悲观锁的实现,是依靠数据库提供的锁机制,以保证操作上最的程度的独占性。在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读,在读取数据时对数据加锁使得其他事务无法对其进行修改操作;在修改数据时,也要对其进行加锁操作,使得其他事务无法读取这些数据。这样随之而来的就是数据库性能上的大量开销,尤其是对于长事务而言,这种开销是无法承受的。
b.乐观锁:基于数据库版本记录机制来实现的,也就是说为数据增加一个版本控制的标识。在基于数据库表的版本控制实现机制中,一般是通过为数据库表增加一个"version"字段,在读取时,将此版本号一起读出,在对数据进行修改操作时,"version"字段的值加1。在提交数据时,将要提交数据的版本号和数据库表中对应数据的version字段的值进行比较,如果要提交的数据版本号大于当前数据库表中的当前版本号,则对数据予以更新;否则,就认为要提交的数据为过期数据。
对于乐观锁的实现,上述方法只是一个举例,每种关系型数据库都会有不同的实现方式,在这里我们主要看看InnoDB中乐观锁的实现。

6.InnoDB中乐观锁的实现

在InnoDB中,会在每行数据后面添加两个隐藏的属性来实现多版本并发控制。一个属性记录这行数据的创建时间,另外一个属性记录当前数据的过期时间(被删除时间)。但在实际的操作中,存储的值并非是时间,而是事务的版本号,每开启一个新的事务,事务的版本号就会递增。RR事务隔离级别下,
select:读取出来的创建的版本号<=当前事务的版本号,以确保事务读取的行要么是在事务开始之前已经存在的,要么是事务自身插入或者修改过的;而删除的版本号为空或>当前事务的版本号,以保证事务所读取到的行在事务开始之前未被删除。
insert:保存当前事务的版本号为行的创建版本号。
update:插入一条新纪录,保存当前事务的版本号为行的创建版本号,同时保存当前事务的版本号到原来删除的行。
delete:保存当前事务的版本号为行的删除版本号。
通过MVCC,虽然每行记录都需要额外的存储空间,更多的行检查工作以及一些额外的维护工作,但可以减少锁的使用,大多数读操作都不用加锁,读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行,也只锁住必要行。在这里要注意的是,MVCC只在RC和RR两种隔离级别下工作。

7.快照读和当前读

a.快照读:读取的记录是可见版本(可能是历史版本),不用加锁。
  如:select * from table where ?
b.当前读:读取的记录是最新记录,并且会返回当前记录,会进行加锁操作,以确保其他并发事务不会再修改这些记录。如:
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锁。

8.Mysql的加锁处理

  Mysql的加锁情况取决于以下前提条件:
a.当前系统会话的隔离级别;
b.id列是否为主键;
c.如果id列不为主键,id列上是否有索引;
d.id列上如果有二级索引,那么这个索引是否是唯一索引;
e.两个SQL的执行计划。
根据以上前提条件,可以将加锁的情况分为如下几种:
(1)RC隔离级别,id列为主键;
对于这种情况,id是主键,此时Mysql会为id列自动创建主键索引。因此,当执行"update branch set director_id=7137 where id=3"这条语句时,会为id=3的这条数据记录加上X锁。加锁情况如下图所示:
      InnoDB中事务隔离级别和锁的关系
            
    
    博客分类: database  
(2)RC隔离级别,id列上没有索引;
在这种情况下,id列上没有索引,id=3这个过滤条件无法通过索引去过滤,因此只能走聚簇索引,进行全表扫描,因此当执行"update branch set director_id=7137 where id=3 "这条语句时,表中的所有记录行都会被加X锁,在这里所然是对全表记录进行加锁,但是加的是"行锁"不是"表锁"。
加锁情况如下图所示:
InnoDB中事务隔离级别和锁的关系
            
    
    博客分类: database  
(3)RC隔离级别,id列是二级唯一索引;
这种情况,id列上没然没有主键索引,但是却有二级唯一索引。因此,当执行"update branch set director_id=7137 where id=3 "这条语句时,由于id是唯一索引列,update语句选择走id列的索引进行条件过滤,在找到id=2的记录后,首先会将唯一索引上的id=2的索引记录加上X锁。同时会根据读取到的数据列,再对相应的主键索引项加上X锁。加锁情况如下图所示:
InnoDB中事务隔离级别和锁的关系
            
    
    博客分类: database  
(4)RC隔离级别,id列是二级非唯一索引;
和第(3)种情况相比,加锁的策略是相同的。唯一不同的是(3)最多只有一个满足等值条件查询的记录,而这种情况将会为满足条件的所有记录加X锁。加锁情况如下图所示:
  InnoDB中事务隔离级别和锁的关系
            
    
    博客分类: database  
(5)RR隔离级别,id列为主键;
这种情况的加锁情况和(1)相同。
(6)RR隔离级别,id列上没有索引;
RR级别和RC级别最大的区别就是RR级别不会出现幻读。所谓不出现幻读就是连续两次当前读,这两次当前读所返回的数据量和数据记录是相同的。要做到这样,就必须保证在第一次当前读和第二次当前读执行的期间,其他的事务不会插入新的满足条件的记录。
为满足这些要求,在执行"select * from branch where id=3 for update;"语句时,在使用X锁的同时,还要使用GAP锁对数据记录区间进行加锁。
由于此时id列上没有索引,因此只能进行全表扫描,需要为表中每条数据记录加上X锁,并在每个GAP上加GAP锁。加锁情况如下图所示:
InnoDB中事务隔离级别和锁的关系
            
    
    博客分类: database  
(7)RR隔离级别,id列是二级唯一索引;
加锁情况和(3)相同。
(8)RR隔离级别,id列是二级非唯一索引;
在这里,id列上有一个非唯一索引,数据库的索引是使用B+树存储的,B+树索引是有序的。在执行"select * from branch where id=3 for update;"这条语句时,首先需要通过id索引定位到第一条满足查询条件的记录,为这个记录加上记录上的X锁的同时还要加上GAP上的GAP锁,然后再在对应的主键索引上的记录加X锁,然后返回。接着读取下一条数据,重复上述的加锁操作,直到进行到不满足查询条件的第一条记录为止,此时不需要加上记录的X锁,但需要加上GAP锁。
根据查询条件可以看出来[2,软件二部,7136]和[3,软件三部,7138]之间可以插入数据[3,a,7100]等,[3,软件三部,7138 ]和[3,软件五部,7140]之间可以插入数据[3,软件t,7123]等数据,[3,软件五部,7140] 和[4,软件四部,7139] 之间可以插入数据[3,z,20]等数据,而[2,软件二部,7136 ]之前和[4,软件四部,7139 ]之后不会插入id为3的数据,因此需要对索引区间(2,3)、[3,3]和(3,4)加GAP锁。加锁情况如下图所示:
      InnoDB中事务隔离级别和锁的关系
            
    
    博客分类: database  
Serializable隔离级别下,不存在快照读,所有的操作都是当前读,读加读锁,写加写锁,读锁和写锁互斥。
注意:在RC级别和RR级别下,select操作均不会进行加锁操作。
在Repeatable Read隔离级别下,针对一个复杂的SQL,首先需要提取其where条件。Index Key确定的范围,需要加上GAP锁;Index Filter过滤条件,视MySQL版本是否支持ICP,若支持ICP,则不满足Index Filter的记录,不加X锁,否则需要X锁;Table Filter过滤条件,无论是否满足,都需要加X锁。
通过加锁情况我们可以了解到,在RR级别下使用X锁防止其他并发事务对记录的更改/删除操作,使用GAP锁实现其他并发事务对区间段内记录的插入操作,因此Mysql的InnoDB在RR级别已经实现了幻读的避免。因此,在上文我们发现事务的隔离级别设置为RR时,并未出现幻读的现象。