面试官:MYSQL事务隔离与MVCC多版本并发控制知道吗?
来源:SegmentFault
时间:2023-01-21 08:03:53 151浏览 收藏
小伙伴们对数据库编程感兴趣吗?是否正在学习相关知识点?如果是,那么本文《面试官:MYSQL事务隔离与MVCC多版本并发控制知道吗?》,就很适合你,本篇文章讲解的知识点主要包括MySQL、Java、数据库、python。在之后的文章中也会多多分享相关知识点,希望对大家的知识积累有所帮助!
数据库的事务隔离
前段时间,公司内部遇到了一个问题,就是我们创建的同一批任务,别分配给了不同的实例去执行,导致线上的结果出现问题。
另一个组的leader说没有开启事务,设置下事务就可以。
数据库一出现一致性问题,就说开启事务,我就有点疑惑,数据库的事务到底是怎么保证一致性的。
在看下面的内容,我们可以先思考几个问题。
数据库的隔离级别都有什么?数据库的MVVC视图是怎么实现的?
数据库的隔离级别是为了解决什么问题的?
看完上面三个问题,自己能回答上来几个呢?不急。我们继续往下看
数据库的事务
数据库的事务我们简单来说就是用来保证数据的正确性,它只有两个操作:事务要么成功,要么失败并进行回滚。
为什么这么做呢?这是因为一般我们进行事务操作,都会进行一组操作。比如你常见的金融转账。
在这个转账事务里面包含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进行自动提交,我们就将自动提交进行关闭
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,
- 事务启动的时候,能看到所有已经提交事务的结果,但是他启动之后,其他事务的变更是看不到的。
- 当事务启动的瞬间,除了已经提交的事务,创建的瞬间还会存在正在运行的事务,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多版本并发控制实现原理。
事务在面试中是比较多的一个点,这样的题目可以多种变换,我们刚开始题目提到的三个问题已经可以解答了。
你来尝试回答下?
下期会说下数据库中的幻读,幻读也是面试中经常遇到的问题哦。
今天带大家了解了MySQL、Java、数据库、python的相关知识,希望对你有所帮助;关于数据库的技术知识我们会一点点深入介绍,欢迎大家关注golang学习网公众号,一起学习编程~
-
499 收藏
-
244 收藏
-
235 收藏
-
157 收藏
-
101 收藏
-
259 收藏
-
411 收藏
-
476 收藏
-
312 收藏
-
244 收藏
-
195 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 507次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 497次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 484次学习
-
- 贪玩的糖豆
- 很好,一直没懂这个问题,但其实工作中常常有遇到...不过今天到这,看完之后很有帮助,总算是懂了,感谢作者分享文章!
- 2023-03-06 07:42:09
-
- 体贴的白开水
- 好细啊,mark,感谢up主的这篇文章内容,我会继续支持!
- 2023-02-18 02:36:24
-
- 年轻的枫叶
- 这篇技术贴出现的刚刚好,好细啊,赞 👍👍,已收藏,关注作者大大了!希望作者大大能多写数据库相关的文章。
- 2023-02-12 15:55:20
-
- 秀丽的大米
- 这篇技术文章真是及时雨啊,太细致了,受益颇多,码起来,关注作者了!希望作者能多写数据库相关的文章。
- 2023-01-21 21:32:45
-
- 温暖的小蝴蝶
- 赞 👍👍,一直没懂这个问题,但其实工作中常常有遇到...不过今天到这,看完之后很有帮助,总算是懂了,感谢大佬分享文章!
- 2023-01-21 15:10:55