面试官:MYSQL事务隔离与MVCC多版本并发控制知道吗?

数据库的事务隔离

前段时间,公司内部遇到了一个问题,就是咱们建立的同一批任务,别分配给了不一样的实例去执行,致使线上的结果出现问题。面试

另外一个组的leader说没有开启事务,设置下事务就能够。sql

数据库一出现一致性问题,就说开启事务,我就有点疑惑,数据库的事务究竟是怎么保证一致性的。数据库

在看下面的内容,咱们能够先思考几个问题。数组

数据库的隔离级别都有什么?

数据库的MVVC视图是怎么实现的?session

数据库的隔离级别是为了解决什么问题的?并发

看完上面三个问题,本身能回答上来几个呢?不急。咱们继续往下看cors

数据库的事务

数据库的事务咱们简单来讲就是用来保证数据的正确性,它只有两个操做:事务要么成功,要么失败并进行回滚。spa

为何这么作呢?这是由于通常咱们进行事务操做,都会进行一组操做。好比你常见的金融转帐。日志

在这个转帐事务里面包含2个操做:code

  • 扣本身银行帐户的钱
  • 给对应的帐户添加收到的钱。

如今思考下,若是咱们没有添加事务,那么会出现什么样的状况呢?

  1. 若是先扣钱成功,执行给别人加钱失败。而钱已经扣了,对方没收到钱,你说怎么办?
  2. 若是先给对方加钱,而扣你钱的时候没扣成功。这钱银行给的补助吗?嘿嘿,那银行确定不开心。

因此了咱们只能在这种操做中使用事务,来保证执行的成功与失败,失败了要进行回滚,保证扣钱的操做也不执行。

事务的ACID

事务具备四个特性,这四个特性简称为ACID

  • 原子性Atomicity:同一组操做,要么作,要么不作,一组中的一两个执行成功不表明成功,全部成功才能够。这就是原子性,作或者不作(失败进行回滚)。
  • 一致性Consistency:数据的一致性,就像上面的举例说的,你扣钱了,对方没加钱,那确定不行。
  • 隔离性Isolation:多个数据库操做同一条数据时,不能互相影响。不能你这边变更,那边数据空间就变换了。
  • 持续性Durability: 事务结果提交后,变更就是永久性的,接下来的操做或者系统故障不能让这个记录丢失。

今天主要说的事务就是隔离。看看事务是怎么保证数据之间的隔离

事务的隔离级别

不一样的事务隔离级别对应的不一样的数据执行效率,隔离的越严格,那么执行的效率就约低下,下面的四个隔离级别是原来越严格。

  • 读未提交(read uncommitted):指数据在事务执行时,尚未提交,其余事务就能够看到结果
  • 读提交(read committed):指数据在其事务提交后,其余事务才能看到结果。视图是在执行sql语句的时候进行建立,具体视图看下面的数据隔离是怎么实现的
  • 可重复读(repeatable read):一个事务在执行过程当中,看到的结果与其启动的时候看到的内容是一致的。启动的时候会建立一个视图快照,该事务状态下,会看的一致是这个视图快照内容,其余事务变动是看不到的。注意是读取的过程,若是是更新,那么会采用当前读,就是其余事务的更新操做会拿到结果,用来保证数据一致性
  • 串行化(serializable):顾名思义,就是将多个事务进行串行化(读写过程当中加锁,读写冲突,读读冲突),一个事务结束后,另一个事务才能执行,带来的效果就是并行化很差,效率低下。

Mysql中默认的事务隔离级别是可重复读,使用下面这个命令进行查看当前的事务级别,

show variables like 'transaction_isolation';

# 下面的语句进行修改事务的级别。

SET session TRANSACTION ISOLATION LEVEL Serializable;(参数能够为:Read uncommitted,Read committed,Repeatable,Serializable)

事务级别

事务的启动方式

在程序中,咱们不少时候都是默认的自动提交,也就是一个sql操做就是一条事务,但有时候须要的是多个SQL进行组合,咱们就要显式的开启事务。

显示开启的语句是用 begin或者 start transaction.一样在事务结束的时候使用commit进行提交,失败使用rollbakc进行回滚。

固然若是不想让SQL进行自动提交,咱们就将自动提交进行关闭set autocommit=0,这样事务就不会自动提交,须要咱们手动的执行commit.

如何避免长事务

关闭自动提交事务后,就须要咱们来本身提交事务,这时候每一个语句执行都是这样的。

begin

sql语句

commit

若是咱们在程序编写中,原本一个sql解决的操做,结果忘记进行事务的提交,到下下下一个SQL才进行commit,这样就会出现长事务。

而长事务每每会形成大量的堵塞与锁超时的现象,事务中若是有读写(读读不冲突,读写冲突,写写冲突)操做,那么会将数据进行锁住,其余事务也要进行等待。

因此在程序中,咱们应该尽可能避免使用大事务,一样也避免咱们写程序的时候出现偶然的大事务(失误😁)。

解决办法是咱们将自动提交打开,当须要使用事务的时候才会显示的开启事务

程序中出现大量的事务等待怎么办

在MySQL中想定位一个长事务问题仍是很方便的。

首先咱们先找到正在执行的长事务是什么。

select t.*,to_seconds(now())-to_seconds(t.trx_started) idle_time from INFORMATION_SCHEMA.INNODB_TRX t G

该语句会展现出事务的执行的开始时间,咱们能够很简单的算出,当前事务执行了多久,其中上面的idle_time就是执行的事务时间

假设咱们如今设定的超过30s执行的事务都是长事务,可使用下面语句进行过滤30s以上的长事务。

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

经过information_schema.innodb_trx 咱们就能定位到长事务。

从而决定长事务是否要被杀死,仍是继续等待。若是出现死锁的状况,处理方式也是相似,查找到死锁的语句,而后进行杀死某个语句的执行(有点暴力)。

数据隔离是怎么实现的

注意:本次数据隔离是创建在可重复读的场景下

在可重复读的场景下,咱们了解每次启动事务的时候,会在当前启动一个视图,而这个视图是整个数据库的视图快照。

嘿嘿,是否是想数据库那么大,为啥咱们没有感受到建立快照时间的消耗呢?

这是由于数据库建立的视图快照利用了全部数据都有多个版本的特性,来实现快速建立视图快照的能力

那数据多个版本是怎么回事呢?

准备下数据

先别急,咱们准备下数据。

如今建立一个表,而且插入三条数据。

create table scores

(

id int not null

primary key,

score float null

);

INSERT INTO scores (id, score) VALUES (1, 3.5);

INSERT INTO scores (id, score) VALUES (2, 3.65);

INSERT INTO scores (id, Score) VALUES (3, 4);

在开始使用前咱们要了解两个小知识点。begin/start transaction 与 start transaction with consistent snapshot。

  • begin/start transaction 视图的建立是创建在begin/ start transaction 以后SQL语句才会建立视图, 好比 下面案例
begin

select source from scores; //视图是在这里开始建立 而不是在begin那里建立

commit
  • start transaction with consistent snapshot:则是该语句执行后,就建立视图。

了解上面两个建立事务的区别后,咱们来看下视图是怎么建立出来多个数据版本的. 如下SQL在两个窗口打开。

事务A | 事务B | 结果

start transaction with consistent snapshot | 开启事务,并建立视图 |

--| start transaction with consistent snapshot |开启事务,并建立视图

select score from scors where id =2 | -- | 事务A中的值为3.65

-- | update scores set scores = 10 where id =2 | 事务B修改成10

--| select score from scores where id =2 | 事务B显示为10

select score from scores where id =2 | --| 事务A显示为3.65

select score from scores where id =2 for update | --| 会被锁住,等待事务B释放锁(间隙锁)

-- |commit | 提交事务B

select score from scores where id =2 for update | --| 这个语句能够看到变成了10(利用了当前读)

select score from scores where id =2 | --| 不加 for update 那么结果仍是3.65

commit|---|---| 提交A的结果

上述流程就是两个不一样的请求过来,对数据库同一个表的不一样操做。

当事务A执行start transaction with consistent snapshot以后,A的视图就开始被建立了,这时候是看不到事务B对其中的修改,就算事务Bcommit以后,只要事务A不结束,它看到的结果就是它启动时刻的值。

这就与不重复提交,执行过程当中看到的结果与启动的时候看到的结果是一致的这句话对应上了

快照多版本

前面说了,快照是事务的启动的时候是基于整个数据库的,而整个数据库是很大,那MYSQL是怎么让咱们无感并快速建立一个快照呢。

快照多版本你能够认为是由如下两部分构成。

  • 事务id(transaction id):这个是由事务启动的时候向InnoDB启动时申请的。而且必定注意哦它是递增的。
  • row trx_id:这个id其实就是事务ID,每次事务更新数据的时候回将事务ID赋值给这个数据版本的事务ID上,将这个数据版本的事务ID称为 row trx_id.

当一行记录存在多个数据版本的时候,那么就有多个row trx_id 。举个例子

版本 | 值|事务ID| 对应的语句操做

v1 | score =3 | 89| --

v2 | score =5| 90| update scores set score = 5 where id =3; select score from scores where id =3;|

v3 | score = 6 | 91 | update scores set score = 6 where id =3;|

v1->v2->v3 这里面涉及了三个版本的迭代。中间是经过undo log 日志来保存更新的记录的。

注意启动快照以后,可重复读隔离状况下,获取到v1的值,不是说MYSQL直接存储的该值,而是利用如今这条记录的最新版本与undo log日志计算出来的,好比经过v3 ->v2—>v1 计算出v1中score值。

版本图

版本计算

上面简单说了下版本的计算规则,可是在MYSQL中,版本并非那么简单的计算的,咱们如今来看下到底怎么计算的,

这个两点咱们在注意一下:

  • 事务在启动的时候会向InnoDB的事务系统申请事务ID,这个事务ID是严格递增的。
  • 每行数据是多个版本,这个版本的id就是row trx_id,而事务更新数据(更新数据的时候才会生成一个新的版本)的时候会生成一个新的数据版本,并把事务ID赋值给这个数据的事务ID==row trx_id,
  1. 事务启动的时候,能看到全部已经提交事务的结果,可是他启动以后,其余事务的变动是看不到的。
  2. 当事务启动的瞬间,除了已经提交的事务,建立的瞬间还会存在正在运行的事务,MYSQL是把这些正在运行的事务ID放入到一个数组中。数组中最小的事务ID记为低水位,当前系统中建立过的事务ID最大值+1记为高水位。
举个简单的例子。

a. 注意一点:获取事务ID与建立数组不是一个原子操做,因此存在事务id为8,而后又存在当前MYSQL中存在活跃事务ID为9 10的事务。

b. 事务ID低于低水位那么对于当前事务确定是可见的,事务ID高于高水位的事务ID值,则对当前事务不可见.

c. 事务ID 位于低水位与高水位之间分为两种状况。

  • 若是事务id是在活跃的数组中表示这个版本是正在执行,可是结果尚未提交,因此这些事务的变动是不会让固然事务看到的。
  • 事务id若是没有在活跃数组中,表明这个事务是已经提交了,因此可见。好比如今建立了90,91,92三个事务,91执行的比较快,提交完毕,90和92尚未提交.这时候建立了一个新的事务id为93,那么在活跃的数组中的事务就是90,92,93,你看91是已经提交了,它的事务还在这个低水位与高水位之间,但结果对于93是可见。

总的上面来讲就是你在我建立的时候事务结果已经提交,那么是可见的,以后提交那么就是不可见的。

读取流程

上面简单说了下老版本视图中的数据是经过最新的版本与undo log 计算出来的,那到底怎么就算的呢?

事务A | 事务B | 结果

start transaction with consistent snapshot 事务 id 89| 开启事务,并建立视图 |

--| start transaction with consistent snapshot 事务id 92 |开启事务,并建立视图

select score from scors where id =2 | -- | 事务A中的值为3.65

-- | update scores set scores = 10 where id =2 | 事务B修改成10

--| select score from scores where id =2 | 事务B显示为10

select score from scores where id =2 | --| 事务A显示为3.65

commit|---|---| 提交A的结果

仍是看这个事务操做。

下面是数据变更的流程。

  • 假设开始以前有两个活跃的事务ID为 78,88.
  • 事务A启动的时候会将78 88,包含它本身放入到活跃数组中。
  • 事务A 操做的语句select score from scors where id =2将其看到的结果认为是v1版本数据好比其如今row trx_id(注意:row trx_id是数据行被更新后事务id才会赋值给row trx id上)是86,而且保存好。
  • 事务B启动时,会发如今活跃数组是78,88,89,本身的92.
  • 事务B 执行更新语句语句后,会生成一个新的版本V2,数据变换就是V1-->V2。记录中间变化的是undo log日志。这样ID 89存储的数据就变成了历史数据。数据版本row trx_id则是92
  • 事务A 查询score数据,就会经过先查到如今的V2版本视图,找到对应的row trx_id = 92,发现row trx_id 位于高水位上,则抛弃这个值,经过V2找到V1,row trx_id为86,而86大于低水位,而低于高水位89+1.可是因为86没有在活跃数组中,并且属于已经提交的事务,则当前事务是能看到该结果的,因此事务A能拿到读取的值。

你看通过简单的几步,咱们就拿到了想要读取的事务数据,因此不论事务A何时查询,它拿到的结果都是跟它读取的数据是一致的。

你看有了MVCC(多版本并发控制)计算别的事务更改了值也不会影响到当前事务读取结果的过程。

咱们常常说不要写一个长事务,经过上面的读取流程能够看到,长事务存在时间长的话,数据版本就会有不少,那么undo log日志就须要保存很久,这些回滚日志会占用大量的内存存储空间。

当没有事务须要读取该日志与版本数据的时候,这个日志才能够删除,从而释放内存空间。

更新流程

事务A | 事务B | 结果

start transaction with consistent snapshot 事务 id 89| 开启事务,并建立视图 |

--| start transaction with consistent snapshot 事务id 92 |开启事务,并建立视图

select score from scors where id =2 | -- | 事务A中的值为3.65

-- | update scores set scores = 10 where id =2 | 事务B修改成10

--| select score from scores where id =2 | 事务B显示为10

select score from scores where id =2 | --| 事务A显示为3.65

select score from scores where id =2 for update | --| 会被锁住,等待事务B释放锁(间隙锁)

-- |commit | 提交事务B

select score from scores where id =2 for update | --| 这个语句能够看到变成了10(利用了当前读)

select score from scores where id =2 | --| 不加 for update 那么结果仍是3.65

commit|---|---| 提交A的结果

上面说了读取的过程,其实在事务中,咱们还有更新流程,更新流程比较简单,更新过程咱们须要保证数据的一致性,不能说别人修改了,咱们还看不到,那样就会形成数据的不一致。

为了保证看到最新的数据,会对更新行的操做加锁(行锁),加锁以后,其余事务对行进行更新操做,必须等待其余事务commit以后才能获取到最新的值,这个过程被称为当前读

想要读取过程当中得到最新的值可使用 上面的语句select score from scores where id =2 for update ,就能够看到当前最新值。

总结

本小节主要梳理了事务的隔离级别,事务的MVCC多版本并发控制实现原理。

事务在面试中是比较多的一个点,这样的题目能够多种变换,咱们刚开始题目提到的三个问题已经能够解答了。

你来尝试回答下?

下期会说下数据库中的幻读,幻读也是面试中常常遇到的问题哦。