数据库事务隔离级别与加锁机制

1、事务隔离级别

数据库一共有四种隔离级别:

READ UNCOMMITTED READ COMMITTED REPEATABLE READ SERIALIZABLE
在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read) 这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。 这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。但是可能出现幻读的情况(只是可能,亲测MySQL不会出现) 这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。

脏读 不可重复读 幻读
在一个事务当中,可以读取到其它未提交的事务对表的修改与操作 在一个事务当中,两次读取的数据可能不一样,会读取到其它已提交事物的修改的值 在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下

隔离级别 脏读 不可重复读 幻读
RU YES YES YES
RC NO YES YES
RR NO NO MAYBE
SE NO NO NO


2、事务隔离实验

开启两个数据库session,以便模仿两个客户端来对数据库进行操作。我们分别成为A端和B端

可以通过

SELECT @@tx_isolation;

来查询当前session的数据库事务隔离级别。

1、READ UNCOMMITTED

先设置当前session的数据库隔离级别

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

A端开启事务,并且查询一次表格

img

这个时候,B端也开启事务,并且更新一条数据,然后回滚

A:
img
B:
img

在B端更新事物,未提交,且回滚之前,A端的一个事务之中,可以查询到这个未提交的更改。回滚之后,便会查询到回滚之后的数据。

这个现象就是脏读

2、READ COMMITTED

先把客户端A的事务级别设定为 READ COMMITTED

然后进行一次查询。然后B端事务中更新一条数据,A端再进行一次查询,会发现脏读的问题已经不会出现了。

A:
img
B:
img

然后B端提交事务

img

A端再进行一次查询

img

然而A依然还在同一次的事务当中,虽然B端是提交了数据之后A端才能够查询到,但是依然是同一事务当中的两次查询出现了不一样的结果,这个现象称之为不可重复读

##
3、REPEATABLE READ

先把客户端A的事务隔离级别设定为 REPEATABLE READ

客户端A开启事务,然后进行一次简单的查询

img

然后B端更新数据,提交,A端再进行查询

A:
img
B:
img

会发现不可重复读的现象已经不会出现了。然后还要验证一下幻读的现象。

B端插入数据,提交事务,a端进行查询

A:
img
B:
img

会发现并没有发生幻读的现象。但是这个是根据数据库有所差异。

##
4、SERIALIZABLE

这个隔离级别是最高的隔离级别了,会要求事务的操作全部串行化。

先把A的级别设定为SERIALIZABLE

A客户端开启事务

img

然后B插入一条数据,会发现阻塞住了。

img

因为这个级别会让事务强制排序,串行运行,所以不会发生幻读问题,但是缺点也显而易见…效率很低,容易死锁。

3、加锁机制

数据库事务采用两段加锁机制。因为在事务刚开始的时候,数据库并不知道哪些数据需要加锁,所以在需要的时候再依次去申请加锁,然后在事务提交的时候再统一释放掉所有的锁。
































事务过程 加锁/解锁处理
begin
insert into table 加insert对应的锁
update table 加update对应的锁
delete from table 加delete对应的锁
commit 依次释放insert,update,delete的锁

这种方式虽然无法避免死锁,但是可以保证数据库的串行化。

对数据的读操作需要S锁(共享锁),加了S锁之后其他事务也可以对数据加S锁,但是无法加X锁。

对数据进行写操作需要X锁(排他锁),加了X锁之后其他任何事务无法再申请更多的锁。

1、READ UNCOMMITTED

这个级别的事务,数据库的实际使用当中一般是不会使用的,因为在这个级别的任何操作都不会加锁,所以说不做过多的讨论。

2、READ COMMITTED

在这个级别当中,所有的读取数据的操作都不会加锁。所以说,在同一个事务当中,如果有其它事务提交了数据的更改,则会在同一事务中发生不可重复读的现象。而在RC的级别当中,对数据的写入,更新以及删除是会加锁的。具体我们看看:

客户端A先对一条数据进行更新:

img

这个时候,客户端a也就对这一行数据加了行锁

这个时候,客户端b对的同一行数据进行更新,就会阻塞

img

但是这里有一点要注意,因为teacher_id是添加了索引的,所以在加锁的时候只对这一行数据加了锁。

如果操作的对象是没有索引的class_name呢?

img

这个时候对另一条数据class_name=3.2的数据进行更新

img

会发现,客户端同样阻塞了。所以说,在对非索引的class_name进行操作的时候,数据库会对整张表的每个数据都添加行锁

3、REPEATABLE READ

不可重复读和幻读的区别

上面的实验可以看到,不可重复读在REPEATABLE READ的模式下是不会出现的。而通过上面对RC的加锁机制的实验可以明白,锁是针对一行数据进行加锁的,也就是行锁。而行锁本身对于update和delete事件可以有效的防止不可重复读的现象出现,而幻读的出现是由于insert操作造成的。也就是一行新的数据的添加。

就是说客户端在两次读取数据的时候,如果多出了一条数据的话,也就会出现幻读的现象。

乐观锁与悲观锁

上面所说的方式是采用悲观锁的形式来实现,但是悲观锁由于在实际当中,采用数据库本身的锁机制来保持数据库事务之间的数据一致性,会导致数据库的性能下降。

如果采用悲观锁的方式来解决幻读问题的话,就需要在读取的时候加排他锁,禁止加写锁,就是隔离级别SERIALIZABLE中采用的方式,会很大程度上降低数据库的性能。

但是如InnoDB这种成熟的数据库引擎当中,会采用一种乐观锁的形式来做。

乐观锁是基于数据库的版本标识来做(Version)。

什么是Version?就是为数据增加一个版本标识,一般来说是在数据库中默默添加一个字段来实现。在读取数据的时候,一同将这个版本标识读取出来,之后此数据如果有更新,则会将这个版本标识增加。将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

在InnoDB当中,每行数据后面添加两个隐藏的字段

这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。

在实际操作中,存储的并不是时间,而是事务的版本号,每开启一个新事务,事务的版本号就会递增,同时会保存当前时间点的数据快照(snapshot)。

在RR的隔离级别当中,四个操作对应的:




























操作 方式
SELECT

1

InnoDB
只查找版本早于当前事务版本的数据行(也就是数据行的版本必须小于等于事务的版本),这确保当前事务读取的行都是事务之前已经存在的,或者是由当前事务创建或修改的行


2
、行的删除操作的版本一定是未定义的或者大于当前事务的版本号。确定了当前事务开始之前,行没有被删除


UPDATE 可以拆分为一个DELETE和一个INSERT
DELETE

 

 
InnoDB
为每个删除行的记录当前事务版本号作为行的删除版本号


INSERT

  
InnoDB
为每个新增行记录当前事务版本号作为创建版本号


通过这个机制,虽然说每一行都需要额外的空间来存储,但是可以减少锁的使用,可以有效提升效率。

上面的方案解决了重复读的问题。但是同时催生了一个问题,就是在此事务当中读取到的数据可能是旧的数据。而在一些对数据的实时性很高的情况下,就有可能出现问题。

在这种读取历史数据的模式下,称之为snapshoot read,而无论在何时,都是读取数据库当前最新数据的模式,称为 current read

在事务当中

SELECT

就是普通的snapshot read

而要使用current read,

可以使用

select * from table where ? lock in share mode;
select * from table where ? for update;

上面两个命令是属于加锁读,在读取数据的同时,会给数据加锁。所以可以读取到最新的数据。但是在加锁读取(update,delete)数据之后,会给数据添加行锁的同时,还会添加一个Gap锁。

什么是Gap锁:

img

其中绿色部分是数据行。在数据之间还存在间隙锁,也就是Gap锁。假定我们对数据30 进行操作(操作包括update,delete,以及上面提到的加锁读)则会在数据两边的间隙添加Gap锁,这样的话,在黄色的(5,30]以及(30, positive infinity]区间的数据都会被加锁,而无法再被其它事务进行增删改查(这里的查指加锁查)。

4、SERIALIZABLE

这个级别很简单,读加共享锁,写加排他锁,读写互斥。使用的悲观锁的理论,实现简单,数据更加安全,但是并发能力非常差。如果你的业务并发的特别少或者没有并发,同时又要求数据及时可靠的话,可以使用这种模式。