当前位置: 首页 > 后端技术 > Java

面试官一口气问了MySQL事务、锁和MVCC,I_0

时间:2023-04-02 00:49:05 Java

面试官:您是如何理解InnoDB引擎中的事务的?考生:在我的理解中,一个事务可以使“一组操作”要么全部成功,要么全部失败。考生:事务的目的是“保证数据的最终一致性”。应聘者:比如我用支付宝给你发了一个888元的红包。那么自然我的支付宝余额会被扣888元,你的支付宝余额会增加888元。考生:而交易是保证我扣余额和你加余额同时成功或失败,这样才能正常转账。面试官:那么,你了解交易的主要特点吗?考生:嗯,就是ACID,分别是Atomicity,Consistency,Isolation,Durability。候选:原子性是指当前事务的操作要么同时成功,要么同时失败。原子性由undolog保证,因为undolog记录的是数据被修改前的信息。考生:比如我们要插入一条数据,undolog会记录一条对应的deletelog。当我们要更新一条记录时,undolog会记录上一次“旧值”的更新记录。考生:如果事务执行过程中出现异常,则进行“回滚”。InnoDB引擎使用undolog中记录的数据将数据“恢复”到事务开始之前。应聘者:一致性这个我以后再说。先说隔离。最重要的是:当事务“并发”执行时,它们内部的操作不能相互干扰。如果多个事务可以同时对一条数据进行操作,就会出现脏读、重复读、幻读等问题。考生:因此,事务之间需要有“一定”的隔离。在InnoDB引擎中,定义了四种隔离级别供我们使用:Candidates:分别是:读未提交(readuncommitted),读提交(readcommitted),可重复读(repeatableread),可序列化(serializable)候选:不同的隔离级别有事务之间的不同隔离(级别越高,事务隔离越好,但性能越低),而隔离是通过MySQL的各种锁来实现的,只是它掩盖了加锁的细节。考生:持久性是指事务一旦提交,它对数据库的改变应该是永久的。说白了就是数据会持久化在硬盘上。考生:持久化是由重做日志保证的。当我们要修改数据时,MySQL首先找到这条记录所在的“页”,然后将页加载到内存中,并修改相应的记录。考生:为了防止内存被修改,MySQL会挂掉(如果内存被改了,直接挂掉,那么这次的修改就相当于丢失了)。考生:MySQL介绍重做日志。内存写入后,会写入redolog。这个redolog记录的是这次对某个页面进行了哪些修改。考生:即使MySQL中途挂了,我们还是可以根据redolog来恢复数据。考生:redolog是顺序写入的,写入速度很快。并且记录物理修改(xxxx页有xxx修改),文件体积小,恢复速度也很快。候选人:我们再谈谈一致性。“一致性”可以理解为使用事务的“目的”,而“隔离性”、“原子性”、“持久性”都是保证“一致性”的手段,保证一致性候选者需要通过应用代码来保证:例如,如果事务过程中出现异常情况,你要回滚事务,而不是强行事务导致数据不一致。面试官:嗯,挺好的。我谈了很多。面试官:刚才你也提到了隔离,然后你说MySQL有四种隔离级别。能分别介绍一下吗?考生:嗯,为了把隔离级别说清楚,顺便说一下MySQL锁相关的知识。考生:在InnoDB引擎下,按照锁的粒度,可以简单的分为行锁和表锁。考生:行锁其实作用于索引(上次已经讲过索引,这里不再赘述)。当我们的SQL命中索引时,锁定的是命中条件下的索引节点(这是行锁)。如果索引没有命中,那么我们锁定的就是整个索引树(表锁)。Candidates:简单的说,整棵树或者某些节点是否被锁定,完全取决于SQL条件是否命中对应的索引节点。考生:行锁可以简单的分为读锁(共享锁,S锁)和写锁(排他锁,X锁)。考生:读锁是共享的,多个事务可以同时读取同一个资源,但不允许其他事务修改。写锁是排他的,写锁会阻塞其他的写锁和读锁。考生:我现在回到隔离级别,举个例子说明一下。面试官:嗯……应聘者:先说readuncommit(读未提交)。例如:A给B转钱,A执行了转账语句,但是A还没有提交交易,B读取数据发现他账户里的钱增加了!B告诉A我收到了钱。A回滚交易[rollback],当B再次查看账户中的钱时,发现钱不多了。候选人:简单的定义就是:事务B读取了事务A还没有提交的数据。这在技术术语中称为“脏读”。考生:对于锁维度,其实在readuncommit隔离级别下,读不会加锁,写会加排他锁。读没有加锁,导致独占锁不可能是独占的。考生:而且我们知道对于更新操作,InnoDB肯定会加写锁(数据库不可能允许同一条记录同时被更新)。至于读操作,如果不加锁,就会造成上面的脏读。考生:脏读在生产环境中是肯定不能接受的。如果读上锁,意味着当数据更新时,没有办法读取,会大大降低数据库的性能。考生:在MySQLInnoDB引擎层面,有一个新的解决方案(解决加锁后的读写性能问题),叫MVCC(Multi-VersionConcurrencyControl)。多版本并发控制的候选:在MVCC下,可以做到读写不阻塞,避免脏读之类的问题。那么MVCC是如何做到的呢?Candidate:MVCC生成一个数据快照(Snapshot),并利用这个快照提供一定级别(语句级别或事务级别)的一致性读Candidate:回到事务隔离级别,对于读提交(readcommitted)隔离级别,它生成语句级快照,而对于可重复读(repeatableread),它生成事务级快照。考生:前面说了readuncommit隔离级别下会发生脏读,readcommit(readcommitted)隔离级别解决脏读。思路其实很简单:读取时产生一个“版本号”,直到其他事务提交后,才会读取最新提交的“版本号”数据。候选:例如:事务A读取记录(生成版本号),事务B修改记录(此时加写锁),事务A再次读取时,根据最新版本号读取(当事务B执行commit,会产生一个新的版本号),如果事务B还没有提交,那么事务A还是读取之前版本号的数据。考生:通过“版本”的概念,解决了脏读问题,而“版本”其实就是快照对应的数据。候选:读提交(readcommitted)解决了脏读,但是还有其他的并发问题。“不可重复读”:一个事务读取另一个事务提交的数据,也就是说一个事务可以看到其他事务所做的修改。候选:不可重复读示例:A查询数据库获取数据,B修改数据库中的数据,导致A多次查询数据库结果不同【危害:A的每次查询结果都被B影响】考生:了解了MVCC的基础知识后,很容易想到可重复读(repeatableread)隔离级别是如何避免不可重复读的问题(前面也提到过)。考生:可重复读(repeatableread)隔离级别是“事务级别”的快照!每次读取都是“当前事务的版本”,即使当前数据被其他事务修改(commit),也只会读取当前事务版本的数据。考生:可重复读(repeatableread)隔离级别会有幻读的问题。“幻读”是指在一个事务中读取其他事务插入的数据,导致读取前后不一致。候选:在InnoDB引擎下的可重复读(repeatableread)隔离级别下,在snapshotreadMVCC的影响下,幻读问题已经解决(因为是读取历史版本数据)候选:而如果是当前read(指select*fromtableforupdate),需要配合gaplock来解决幻读问题。考生:剩下的就是可序列化(serializable)隔离级别了。它的最高隔离级别相当于不允许事务并发。事务之间的执行是串行的。它的效率最低,但也是最安全的。采访者:嗯,是的。我看你提到了MVCC,为什么不说说他的原理呢?考生:MVCC主要是通过readview和undolog实现考生:undolog前面也提到了,它会记录修改数据前的信息,事务中的原子性是通过undolog实现的。因此,就有了undolog可以帮助我们找到“版本”数据候选者:而readview其实就是在查询的时候,InnoDB会生成一个readview。读视图有几个重要字段,分别是:trx_ids(尚未提交Commit的事务版本号集合)、low_limit_id(下次要生成的事务ID值)、low_limit_id(尚未提交的事务ID的最小值)versionnumber)和creator_trx_id(当前交易版本号)候选:每行数据中有两个隐藏字段分别是DB_TRX_ID(记录当前ID)和DB_ROLL_PTR(指向undolog中上一版本数据的位置指针).“版本”实现非阻塞读写,版本数据存在undolog中。考生:对于不同的隔离级别(readcommit和repeatableread),无非就是readcommit隔离级别下每次获取一个新的readview,repeatableread隔离级别下每个事务只获取一个readview面试官:嗯,好的。不细说了,今天就到这里吧。本文总结:为了保证数据的最终一致性,事务具有四大特性,即原子性、一致性、隔离性和持久性。原子性由撤销日志保证。持久性由重做日志保证。隔离由数据库隔离级别保证。选项包括读取取消提交、读取提交、可重复读取和可序列化。一致性是交易的目的。一致性由应用程序保证。事务并发会出现脏读、重复读、幻读等各种问题。以上不同的隔离级别可以解决并发事务带来的问题,而隔离级别实际上是通过MySQL锁来实现的。频繁加锁会导致数据库性能变差。引入MVCC多版本控制,实现非阻塞读写。MVCC提高数据库性能的原理是通过readview和undolog来实现的