怎么样注册一个网站,全球速卖通大学,微商城系统开发,做影视网站推荐哪个服务器原标题#xff1a;从InnoDB了解MVCCMVCC全称是Multi-Version Concurrency Control#xff0c;即多版本并发控制。这是种很常用的技术#xff0c;现在几乎所有的关系数据库都支持它。平时它默默工作#xff0c;像个透明人#xff0c;似乎不用关心它的细节。但是当我们偶尔在…原标题从InnoDB了解MVCCMVCC全称是Multi-Version Concurrency Control即多版本并发控制。这是种很常用的技术现在几乎所有的关系数据库都支持它。平时它默默工作像个透明人似乎不用关心它的细节。但是当我们偶尔在数据库里面遇到一些奇怪问题时却不得不需要关注它。因为很可能这些“奇怪”的问题不过是MVCC里的正常行为而且MVCC的设计思路还能在我们日常的开发中起到一些借鉴作用。所以对于大部分开发者而言了解MVCC还是挺有意义的。说起来MVCC是怎么产生的呢其实看名字就能猜到啦和并发有关。这东西的原理挺简单我们自己也能设计哦首先我们回顾一下以前教科书里的数据库系统它的并发是怎么实现的呢如图当一个事务要读loan_id1001的这行行数据时会对其加读锁(S)而另一个事务要修改这行数据时会要求对其加写锁(X)。这样一来并发读是可以的但是读和写互相阻塞性能较低。然后有人想到了多版本的方式来提高性能灵感就是CopyOnWrite如图修改数据时写事务会插入并锁定一条新的数据(sn2)并不会影响旧数据(sn1)。所以读事务不需要加锁当写事务没有提交时可以继续读版本1的数据而写事务提交并释放锁以后读事务就可以读版本2的数据了。这里读和写的事务不再相互阻塞而且写不需要加锁并发性能得到了提高。接下来再处理下一致性的问题于是表可以变成这样从上图可以看出对读事务而言只需要读每一条loan_id的最大有效数据也就是sn2的数据而写事务会创建一个新版本3并在提交修改时将新版本的status从pending修改为success使其生效。而在读事务的执行过程中如果写事务完成了版本3的提交读事务能否读到版本3的数据呢答案当然是不能。读事务应该在最开始获得有效的最大sn也就是版本2。之后即使写事务提交了版本3读事务仍会以版本2作为最大有效版本。这样可以保证读数据的一致性。也就是说即使读到的是较老版本的数据也比读到一半老版本一半新版本的数据好。因为失去一致性的数据其实是错误的。到此为止一个简单的MVCC就倒腾出来了。是不是很简单呢这个设计简单但实用其实在许多数据仓库里还这么用着呢。不过这个设计确实太简单还有一些重要问题需要解决1. 无效的老版本数据怎么处理对数据仓库可以一直保留但对普通应用通常不合适。2. 并发修改怎么处理对数据仓库修改的事务只需要一个就是ETL job但对普通应用显然不够啊。好啦接下来我们还是去看看别人家孩子吧。毕竟别人家孩子早就会打酱油了咱们还是别光自己折腾啦。首先我们来看一下Mysql InnoDB的MVCC实现。关于InnoDB在《高性能MySql》里面有段简化描述它通过两个隐藏列实现。两个列一个保存了行的创建时间另一个保存了过期时间(实际保存的是系统版本号不过可以等价看做时间)。每开始一个新的事务这个版本号就会增加并且将当前版本号作为事务版本号。在InnoDB默认repeatable-read隔离级别下它的工作方式是查询数据它会检索创建时间在事务版本号之前的数据也就是select * from table where create_version${version}所以新事务创建的数据是不可见的同时会检查数据的删除时间保证新的删除操作不可见。最终相当于select * from table where create_version${version} and (delete_version is null or delete_version${version})删除数据把当前数据的删除时间设置为当前事务版本号。插入数据创建一条新的数据创建时间就是当前事务版本号。更新数据删除和插入的综合删除原数据并且插入一条新数据。这是一个简单有效的MVCC模型……不过等一下这看起来和我们的设计不是差不多吗我书读得少不要骗我这好像也没有解决之前我们提出的问题啊但其实呢这段描述只是为了方便读者理解InnoDB实际的实现……不是这样的。这个书读得多也会被骗的……所以呢接下来我们只好稍微深入下细节了看看InnoDB其实是怎么实现并解决我们提出的问题的。首先它的隐藏列实际不是创建时间和删除时间而是当前事务id列和删除标志位。这两个列更像我们之前自己的设计了吧它们也能提供在简化模型里提到的那些功能。而且事务id还有更多的作用这个后面会提到。现在再看看我们先前提出的第一个问题失效的数据怎么办是不是可以定期去各表里扫描回收呢是的这是个很好的办法它确实要扫描回收。不过呢它稍微聪明一些。它并不到各个表里扫描而是去undo log里扫描。而在这之前它已经将老版本的数据移动到了undo log。这里可能需要为了解数据库较少的同学补充一点数据库undo log和redo log的知识undo log用于事务回滚里面放着修改前的数据需要回滚事务时可以根据它把修改前的数据替换回去redo log重做日志用于恢复可以认为它是数据的保护者。数据修改时通常不会马上写入磁盘而会记录到redo log并只修改内存里缓存的数据。当修改操作被写入redo log后就可以认为修改不会丢失了。而对数据的磁盘持久化可以放到后面更合适的时候。这主要是性能考虑磁盘数据的修改容易导致随机写入远比redo log的顺序写入慢。这里需要提醒下undo log同样需要redo log的保护对undo的修改同样会记录redo log。在需要恢复系统的时候会根据redo log恢复到当前状态(此时undo log的恢复同样依赖redo log)再根据undo log回滚没有提交的事务。好回到我们关于回收失效数据的话题吧。所以呢其实旧版本的数据是保存在undo log里面的。当一条数据被修改的时候旧版本的数据会被移动到undo log而新的修改会在原位置进行。而每条数据还有一个隐藏列称为回滚指针会指向被移动到undo log里的这条旧数据。当我们需要读取这条旧数据的时候就需要先找到现在最新版的数据然后根据这条数据的回滚指针去查找undo log里的旧数据。如果这条旧数据的版本还是太新不是我们想要的怎么办呢它也有指向更旧数据的回滚指针而更旧的数据也在之前就被移动到了undo log里我们继续回溯就好了。而失效数据的回收是通过Purge后台进程实现的。Purge进程定期扫描undo log按照从旧到新的顺序检查每条记录是否应被清理回收。在扫描前它会先取得当前活动事务列表借此判断扫描到的记录是不是失效数据并清理回收。这里再提出一个小问题我们什么情况下会需要读取undo log里面的旧数据呢其实常见读取数据有两种方式一致读也叫快照读。当我们根据where条件查找数据时或者单纯的select数据时在MVCC里采用的是一致读。此时是根据我们事务启动时的时间点读取该时间点的一个数据快照(snapshot)。如果新修改发生在我们的读事务启动之后或者新修改所属的事务还没有提交这些新修改对我们都应该是不可见的。所以我们不能读最新的数据而需要根据回滚指针读取旧数据。当前读在真正需要修改数据时肯定不能按照快照获取的数据进行修改否则会丢失修改。这时就需要读取该数据现在的最新值并且在最新值基础上进行修改这就是当前读。如果此时最新值所属的事务还没有提交就必须等待其回滚或提交。我们之所以需要读取undo log里面的旧数据就是因为很多时候我们都在使用一致读。一致读不需要对数据加锁有很好的性能。接下来我们再看看先前提出的第二个问题在MVCC下的并发修改怎么实现呢还是靠加锁。1. 对该数据加写锁(X)2. 记录redo log3. 复制修改前的旧值到undo log4. 在原来数据的位置修改数据并修改回滚指针指向undo log中的旧值。如果是修改主键的话因为InnoDB在主键上有聚簇索引 修改步骤会有点差别。但修改主键本身不是好的设计所以我们不讨论。这里又有个小问题一致性读时InnoDB的事务隔离是怎么实现的呢对没有提交的事务虽然它的事务id比我们的事务id更小它的修改仍然应该对我们不可见。这时就需要前面提到的每条数据上的隐藏列——事务id列的帮助了。当我们的事务开启时会取得当前活动事务列表。根据这份列表就可以排除没有提交的修改数据。这时通过比对数据上记录的事务id和活动事务列表就能判断该数据是否可见1. 如果是当前事务自己的修改可见2. 如果大于当前事务id不可见(repeatable-read)3. 如果小于最小活动事务id可见4. 其他情况需要和活动事务列表做详细比对。好啦我们现在总算解决了我们前面提到的几个问题新的方案可以投入使用了(撒花)。看起来别人家孩子果然是比较强啊。其实除InnoDB外的其他数据库对MVCC的实现也有自己的一些特点但我们这里就不研究其差异了大家有兴趣的话可以自己去看看。另祝大家在阅读了这篇文章后都能在以后的设计中有更多的思路和灵感谢谢本文作者杨真(点融黑帮)入坑Java开发十余年喜欢软件设计和重构现于点融LoanBusiness团队从事后端开发。返回搜狐查看更多责任编辑