分析MySQL数据库Innodb中的事务隔离级别和锁的关系

72次阅读
没有评论

共计 5774 个字符,预计需要花费 15 分钟才能阅读完成。

本篇内容主要讲解“分析 MySQL 数据库 Innodb 中的事务隔离级别和锁的关系”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让丸趣 TV 小编来带大家学习“分析 MySQL 数据库 Innodb 中的事务隔离级别和锁的关系”吧!

update class_teacher set class_name= 初三三班 where id=1;

commit;

select id,class_name,teacher_id from class_teacher where teacher_id=1;

idclass_nameteacher_id1 初三三班 12 初三一班 1

读到了事务 B 修改的数据,和第一次查询的结果不一样,是不可重读的。

commit;

事务 B 修改 id= 1 的数据提交之后,事务 A 同样的查询,后一次和前一次的结果不一样,这就是不可重读(重新读取产生的结果不一样)。这就很可能带来一些问题,那么我们来看看在 RR 级别中 MySQL 的表现:

事务 A 事务 B 事务 Cbegin;

begin;

begin;

select id,class_name,teacher_id from class_teacher where teacher_id=1;

idclass_nameteacher_id1 初三二班 12 初三一班 1

update class_teacher set class_name= 初三三班 where id=1;

commit;

 

insert into class_teacher values (null, 初三三班 ,1);

 

commit;

select id,class_name,teacher_id from class_teacher where teacher_id=1;

idclass_nameteacher_id1 初三二班 12 初三一班 1

没有读到事务 B 修改的数据,和第一次 sql 读取的一样,是可重复读的。

没有读到事务 C 新添加的数据。

commit;

我们注意到,当 teacher_id= 1 时,事务 A 先做了一次读取,事务 B 中间修改了 id= 1 的数据,并 commit 之后,事务 A 第二次读到的数据和第一次完全相同。所以说它是可重读的。那么 MySQL 是怎么做到的呢?这里姑且卖个关子,我们往下看。

#### 不可重复读和幻读的区别 ####
很多人容易搞混不可重复读和幻读,确实这两者有些相似。但不可重复读重点在于 update 和 delete,而幻读的重点在于 insert。

如果使用锁机制来实现这两种隔离级别,在可重复读中,该 sql 第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复读了。但这种方法却无法锁住 insert 的数据,所以当事务 A 先前读取了数据,或者修改了全部数据,事务 B 还是可以 insert 数据提交,这时事务 A 就会发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。需要 Serializable 隔离级别,读用读锁,写用写锁,读锁和写锁互斥,这么做可以有效的避免幻读、不可重复读、脏读等问题,但会极大的降低数据库的并发能力。

所以说不可重复读和幻读最大的区别,就在于如何通过锁机制来解决他们产生的问题。

上文说的,是使用悲观锁机制来处理这两种问题,但是 MySQL、ORACLE、PostgreSQL 等成熟的数据库,出于性能考虑,都是使用了以乐观锁为理论基础的 MVCC(多版本并发控制)来避免这两种问题。

#### 悲观锁和乐观锁 ####

悲观锁

正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。

乐观锁

相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。

而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本(Version)记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个“version”字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

要说明的是,MVCC 的实现没有固定的规范,每个数据库都会有不同的实现方式,这里讨论的是 InnoDB 的 MVCC。

####MVCC 在 MySQL 的 InnoDB 中的实现
在 InnoDB 中,会在每行数据后添加两个额外的隐藏的值来实现 MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。在实际操作中,存储的并不是时间,而是事务的版本号,每开启一个新事务,事务的版本号就会递增。在可重读 Repeatable reads 事务隔离级别下:

SELECT 时,读取创建版本号 = 当前事务版本号,删除版本号为空或 当前事务版本号。

INSERT 时,保存当前事务版本号为行的创建版本号

DELETE 时,保存当前事务版本号为行的删除版本号

UPDATE 时,插入一条新纪录,保存当前事务版本号为行创建版本号,同时保存当前事务版本号到原来删除的行

通过 MVCC,虽然每行记录都需要额外的存储空间,更多的行检查工作以及一些额外的维护工作,但可以减少锁的使用,大多数读操作都不用加锁,读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行,也只锁住必要行。

我们不管从数据库方面的教课书中学到,还是从网络上看到,大都是上文中事务的四种隔离级别这一模块列出的意思,RR 级别是可重复读的,但无法解决幻读,而只有在 Serializable 级别才能解决幻读。于是我就加了一个事务 C 来展示效果。在事务 C 中添加了一条 teacher_id= 1 的数据 commit,RR 级别中应该会有幻读现象,事务 A 在查询 teacher_id= 1 的数据时会读到事务 C 新加的数据。但是测试后发现,在 MySQL 中是不存在这种情况的,在事务 C 提交后,事务 A 还是不会读到这条数据。可见在 MySQL 的 RR 级别中,是解决了幻读的读问题的。参见下图

读问题解决了,根据 MVCC 的定义,并发提交数据时会出现冲突,那么冲突时如何解决呢?我们再来看看 InnoDB 中 RR 级别对于写数据的处理。

####“读”与“读”的区别
可能有读者会疑惑,事务的隔离级别其实都是对于读数据的定义,但到了这里,就被拆成了读和写两个模块来讲解。这主要是因为 MySQL 中的读,和事务隔离级别中的读,是不一样的。

我们且看,在 RR 级别中,通过 MVCC 机制,虽然让数据变得可重复读,但我们读到的数据可能是历史数据,是不及时的数据,不是数据库当前的数据!这在一些对于数据的时效特别敏感的业务中,就很可能出问题。

对于这种读取历史数据的方式,我们叫它快照读 (snapshot read),而读取数据库当前版本数据的方式,叫当前读 (current read)。很显然,在 MVCC 中:

快照读:就是 select

select * from table ….;

当前读:特殊的读操作,插入 / 更新 / 删除操作,属于当前读,处理的都是当前的数据,需要加锁。

select * from table where ? lock in share mode;

select * from table where ? for update;

insert;

update ;

delete;

事务的隔离级别实际上都是定义了当前读的级别,MySQL 为了减少锁处理(包括等待其它锁)的时间,提升并发能力,引入了快照读的概念,使得 select 不用加锁。而 update、insert 这些“当前读”,就需要另外的模块来解决了。

### 写(当前读)
事务的隔离级别中虽然只定义了读数据的要求,实际上这也可以说是写数据的要求。上文的“读”,实际是讲的快照读;而这里说的“写”就是当前读了。
为了解决当前读中的幻读问题,MySQL 事务使用了 Next-Key 锁。

####Next-Key 锁
Next-Key 锁是行锁和 GAP(间隙锁)的合并,行锁上文已经介绍了,接下来说下 GAP 间隙锁。

行锁可以防止不同事务版本的数据修改提交时造成数据冲突的情况。但如何避免别的事务插入数据就成了问题。我们可以看看 RR 级别和 RC 级别的对比

RC 级别:

事务 A 事务 Bbegin;

begin;

select id,class_name,teacher_id from class_teacher where teacher_id=30;

idclass_nameteacher_id2 初三二班 30

 

update class_teacher set class_name= 初三四班 where teacher_id=30;

insert into class_teacher values (null, 初三二班 ,30);

commit;

select id,class_name,teacher_id from class_teacher where teacher_id=30;

idclass_nameteacher_id2 初三四班 3010 初三二班 30

 

RR 级别:

事务 A 事务 Bbegin;

begin;

select id,class_name,teacher_id from class_teacher where teacher_id=30;

idclass_nameteacher_id2 初三二班 30
update class_teacher set class_name= 初三四班 where teacher_id=30;

insert into class_teacher values (null, 初三二班 ,30);

waiting….

select id,class_name,teacher_id from class_teacher where teacher_id=30;

idclass_nameteacher_id2 初三四班 30
commit; 事务 Acommit 后,事务 B 的 insert 执行。

通过对比我们可以发现,在 RC 级别中,事务 A 修改了所有 teacher_id=30 的数据,但是当事务 Binsert 进新数据后,事务 A 发现莫名其妙多了一行 teacher_id=30 的数据,而且没有被之前的 update 语句所修改,这就是“当前读”的幻读。

RR 级别中,事务 A 在 update 后加锁,事务 B 无法插入新数据,这样事务 A 在 update 前后读的数据保持一致,避免了幻读。这个锁,就是 Gap 锁。

MySQL 是这么实现的:

在 class_teacher 这张表中,teacher_id 是个索引,那么它就会维护一套 B + 树的数据关系,为了简化,我们用链表结构来表达(实际上是个树形结构,但原理相同)

如图所示,InnoDB 使用的是聚集索引,teacher_id 身为二级索引,就要维护一个索引字段和主键 id 的树状结构(这里用链表形式表现),并保持顺序排列。

Innodb 将这段数据分成几个个区间

(negative infinity, 5],

(5,30],

(30,positive infinity);

update class_teacher set class_name= 初三四班 where teacher_id=30; 不仅用行锁,锁住了相应的数据行;同时也在两边的区间,(5,30]和(30,positive infinity),都加入了 gap 锁。这样事务 B 就无法在这个两个区间 insert 进新数据。

受限于这种实现方式,Innodb 很多时候会锁住不需要锁的区间。如下所示:

事务 A 事务 B 事务 Cbegin;begin;begin;

select id,class_name,teacher_id from class_teacher;

idclass_nameteacher_id1 初三一班 52 初三二班 30

update class_teacher set class_name= 初一一班 where teacher_id=20;

insert into class_teacher values (null, 初三五班 ,10);

waiting …..

insert into class_teacher values (null, 初三五班 ,40);commit; 事务 A commit 之后,这条语句才插入成功 commit;
commit;

update 的 teacher_id=20 是在 (5,30] 区间,即使没有修改任何数据,Innodb 也会在这个区间加 gap 锁,而其它区间不会影响,事务 C 正常插入。

如果使用的是没有索引的字段,比如 update class_teacher set teacher_id=7 where class_name= 初三八班(即使没有匹配到任何数据), 那么会给全表加入 gap 锁。同时,它不能像上文中行锁一样经过 MySQL Server 过滤自动解除不满足条件的锁,因为没有索引,则这些字段也就没有排序,也就没有区间。除非该事务提交,否则其它事务无法插入任何数据。

行锁防止别的事务修改或删除,GAP 锁防止别的事务新增,行锁和 GAP 锁结合形成的的 Next-Key 锁共同解决了 RR 级别在写数据时的幻读问题。

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

这里要吐槽一句,不要看到 select 就说不会加锁了,在 Serializable 这个级别,还是会加锁的!

到此,相信大家对“分析 MySQL 数据库 Innodb 中的事务隔离级别和锁的关系”有了更深的了解,不妨来实际操作一番吧!这里是丸趣 TV 网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-07-26发表,共计5774字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)