MySQL:事务隔离的实现原理

事务的原则

所谓事务,就是保证一组数据库操作,要么全部成功,要么全部失败。一共支持下面四个原则

  • 1、原子性:在事务中的操作,必须同时完成或者同时回滚,不会只成功或者回滚一部分。

  • 2、一致性:不能破坏数据库的一致性状态。比如A向B转账,不可能A扣了钱,B却没收到。

  • 3、隔离性:不同的事务之前互相不能影响。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。

  • 4、持久性:事务完成以后,即保存数据,不能回滚。

事务并发造成的问题

多个事务并发运行的时候,同时读写一个数据,可能会出现脏写、脏读、不可重复读、幻读几个问题

  • 所谓的脏写,就是两个事务都更新一个数据,结果有一个人回滚了把另外一个人更新的数据也回滚没了。

  • 脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据。

  • 不可重复读:事务A多次读取同一数据,事务B在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。

  • 幻读:事务A对表中的数据进行了修改,涉及到表中的全部行。同时,事务B也修改这个表中的数据,向表中插入一行新数据。那么,事务A发现表中还有自己没有修改的行,就好象发生了幻觉一样。

注意:

  • 不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。
  • 解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

MySQL的事务隔离级别

事务隔离级别,就是为了解决上面几种问题而诞生的。

为什么要有事务隔离级别?

  • 因为事务隔离级别越高,在并发下会产生的问题就越少,但同时付出的性能消耗也将越大,因此很多时候必须在并发性和性能之间做一个权衡。
  • 所以设立了几种事务隔离级别,以便让不同的项目可以根据自己项目的并发情况选择合适的事务隔离级别,

在MySQL中,事务是在存储引擎中实现的。

  • 在MySQL的众多存储引擎中,但不是所有的引擎都支持事务。InnoDB引擎是支持事务的
  • 注意:下面这里说的事务隔离级别指的是InnoDB下的事务隔离级别。

默认有是四种隔离级别:

  • 读未提交:别人改数据的事务尚未提交,我在我的事务中也能读到。只能避免脏写问题;
  • 读已提交:别人改数据的事务已经提交,我在我的事务中才能读到。
  • 可重复读:别人改数据的事务已经提交,我在我的事务中也不去读。,可以避免脏写和脏读问题。可以避免脏读、脏写和不可重复读的问题;
  • 串行:我的事务尚未提交,别人就别想改数据。可以避免所有问题。

这4种隔离级别,并行性能依次降低,安全性依次提高。

在这里插入图片描述

事务隔离与视图

在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。

  • “读未提交”隔离级别下,直接返回记录上的最新值,没有视图概念
  • 在“读提交”隔离级别下,这个视图是在每个SQL语句开始执行的时候创建的
  • 在“可重复读”隔离级别下,这个事务是在事务启动的时候创建的,整个事务存在期间都会用到这个视图
  • “串行读”隔离级别下,直接用加锁的方式来避免并行访问

我们可以看到在不同的隔离级别下,数据库行为是有所不同的。

  • Oracle 数据库的默认隔离级别其实就是“读提交”,因此对于一些从 Oracle 迁移到 MySQL 的应用,为保证数据库隔离级别的一致,你一定要记得将 MySQL 的隔离级别设置为“读提交”。
  • MySQL的默认基本是“可重复读”,但是它依托于MVCC机制,解决了脏读、脏写、不可重复读、幻读的问题。MVSS是基于undo log多版本链条+ReadView机制来做的

对于RR,你可以这么想,每个事务启动的时候打一个快照,别人改的“我不听我不听”

配置的方式是,将启动参数 transaction-isolation 的值设置成 READ-COMMITTED。你可以用 show variables 来查看当前的值。

mysql> show variables like 'transaction_isolation';
+-----------------------+----------------+
| Variable_name | Value |
+-----------------------+----------------+
| transaction_isolation | READ-COMMITTED |
+-----------------------+----------------+

MySQL默认设置的事务隔离级别,是RR级别的

假设你要修改MySQL的默认事务隔离级别,是下面的命令,可以设置级别为不同的
level,level的值可以是REPEATABLE READ,READ COMMITTED,READ UNCOMMITTED,SERIALIZABLE几种级别。

SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;

但是一般来说,真的其实不用修改这个级别,就用默认的RR其实就特别好。除非你一定要在你的事务执行期间多次查询的时候,必须要查到别的已提交事务修改过的最新值,就可以改为READ_COMMITTED级别

事务隔离的实现

隔离的实现主要有读写锁和MVCC(Multi-Version Concurrency Control)多版本并发处理方式。

读写锁(多个事务更新同一行数据)

最简单直接的的事务隔离实现方式

  • 每次读操作需要获取一个共享(读)锁,每次写操作需要获取一个写锁。
  • 共享锁之间不会产生互斥,共享锁和写锁之间、以及写锁与写锁之间会产生互斥。
  • 当产生锁竞争时,需要等待其中一个操作释放锁后,另一个操作才能获取到锁。

锁机制,解决的就是多个事务同时更新数据,此时必须要有一个加锁的机制

  • 行锁(记录锁):解决的就是多个事务同时更新一行数据
  • 间隙锁:解决的就是多个事务同时更新多行数据

MVCC

  • 在读写锁中,读和写的排斥作用大大降低了事务的并发效率,于是人们又提出了能不能让读写之间也不冲突的方法,就是读取数据时通过一种类似快照的方式将数据保存下来,这样读锁就和写锁不冲突了。
  • 不同的事务session会看到自己特定版本的数据,即使其他的事务更新了数据,但是对本事务仍然不可见,本事务看到的数据始终是第一次查询到的数据。
  • 在数据库中,这个快照的处理方式叫多版本并发控制(Multi-Version Concurrency Control)。
  • 这种方式真正实现了非阻塞读,只有在写操作时才需要加行级锁,因此并发效率更高。

在各个数据库系统的,MVCC的实现机制不尽相同,下面来详细介绍一下InnoDB是如何实现MVCC的,主要讨论可重复读级别的实现。

两个概念

首先,需要了解两个概念:ReadView、undo log

ReadView
  • ReadView其实就是上面提到的快照
  • ReadView主要是用来做可见性判断的,即通过ReadView可以知道:哪些事务的提交结果对当前事务可见,哪些事务的提交结果对当前事务不可见。
  • 关于如何判断可见性,是根据可预见性判断算法来的
undo log

readView是事务的快照,但是通过ReadView仅仅能知道哪些事务的提交结果对当前事务可见,可是还是不知道当前事务的数据快照在哪啊。undo log就是来解决这个问题的。

undo log就是我们通常说的回滚日志,undo log存放的是数据的历史记录,也可以叫数据的快照。

"快照"在MVCC中是怎么工作的?

在可重复读的级别下,事务在启动的时候就“快照”。注意,这个快照是基于整个库的。

这个好像看上去不太现实啊。如果一个库有100G,那么我启动一个事务,MySQL就要拷贝100G的数据出来,这个过程地多慢啊。

实际上,我们并不需要拷贝这100G的数据。我们先来看看这个快照是怎么实现的。

  • InnDB里面每个事务都有一个唯一的事务ID,叫做transaction id。它是在事务开始的时候向InnoDB的事务系统申请的,是按照申请顺序严格递增的

  • 而每行数据也都是有多个版本的。每次事务更新的时候,都会生成一个新的数据版本,并且把transaction id赋值给这个数据版本的事务ID,记作row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它

  • 也就是说,数据表中的一行记录,其实可能有多个版本 (row),每个版本有自己的 row trx_id。
    在这里插入图片描述

  • 图中虚线框里是同一行数据的 4 个版本,当前最新版本是 V4,k 的值是 22,它是被transaction id 为 25 的事务更新的,因此它的 row trx_id 也是 25。

  • 每次语句更新都会生成undo log(回滚日志),上图中三个虚线箭头,就是undo log;而V1、V2、V3并不是物理上真实存在的,而是每次需要的时候根据当前版本和undo log计算出来的。比如,需要V2的时候,就是通过V4依次执行U3、U2算出来的。

  • 按照可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。

  • 因此,一个事务只需要在启动的时候声明说:“以我启动的时刻为准,如果一个数据版本是在我启动之前生成的,就认;如果是在我启动之后才生成的,我就不认,我必须要找到它的上一个版本”

  • 当然,如果“上一个版本”也不可见,那么就需要继续向前找。还有,如果是这个事务自己更新的数据,它自己还是要认的。

  • 在实现上,InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务ID,“活跃”是指,启动了但是还没有提交。

  • 数组里面事务ID的最小值记为低水位,当前系统里面已经创建过的事务ID的最大值加1为高水位

  • 这个视图数组和高水位,就组成当前事务的一致性视图(read-view)

  • 而数据版本的可见性规则,就是基于数据的 row trx_id 和这个一致性视图的对比结果得到
    的。

  • 这个视图数组把所有的 row trx_id 分成了几种不同的情况。

在这里插入图片描述

这样,对于当前事务的启动瞬间来说,一个数据版本的 row trx_id,有以下几种可能:

  1. 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;
  2. 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;
  3. 如果落在黄色部分,那就包括两种情况
    a. 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见;
    b. 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。

你看,有了这个声明后,系统里面随后发生的更新,是不是就跟这个事务看到的内容无关了呢?因为之后的更新,生成的版本一定属于上面的 2 或者 3(a) 的情况,而对它来说,这些新的数据版本是不存在的,所以这个事务的快照,就是“静态”的了。

InnoDB利用了“所有数据都有多个版本”这个特性,实现了“秒级创建快照”的能力

可重复读实现

  • 在MySQL中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。

  • 假设一个值从 1 被按顺序改成了 2、3、4,在回滚日志里面就会有类似下面的记录。

在这里插入图片描述

  • 当前值是4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的read-view。

    • 如图所示,在视图A、B、C里面,这一个记录的值分布是1、2、4,同一条记录在系统中可以有多个版本,就是数据库的多版本并发控制(MVCC)。
    • 对于read-view A,要得到1,就必须将当前值依次执行图中所有的回滚操作得到。
  • 同时你会发现,即使现在有另外一个事务正在将4改成5,这个事务跟read-view A、B、C对应的事务是不会冲突的。

  • 那么回滚日志什么时候会删除呢?当系统判断没有事务再需要用到这些回滚日志时,就会删除这个日志。

  • 什么时候才不需要了呢?就是当系统里没有比这个回滚日志更早的 read-view 的时候。

为什么建议尽量不要使用长事务。

  • 长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能会访问数据库里面的任何数据,所以这个事务提交前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。

  • 在MySQL 5.5以及之前的版本,回滚日志是跟数据字典一起放在ibdata文件里面的,即使长事务最终提交,回滚段被清理,文件页不会变小。我见过数据只有 20GB,而回滚段有200GB 的库。最终只好为了清理回滚段,重建整个库。

  • 除了对回滚段的影响,长事务还占用锁资源,也可能拖垮整个库

如何避免长事务对业务的影响

这个问题,我们可以从应用开发端和数据库端来看

(1) 从应用开发端来看

  • 确认是否使用了set autocommit=0。这个确认工作可以在测试环境中开展,把MySQL 的 general_log 开起来,然后随便跑一个业务逻辑,通general_log 的日志来确认。一般框架如果会设置这个值,也就会提供参数来控制行为,你的目标就是把它改成 1
  • 确认是否有不必要的只读事务。有些框架会习惯不管什么语句先用 begin/commit 框起来。或者好几个 select 语句放到了事务中。这种只读事务可以去掉
  • 业务连接数据库时,根据业务本身的评估,通过 SET MAX_EXECUTION_TIME命令,来控制每个语句执行的最长时间,避免单个语句意外执行太长时间。

(2)其次,从数据库端来看:

  • 监控 information_schema.Innodb_trx表,设置长事务阈值,超过就报警/或者kill
  • Percona 的 pt-kill 这个工具不错,推荐使用;
  • 在业务功能测试阶段要求输出所有的 general_log,分析日志行为提前发现问题;
  • . 如果使用的是 MySQL 5.6 或者更新版本,把 innodb_undo_tablespaces 设置成 2(或更大的值)。如果真的出现大事务导致回滚段过大,这样设置后清理起来更方便。

事务的启动方式

MySQL 的事务启动方式有以下几种:

  1. 显式启动事务语句, begin 或 start transaction。配套的提交语句是 commit,回滚语句是 rollback。
  2. set autocommit=0,这个命令会将这个线程的自动提交关掉。意味着如果你只执行一个 select 语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开连接。

因此,会建议总是使用 set autocommit=1, 通过显式语句的方式来启动事务。

set autocommit=1会不会导致“多一次交互”的问题呢?

会,但是可以使用commit work and chain 语法解决

  • 当设置了 autocommit=1,用begin显式启动的事务,如果执行commit则提交事务。
  • 如果执行commit work and chain ,则是提交事务并自动启动下一个事务,这样也省去了再次执行begin语句的开销。
  • 同时带来的好处是从程序开发的角度明确地知道每个语句是否处于事务中。

你可以在 information_schema 库的 innodb_trx 这个表中查询长事务,比如下面这个语句,用于查找持续时间超过 60s 的事务。

select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60

比较行锁、表锁、间隙锁

首先锁的存在,目的是为了在并发场景下,保持数据的安全、一致。
并发场景有:

  • 读-读 :此并发场景不需要进行并发控制,也就是不需要加锁。
  • 读-写 :此并发场景需要并发控制,不然就会出现脏读,幻读,不可重复读的问题。
  • 写-写 :此并发场景需要并发控制,不然就会出现更新丢失的问题。

进行并发控制,常规手段就是加锁,其中mysql的锁有以下几种:

  • 行锁:锁住表中的一行;比如 update user set name=‘张三’ where id=1;会锁住id=1的那一行数据,其他事务再想更新,就只能等前一个事务释放锁。
  • 表锁:锁住整个表,比如update user set name=‘张三’;由于没有加where条件,此更新sql会对整个表进行更新,也就是会锁住整个表。
  • 间隙锁:比如事务A执行update user set name=‘张三’ where id >1 and id<4; 假如表中只有id=1、2 两条数据,A事务还没提交,那么此时事务B再次插入一条id=3的数据,理论上是允许的,但是实际上是B只能等A提交,因为事务A执行的是id>1and id<4,范围涵盖了id=3的,也即是把id=3的这个间隙也给锁了,叫做间隙锁。

除了这3种锁,还有乐观锁、悲观锁、记录锁、自增锁、意向锁;

既然有了行锁、表锁、间隙锁,为什么还需要MVCC呢?

  • 为了优化性能
  • 虽然所可以保证数据安全,但是加了锁会导致并发性能的降低。因为能不用锁就尽量不用锁
  • 在读-读、读-写、写-写这3种并发场景中,读-写 可以不使用锁,而是使用MVCC来实现数据的并发操作以及安全一致性。
  • 因此,mysql是同时使用了MVCC+行锁、表锁、间隙锁来保证了数据安全,又尽可能大的实现了性能最优化。

RR模式下幻读的问题我觉得要分快照读和当前读两种情况来讨论:

  • 对于快照读,RR中一个事务的所有快照读读取的都是同一份快照,所以无论其他的事务怎么修改,无论是更新还是插入删除,都不会影响当前事务的快照读结果,也就不会出现不可重复读、幻读的情形。
  • 对于当前读,你读取的行,以及行的间隙都会被加锁,直到事务提交时才会释放,其他的事务无法进行修改,所以也不会出现不可重复读、幻读的情形。

事务隔离,为什么你改了我还看不见

因为你还没有提交

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值