Mysql基础篇-25-Mvcc与锁的理解(事务隔离级别)

1. 前言

如果数据库中的事务都是串行执行的,这种方式可以保障事务的执行不会出现异常和错误,但带来的问题是串行执行会带来性能瓶颈;
而事务并发执行,如果不加以控制则会引发诸多问题,包括死锁、更新丢失等等。
这就需要我们在性能和安全之间做出合理的权衡,使用适当的并发控制机制保随并发事务的执行。

2. 并发事务带来的问题

首先我们先来了解一下并发事务会带来哪些问题。并发事务访问相同记录大致可归纳为以下3种情况:

  • 读-读:即并发事务相继读取同一记录;
  • 写-写;即并发事务相继对同一记录做出修改;
  • 写-读或读-写:即两个并发事务对同一记录分别进行读操作和写操作。

2.1 读-读

因为读取记录并不会对记录造成任何影响,所以同个事务并发读取同一记录也就不存在任何安全问题,所以允许这种操作。

2.2 写-写

如果允许并发事务都读取同一记录,并相继基于旧估对这一记录做出修改,那么就会出现前一个事务所做的修改被后面事务的修改覆盖,即出现提交覆盖的问题。

另外一种情况,并发事务相继对同一记录做出修改,其中一个事务提交之后之后另一个事务发生回滚,这样就会出现已提交的修改因为回滚而丢失的问题,即回滚覆盖问题。

这两种问题都造成丢失更新,其中回滚覆盖称为第一类丢失更新问题,提交覆盖称为第二类丢失更新问题。

2.3 写-读或读-写

这种情况较为复杂,也最容易出现问题。

如果一个事务读取了另一个事务尚未提交的修政记录,那么就出现了脏读的问题;

如果我们加以控制使得一个事务只能读取其他已提交务的修改的数据,那么这个事务在另一手物提交修改前后读取到的数据是不一样的,这就意味看发生了不可重复读;

如果一个事务根据一些条件查询到一些记录,之后另一事物向表中插入了一些记录,原先的事务以相同条件再次查询时发现得到的结果跟第一次查词得到的结果不一致,这就意味着发生了幻读。

2.4 不可重复读和幻读的区别?

精炼解释:

  • 不可重复读的重点是修改:
    同样的条件, 你读取过的数据, 再次读取出来发现值不一样了

  • 幻读的重点在于新增或者删除
    同样的条件, 第1次和第2次读出来的记录数不一样

当然, 从总的结果来看, 似乎两者都表现为两次读取的结果不一致.
但如果你从控制的角度来看, 两者的区别就比较大
对于前者, 只需要锁住满足条件的记录
对于后者, 要锁住满足条件及其相近的记录

详细说明:

  1. 不可重复读 是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
    例如,一个编辑人员两次读取同一文档,但在两次读取之间,作者重写了该文档。当编辑人员第二次读取文档时,文档已更改。原始读取不可重复。如果只有在作者全部完成编写后编辑人员才可以读取文档,则可以避免该问题

要避免这种情况,通常可以用 set tran isolation level repeatable read 来设置隔离级别,这样事务A 在两次读取表T中的数据时,事务B如果企图更改表T中的数据(细节到事务A读取数据)时,就会被阻塞,知道事务A提交! 这样就保证了,事务A两次读取的数据的一致性。

  • 幻读 是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。

例如,一个编辑人员更改作者提交的文档,但当生产部门将其更改内容合并到该文档的主复本时,发现作者已将未编辑的新材料添加到该文档中。如果在编辑人员和生产部门完成对原始文档的处理之前,任何人都不能将新材料添加到文档中,则可以避免该问题。

3. 事务的隔离级别

对于以上提到的并发事务执行过程中可能出现的问题,其严重性也是不一样的,我们可以按照问题的严重程度排个序:

丢失更新 > 脏读 > 不可重复读 > 幻读

因此如果我们可以容忍一些严重程度较轻的问题,我们就能获取一些性能上的提升。于是便有了事务的四种隔离级别:

  • 读未提交(Read Uncommitted):允许读取未提交的记录,会发生脏读、不可重复读、幻读;
  • 读已提交(Read Committed):只允许读已提交的记录,不会发生脏读,但会出现可重复读、幻读;
  • 可重复读(Repeatable Read):不会发生脏读和不可重复读的问题,但会发生幻读问题;但MySQL在此隔离级别下利用间隙锁可以禁止幻读问题的发生;
  • 可串行化(Serializable):即事务串行执行,以上各种问题自然也就都不会发生。

4. 隔离级别的实现

SQL规范定义了以上四种隔离级别,但是并没有给出如何实现四种隔商级别,因此不同数据库的实现方式和使用方式也并不相同。
而SQL隔离级别的标准是依据基于锁的实现方式来制定的,因为有必要先了解一下传统的基于锁的隔离级别是如何实现的。

4.1 传统隔离级别的实现

既然说到传统的隔离级别是基于锁实现的,我们先来了解一下锁。

    • 共享锁(Shared Locks):简称S锁,事务对一条记录进行读操作时,需要先获取该记录的共享锁。
      -
    • 排他锁(Exclusive Locks):简称X锁,事务对一条记录进行写操作时,需要先获取该记录的排他锁。

需要注意的是,加了共享锁的记录,其他事务也可以获得该记录的共享锁,但是无法获取该记录的排他锁,即S锁和S锁是兼容的,S锁和X锁是不兼容的;
而加了排他锁的记录,其他事务既无法获取该记录的共享锁也无法获取排他锁,即X锁和X锁也是不兼容的。
另外,刚刚说到事务对一条记录进行读操作时,需要先获取该记录的S锁,但有时务在读取记录时需要阻止其他事务访问该记录,这时就需要获取该记录的x锁。
以MySQL为例,有以下两种锁定读的方式:

  • 读取时对记录加S锁:

SELECT…LOCK IN SHARE MODE;

如果事务执行了该语句,则会在读取的记录上加S锁,这样就允许其他事务也能获取到该记录的S锁:而如果其他事务需要获取该记录的X锁,那么就需要等待当前事务提交后释放掉S锁。

  • 读取时对记录加X锁:

SELECT……FOR UPDATE;

如果事务执行了该语句,则会在读取的记录上加X锁,这样其他事务想要取该记录的S锁或X锁,那么需要等待当前事务提交后释放掉X锁。

对于锁的粒度而言锁又可以分为两种:

  • 行锁:只锁住某一行记录,其他行的记录不受影响。
  • 表锁:锁住整个表,所有对于该表的操作都会受影响。

4.2 基于锁实现隔离级别

在基于锁的实现方式下,四种隔离级别的区别就在于加锁方式的区别:

  • 读未提交:
    读操作不加锁,读读,读写,写读并行;
    写操作加X锁且直到事务提交后才释放。 长锁

  • 读已提交:
    读操作加S锁,操作完立即释放; 短锁
    写操作加X锁且直到事务提交后才释放; 长锁
    读操作不会阻塞其他事务读或写,写操作会阻塞其他事务写和读,因此可以防止脏读问题。

  • 可重复读:
    读操作加S锁且直到事务提交后才释放; 长锁
    写操作加X锁且直到事务提交后才释放; 长锁
    读操作不会阻塞其他事务读但会阻塞其他事务写;
    写操作会阻塞其他事务读和写,因此可以防止脏读、不可重复读。

  • 串行化:
    读操作和写操作都加X锁且直到事务提交后才释放; 长锁
    粒度为表锁,也就是严格串行

注意:

  • 如果锁获取之后直到事务提交后才释放,这种锁称为长锁;
  • 如果锁在操作完成之后就被释放,这种锁称为短锁。

例如,在读已提交隔离级别下,读操作所加S锁为短锁,写操作所加X锁为长锁。

对于可重复读和串行化隔离级别,读操作所加S锁和写操作所加X锁均为长锁,即事务获取锁之后直到事务提交后才能释放,这种把获取锁和释放锁分为两个不同的阶段的协议称为两阶段锁协议(2-phase locking)。
两阶段锁协议规定:

  • 在加锁阶段,一个事务可以获得锁但是不能释放锁;
  • 在解锁阶段事务只可以释放锁,并不能获得新的锁。

两阶段锁协议能够保证事务串行化执行,解决事务并发问题,但也会导致死锁发生的概率大大提升。

5. Mysql隔离级别的实现

不同数据库对于SQL标准中规定的隔离级别支持是不一样的,数据库引擎实现隔离级别的方式虽然都在尽可能地贴近标准的隔离级别规范,但和标准的预期还是有些不一样的地方。

MySQL(InnoDB)支持的4种隔离级别,与标准的各级隔离级别允许出现的问题有些出入,比如MySQL在可重复读隔离级别下可以防止幻读的问题出现,但也会出现提交覆盖的问题。
相对于传统隔离级别基于锁的实现方式,MySQL 是通过MVCC(多版本并发控制)来实现读-写并发控制,又是通过两阶段锁来实现写-写并发控制的。即 MVCC+2PL 实现
MVCC是一种无锁方案,用以解决事务读-写并发的问题,能够极大提升读-写并发操作的性能。

6. MVCC是什么?

mvcc(multi-version-concurrent-control)
MVCC即多个不同版本的数据实现并发控制技术,其基本思想是为每次事务生成一个新版本的数据,再读取数据时选择不同版本的数据即可以实现事务结果的完整性读取。即通过保存数据在某个时间点的快照来实现,不同存储引擎的MVCC实现不一致.

6.1 Mvcc作用是什么?

大多数的Mysql事务型存储引擎,如InnoDB,Falcon以及PBXT都不使用一种简单的行锁机制,事实上,他们都和MVCC-多版本并发控制来使用,因为锁虽然可以控制并发操作,但是系统开销大,而MVCC可以在大多数情况下代替行锁,使用mvcc能降低系统开销,提供并发的读写性能.

操作的时候会生成事务Id(update,delete,add)

  • 每条记录都会保存两个隐藏的列,trx_id(事务id)和roll_point(回滚指针)两个字段
  • 每次操作都会生成一条undo log日志,回滚指针指向前一条记录,从而形成版本链

查询的时候:

  • 如果是可重复读,会引用上一次读操作生成的readview(快照),

  • 读已提交 ,每次读操作都会生成readview

查询的时候会读取ReadView:[未提交的事务id]数组+已提交的最大事务id(快照点已提交的最大事务id),并根据readview从undo log日志中最新的记录依次往下找:
在这里插入图片描述

《可重复读-mysql的默认隔离级别》:从最新记录开始找:

  • 如果当前记录: 事务id小于未提交事务的最小id,则说明已经提交,则可读;

  • 如果当前记录: 事务id在未提交事务id的数组中,则不可读(只有自己可读)

  • 如果当前记录: 事务id大于已提交事务的最大id,则不可读,说明是将来新开事务id,不可见。

  • 《读已提交》:从最新记录开始找到事务id为已提交事务的最大id为止;

MVCC只针对读已提交和可重复读,如果是读未提交,每次查询都读取最新记录即可。

7. 关于Mvcc更多信息补充

MySQL的大多数事务型存储引擎实现的都不是简单的行级锁。基于提升并发性能的考虑,它们一般都同时实现了多版本并发控制(MVCC)。不仅是MySQL,包括Oracle、PostgreSQL等其他数据库系统也都实现了MVCC,但各自的实现机制不尽相同,因为MVCC没有一个统一的实现标准。

可以认为MVCC是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行。

MVCC的实现,是通过保存数据在某个时间点的快照来实现的。也就是说,不管需要执行多长时间,每个事务看到的数据都是一致的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。如果之前没有这方面的概念,这句话听起来就有点迷惑。熟悉了以后会发现,这句话其实还是很容易理解的。

前面说到不同存储引擎的MVCC实现是不同的,典型的有乐观(optimistic)并发控制和悲观(pessimistic)并发控制。下面我们通过InnoDB的简化版行为来说明MVCC是如何工作的。

InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间)。当然存储的并不是实际的时间值,而是系统版本号(system version number)。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。下面看一下在REPEATABLE READ隔离级别下,MVCC具体是如何操作的。

7.1 SELECT

InnoDB会根据以下两个条件检查每行记录:

  • a.InnoDB只查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
  • b.行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。只有符合上述两个条件的记录,才能返回作为查询结果。

7.2 INSERT

InnoDB为新插入的每一行保存当前系统版本号作为行版本号。

7.3 DELETE

InnoDB为删除的每一行保存当前系统版本号作为行删除标识。

7.4 UPDATE

InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。

保存这两个额外系统版本号,使大多数读操作都可以不用加锁。

这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行。不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。

MVCC只在REPEATABLE READ和READ COMMITTED两个隔离级别下工作。其他两个隔离级别都和MVCC不兼,因为READ UNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE则会对所有读取的行都加锁。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alan0517

感谢您的鼓励与支持!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值