订了周六早上回杭州的火车票,在北京待了近一个星期,终于可以回家了。走新开的杭宁高铁,全程只要5个小时,不错,以后不再考虑飞机了。好吧,下面继续。
我们一直在说ORACLE是行级并发,所以我们会认为只要不同的事务更新不同的行,它们就不会Fail。但这不是事实。实际使用中,如果多个事务并发地更新同一页面中的不同行,你会发现系统经常会提示“Can't
serialize
access”的错误。造成这一错误的原因很多,比如并发的事务太多导致页面上没有空间来容纳新的ITL,但还有一类原因值得说一说,就是关于保留空间的。
ORACLE管这个叫FSC(Free Space
Credit),而我更习惯叫它保留空间(也许是以前论文看多了吧)。简单地说,如果你在页面上删除了一行,或者通过更新使一行长度变短,那些释放出来的空间就变成了你的保留空间,在ORACLE中,每个事务在特定页面上的保留空间,被纪录在页面内该事务的ITL中。同时,在逻辑上,系统还维护了页面的公共空闲空间(TF),当一个事务提交时(准确地说,是Cleanout时),它的FSC被追加到TF中。
诅咒ACID,它总是让一件简单的事情变得无比复杂。所以问题在于,在你的事务提交之前,你的FSC不能被其它事务使用,否则一旦那个事务Commit,而你要Rollabck,系统将崩溃。好吧,所以我们会遵循一个简单的协议:当你需要更多的空间时,你总是优先使用你的FSC,如果不够,你可以使用TF,但你绝不能使用其它活跃事务的FSC。也许在其它系统中,故事到了这里就结束了,但在页面级MVCC中,问题没那么简单。
如果有一个事务在你的SnapshotSCN之后开始,它释放了FSC(比如删除了一行)并且提交到TF中,然后你使用了TF,接下来问题来了,当你需要构造CR版本时,你的更新需要保留,而那个事务的删除动作则需要撤销,然后你突然发现:你的CR版本无法存放在一个页面中。所以在ORACLE中,你需要保证你的页面级CR版本是可以被构造出来的,一个简单的道理是:在当前页面中,你不能Cleanout那些在你SnapshotSCN之后开始的ITL。
但问题在于,在某些情况下,你甚至无法感知到这种危险的发生。比如在上面的例子中,同样是一个事务在你的SnapshotSCN之后开始,释放了FSC并且提交,然后另一个事务Cleanout了前一事务,并重用了其ITL。然后,当你来使用TF的时候,你如何确定这样的动作是安全的,这是不可能完成的任务。
所以ORACLE采用了一种最为保守的设计,它在页面上维护了该页面最近一次Cleanout的SCN(CSC),如果一个事务需要使用TF,但是它的SnapshotSCN
〈 CSC,那么他就会Fail。所以,如果你的应用需要在少量数据上频繁地对一非定长属性进行更新,你最好修改你的应用。
这样的设计看上去很糟糕,但我很惊奇在实际应用中错误并不会频繁出现,也许是更新事务大多比较短的原因吧。但是ORACLE确实提供了一种解决这类问题的方法,那就是行级SCN(Row-Level
SCN)。
没错,ORACLE确实支持行级SCN,你只需要在CREATE
TABLE的时候指定ROWDEPENDENCIES即可,ORACLE说这一选项主要用于提高数据复制的并行能力,好吧,我可以理解这一点,但我不太理解为什么在每行上加个SCN就可以解决前面的问题。好吧,也许它采用了不同的FSC回收策略,毕竟ORACLE并不推荐你这样使用,不是吗?
最后,再说一个细节,如果你只对协议本身有兴趣,你可以直接跳过。我想许多DBA曾经看过DSI或者其它什么的介绍ITL结构的书,你可能注意到,在ITL中,FSC和CommitSCN是共用空间的,这是怎么回事呢?好吧,事实是,SCN被分成了Base和Wrap两个域,而FSC用的是Wrap域;Fast
Cleanout实际上只更新了Base域,你需要和CSC的Wrap域一起来构造完整的CommitSCN,换句话说,如果你的CommitSCN的Wrap域与当前页面上CSC的Wrap域不同,你的Fast
Cleanout就不会成功。
这样的概率很小,因为SCN的增长规则是先增长Base,溢出(40亿次)后再把Wrap+1,同时Base归0。而你不用担心CSC未来的Wrap域会改变,因为在这之前,你的Cleanout在释放FSC后,必然会将CommitSCN设置为准确的值。
好吧,我承认这真是一个不错的设计,从空间对齐的角度来看,ORACLE把ITL减少了8个字节。但是拜托下次再干这种事的时候,多来点注释行吗?