您好,登录后才能下订单哦!
这篇文章给大家介绍SQLite原子提交的原理是什么,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。
1.0 简介
“原子提交”是SQLite这种支持事务的数据库的一个重要特性。原子提交意味着某个事务中数据库的变化会完整完成或者根本不完成。原子提交意味着不同的写入分别写入到数据库的不同部分就似同时发生在同一个时间点一样。
实际上硬件会连续的写到海量存储器中,只是写一个扇区所用的时间非常少。所以,同时或瞬间写入到数据文件的不同部分成为可能。SQLite的原子提交逻辑会使得一个事务中的变化就象同时发生的一样。
事务的原子是SQLite的重要特性,即使事务由于操作系统出错或掉电发生中断也能保持其原子性。
本文描述了SQLite实现原子操作的技术。
2.0 硬件设定
在这往篇文章中,我们把海量存储特指定为“硬盘”,即使它可能是flash memory.
我们假定硬盘是以扇区为单位进行整块写入的。我们不能单独修改硬盘的小于扇区的部分。如果需要修改硬盘小于扇区的部分,你也必须整个读入此部分所在扇区,对此扇区进行修改,然后将整个扇区写回硬盘。
在传统的Spinning disk中,扇区是最小的传输单元---无论是读还是写。然而,对于flash memory,每次读的最小数目通常都远小于最小写操作数目。SQLite 只关心写操作的最小数目,因此在本文中,当我们说“扇区”的时候,就是指单次写入的最少字节总数。
SQLite 3.3.14以前的版本,我们假定任何情况下,一个扇区是512字节。这是一个编译时设定的值,而且从没针对更大数进行测试过。当磁盘驱动器内部使用的是以512字节为单位的扇区时,512字节的假定显得非常合理。然而,现在的磁盘都已经发展到4k每扇区了。同样, flash memory 的扇区大小通常都大于512字节。因此,从3.3.14版本开始,SQLite有一个函数去获取文件系统的扇区真实大小。在当前的实现中(3.5.0),这个函数仍然简单的返回512—因为在win32及unix环境下,没有标准方法去取得扇区的真实大小。但这个方法在人们需要针对他们应用进行调整的时候是非常有意义的。
SQLite并不假定扇区写操作是原子的。然而,我们假定扇区写操作是线性的。所谓“线性”是指,当开始扇区写操作时,硬件从前一个扇区的结束点开始,然后一字节一字节的写入,直到此扇区的结束点。这个写操作可能是从尾向头写,也可能是从头向尾写。如果在一个扇区写入操作时发生掉电故障,这个扇区可能会一部分已经修改完成,还有一部分还没来得及进行修改。SQLite的关键设定是这样的:如果一个扇区的任何部分发生修改,那么不是它开始的部分发了变化,就是它结束部分发生了变化。所以硬件从来都不会从一个扇区的中间部分开始写入。我们不知道这个假定是否总是真实的,但无论如何,看起来还是蛮合理的。
上段中,SQLite并没有假定扇区写操作是原子的。在SQLite3.5.0版本中,新增了一个VFS(虚拟文件系统)接口。SQLite通过VFS与实际的文件系统进行交互。SQLite已经为windows及unix编写了一个缺省的VFS实现。并且可以让用户在运行时实现一个自定义的VFS实现。VFS接口有一个方法叫:xDeviceCharacteristics.此方法读取实际的文件系统各种特性。xDeviceCharacteristics方法可以指明扇区写操作是原子的,如果确实指定扇区写是原子的,SQLite是不会放过这等好处的。但在windows及unix中,缺省xDeviceCharacteristics的实现并没有指明扇区写是原子的,所以这些优化通常会忽略掉了。
SQLite假定操作系统会对写进行缓冲,因此写入请求返回时,有可能数据还没有真实的写入到存储中。SQLite 同时还假定这种写操作会被操作系统记录。因此, SQLite需要在关键点做"flush" 或 "fsync" 函数调用。SQLite假定flush或fsync在数据没有真实的写入到硬盘之前是不会返回的。不幸的是,我们知道在一些windows及unix版本中,缺少flush或fsync的真正实现。这使得SQLite在写入一个提交发生掉电故障后数据文件得到损坏。然而,这不要紧,SQLite能够做一些测试或补救。SQLite假定操作系统会是广告中那样漂亮运行。如果这些都不是问题,那么剩下的只期望你家的电源不要间歇性的休息。
SQLite假定文件增长方式是指新分配的文件空间,刚分配的时候是随机内容,后来才被填入实际的数据。换而言之,文件先变大,然后再填充其内容。这是一悲观假定,因而SQLite不得不做一些额外的操作来防止因断电发生的破坏数据文件—发生在文件大小已经增大,而文件内容还没完全填入之间的掉电。VFS的xDeviceCharacteristics可以指明文件系统是否总是先写入数据然后才更变文件大小的。(这就是那个:SQLITE_IOCAP_SAFE_APPEND属性,如果你想查看代码的话)
当xDeviceCharacteristics方法指示了文件内容先写入然后才改变文件大小的话,SQLite会减少一些相当的数据保护及错误处理过程,这将大大减少一个提交磁盘IO操作。然而在当前的版本,windows及unix的VFS实现并没有这样假定。
SQLite假定文件删除从用户进程角度来讲是原子的。也就说当SQLite要求删除一个文件,也在这删除的过程中间,断电了,一旦电源恢复,只有下列二种情况之一分发生:文件仍然存在,所有内容都没有发生变化;或者文件已经被删除掉了。如果电源恢复之后,文件只发生了部分删除,或者部分内容发生了变化或清除,或者文件只是清空,那么数据库还有用才怪呢。
SQLite假定发现或修改由于宇宙射线,热噪声,量子波动,设备驱动bug等等其他可能所引发的错误,都由操作系统或硬件来完成。SQLite并不为此类问题增加任何数据冗余处理。SQLite假定在写入之后去读取所获得的数据,是与写入的数据完全一致的!
3.0 单个文件提交
我们着手观察SQLite在针对一个数据库文件时,为保证一个原子提交所采取的步骤。关于在多个数据库文件之间为防止电源故障损坏数据库及保证提交的原子性所采用的技术及具体的文件格式在下一节进行讨论。
3.1 初始状态
当一个数据库第一次打开时计算机的状态示意图如右图所示。图中最右边("Disk”标注)表示保存在存储设备中的内容。每个方框代表一个扇区。蓝色的块表示这个扇区保存了原始资料。图中中间区域是操作系统的磁盘缓冲区。在我们的案例开始的时候,这些缓存是还没有被使用—因此这些方框是空白的。图中左边区域显示SQLite用户进程的内存。因为这个数据库联接刚刚打开,所以还没有任何数据记录被读入,所以这些内存也是空的。
3.4 申请一个Reserved Lock
在修改一个数据库之前,SQLite首先得拥有一个针对数据库文件的“Reserved”锁。Reserved锁类似于共享锁,它们都允许其他数据库连接读取信息。单个Reserved
锁能够与其他进程的多个共享锁一起协作。然后一个数据库文件同时只能存在一个Reserved 。因此只能有一个进程在某一时刻尝试去写一个数据库文件。
Reserved 锁的存在是宣告一个进程将打算去更新数据库文件,但还没有开始。因为还没有开始修改,因此其他进程可以读取数据,但不应该去尝试修改该数据库。
3.5 生成一个回滚日志文件
在修改数据库文件之前,SQLite会生成一个单独的回滚日志文件,并在其中写进将被修改的页的原始数据。回滚日志文件意味它将包含了所有可以将数据库文件恢复到原始状态的数据。
回滚日志文件有一个小的头部(图中绿色标记部分)记录了数据库文件的原始大小。因此,如果一旦即使数据库文件变大,我们还是会知道它原始大小。数据库文件中被修改的页码及他们的内容都被写进了回滚日志文件中。
当一个新文件刚被创建,大部分的桌面操作系统(windows,linux,macOSX)实际并不会马上写入数据到硬盘。此文件还只是存在于操作系统磁盘缓存中。这个文件还不会立即写到存储设备中,一般都会有一些延迟,或者到操作系统相当空闲的时候。用户的对于文件生成感觉是要远远快(先)于其真实的发生磁盘I/O操作。右图中我们用图例说明了这一点,当新的回滚日志文件创建之后,它还只是出现在操作系统磁盘缓存之中,还没真实在写入到硬盘之上。
3.8 获得一个独享(Exclusive)锁
在修改数据库文件本身之前,我们必须取得一个针对此数据库文件的独享锁。取得此锁的过程是分二步走的。首先SQLite取得一个“临界”(Reserved)锁,然后将此锁提升成一个独享锁。
一个临界锁允许其他所有已经取得共享锁的进程从数据库文件中继续读取数据。但是它会阻止新的共享锁的生成。也就说,临界锁将会防止因大量连续的读操作而无法获得写入的机会。这些读取者可能有一打,也可能上百,甚至于上千。任何一个读取者在开始读取之前都要申请一个共享锁,然后开始读取它需要的数据,然后释放共享锁。然而存在这样一种可能:如果有太多的进程来读取同一个数据文件,在老的进程释放它的共享锁之前总是会有新的进程申请共享锁,因此不会存在某一时刻这个数据库文件上没有共享锁的存在,也因此写入者不会拥有取得一个独享锁的机会。临界锁的概念可以使现有的读取者完成他们的读取,同时阻止新的读取者读取,最后所有的读取者都读完之后,这个临界锁就可以被提升为独享锁了。
3.10 刷新变更到存储
一个附加的flush操作是必要的,这样才可以保证针对此文件的变化真正的写入到永久存储器中。这也是一个重要的步骤,将可以保证数据在掉电之后也将是完整无损的。然而,因为写入到磁盘所固有的慢,这个步骤同上面3.7节将日志文件flush到磁盘中一样,占据了SQLIite事务提交操作的绝大部分时间。
3.11 删除回滚日志文件
当数据变更已经安全的写入到硬盘之后,回滚日志文件就没有必要再存在了,因此立即删除之。如果在删除之前又掉电了或者系统崩溃了,恢复进程(在后面将会提到)会将日志文件的内容写回到数据库文件中—即使这个数据库没有发生变化。如果删除之后系统崩溃或者又停电了,看起来好象所有变化都已经写入到磁盘。因此,SQLite判断数据库文件是否完成了变更是依赖于回滚日志文件是否存在。
删除一个文件实际上不是一个原子操作,但从用户进程的角度来看,它是一个原子操作。一个进程总是可以向操作系统询问某个文件存在否,而它得到的答案只有“YES”和“NO”二种。在一个事务提交的中间,系统崩溃或又停了,之后,SQLite会向操作系统咨询回滚日志文件存在与否,如果存在,则这个事务是没有完成,被中断了,需要对数据库文件进行回滚。如果日志文件不存在,意味着事务已经提交ok了。.
事务存在的可能性依赖于是否有回滚日志文件。删除一个文件对于一个用户进程来说是原子性的。因此,整个事务看起来也是一个原子操作。.
4.4 回滚没有完成的变更
一旦进程获得一个独享锁,它就被允许更新数据库文件。然后从日志文件中读取原始的内容,并写回到数据库文件中。是否还记得在这个被中止的事务的开始的时候,数据库文件原始大小已经被写进了日志文件的头部。SQLite使用这些信息来截断数据库文件,让文件恢复到原始大小—如果这个没有完成的事务使得数据库变大了。最后,数据库文件大小及内容肯定与这个被中断事务开始之前是一样的了。
6.0原子操作的一些实现细节
3.0节大致描述了SQLite中原子提交是如何工作的。但它略过了许多重要的细节。下面的这些部分将尝试补充说明这些地方。
6.1 总是记录整个扇区
当数据库文件的原始代码被写入到日志文件时(参见3.5节),SQLite总是写入完整的扇区,即使数据文件页大小是小于一个扇区。由于历史上的原因,SQLite的扇区大小原先是固定为512字节,此外由于最小的页大小是512字节,因此这从来都不是一个问题。自SQLite3.3.14版本以来,SQLite便有可能使用最小扇区大于512字节的海量存储设备。所以,自从3.3.14版本开始,只要一个扇区中的任何一页被写进到回滚日志文件中,那么同一扇区中的所有节都会写入到日志文件中去。
将扇区中的所有页都写入日志文件中去是很重要的,它将可以防止因为在写一个扇区时发生掉电故障而导致数据库损坏。假充页1,2,3,4都是保存扇区1中,页2被修改了。为了将这种变更写回到页2中,实际的硬件设备将也会同时重写页1,3及4的内容—这是因为硬件必须以扇区为单元作写操作。如果一个写操作正在进行的时候,由于电源的原因,发生了中断,这样,页1,3,4中会有1页或者多页数据是不完整,不正确的。因此为了防止这种损坏,数据库文件的同一扇区中的所有页都必须写入到日志文件中去。
6.2 写日志文件时垃圾的处理
当向一个日志文件追加数据时,SQLite总是悲观的假定文件会首先变大,变大的部分会填之一些无效的垃圾数据,在此之后正确的数据才会取代这些垃圾。换而言之,SQLite假定文件先改变大小,然后内容才会写进来。如果在文件大小增大之后,在内容还没有写完之前发生掉电故障,那么这些日志文件就会留下一些垃圾数据在其中。下次当电源恢复,另一个SQLite进程就会看到这些保存了垃圾数据的日志文件,并同时会把这些垃圾数据回滚到数据库文件中去,然后整个数据库就玩完了。
SQLite采用了两种预防措施。第一种,SQLite会在日志文件的头部记录下该日志文件中包含的页的数量。这个数量初始值是0。所以在尝试回滚一个不完整(或不正确)的回滚日志文件时,处理回滚的进程会看到该日志只包含0个页面,那么它就会不对数据库作任何改变。提交之后前,日志文件会被flush到硬盘中以确保所有的内容都同步到硬盘,同时没有任何垃圾内容留在其中,然后日志文件头部的页总数值才会置成真实有效的数据(原先数值是0)。日志文件的头部总是存放在区别于所有的页数据之外的独立扇区中,以此来保证它可被单独修改并且flush,即使发生掉电也不会危及数据页。请注意,日志文件会被flush两次:第一次写页数据,第二次是将页面数量写入到文件头部中。
前面的章节描述了当synchronouspragma设置成”full”发生的事情。
PRAGMAsynchronous=FULL;
缺省的synchronous设置是“full”,所以上面描述是通常会发生的情形。然而,如果synchronous设置成“normal”,那SQLite只会flush日志文件一次,就是在页面数量写入之后。这将意味着会有数据损坏的风险。因为有可能被修改的页面数量(非0)比所有的页数据更早一步写入到硬盘中。也数据的写入请求虽然会先被发起,但SQLite假定底层的文件系统可能会对写入请求重新排序,所以有可能页面数量会先写到磁盘中,即使是它的写请求是在最后。所以作为第二个预防手段,SQLite会为日志文件中的每一页数据使用一个32位的校验和,当回滚数据时(节4.4),这些值用来验证这些页是否有效。一旦发现有不正确的校验和时,那么就会放弃回滚。要注意的是,校验值并不确保页面数据百分百的正确,有极小的可能会出现即便数据错误校验和也是正确的。但使用校验和还是能使出错的可能性降到少之又少。
注意,如果synchronous设置成full时校验和不是必须的。只有当synchronous设置成normal时,我们才使用这些校验和。不过,这些校验和是没有坏处的,所以无论synchronous设是什么,它们都包括在日志文件里了。
6.3 提交前缓存溢出
节3.0描述的提交过程都假设所有的数据库变更在提交前都适合用户的内存大小。这是通常情况。但有时一个非常大的修改在事务提交前会超出用户空间的内存缓存大小。在这种情况下,事务完成之前,缓存必须先将数据先写入到数据库中。
在缓存溢出开始时,这个数据库联接的状态如3.6节提到的。原始的页数据已经被写入到回滚日志文件中了,修改的部分还保存在用户内存中。要处理这种缓存溢出,SQLite会执行3.7节到3.9节的内容。换言之,回滚日志被flush到硬盘,独享锁已经申请到,修改已经被写入到数据库了。但剩余的步骤会推迟到这个事务被真正提交。新的日志文件头会追加到回滚日志文件尾部(处于它自己单独的扇区中),独享锁仍然保留,但其他处理则回到3.6节.当这个事务提交时,或者另外的缓存溢出发生, 3.7节及3.9节会再次发生(3.8节在第二次或以后过程中被省略掉,因为独享锁已经拿到了)。
一次缓存溢会使数据库的临界锁提升为独享锁。这将减少并发。一次缓存溢出也会导致额外的硬盘flush(fsync)操作,这些操作比较慢,因此缓存溢出会严重降低性能。因此,应该尽可能的避免缓存溢出。
7.0 优化
性能分析显示,在大部分的操作系统和环境下面,SQLite主要耗时是在磁盘IO上面。如果我们能够减少磁盘IO数量就会显著的提高SQLite的性能。本节将描述SQLite在不影响提交原子性的前提下,为减少磁盘IO数量所采用的一些技术。
7.1 在事务间保存缓存
事务提交处理过程中,节3.12指出一旦共享锁被释放,用户空间所有的缓存的数据库内容镜像都必须得抛弃。这是因为如果没有一个共享锁,其他进程就可以随便修改数据库的内容,所以任何一块数据库数据在用户空间的缓存都可能会过期无效。因此,每一个新的事务会尝试去重新读取它以前读取过的数据。这并不像听起来这样糟糕,因为第一次读取过的数据还可能存在于操作系统的磁盘缓存中。所以这个读实际上只是一次数据从内核空间到用户空间的复制。但尽管这样,这还是需要占用cpu时间的。
自从SQLite3.3.14开始,新增了一个机制用来减少一些不必要的数据重复读取操作。最新的SQLite中,用户空间的页面缓存在用户锁释放之后仍然保留。之后,当要开始一个新事务,在取得一个共享锁之后,SQLite会尝试检查在此期间是否有进程对数据进行了修改。如果在锁释放这段时间,数据库发生过任何的变化,那么用户空间的缓存就会被释放。但通常情况下,数据文件是没有被修改过的,因此用户空间的缓存因而得到保留,一些不必要的读取操作从而得到了减免。
为了判断数据库文件是否被修改过,SQLite使用了一个计数器,存于数据库文件头部(处于字节24~27),每针对数据库做一次修改,就会对此值进行一回增长。SQLite会在释放一个锁之前记录一份这个值的。当下回取得锁之后,就会去与原先保存的值进行比较。如果值不一致,则必须清除这些缓存,反之缓存可以重新使用。
7.2 独享访问模式
SQLite从3.3.14版本之后增加一个“独享访问模式”概念。当处于独享访问模式时,SQLite会在一个事务完成之后仍然保留独享锁。这将阻止其他进程访问这个数据库;由于大部分的开发都只有一个进程访问数据库,所以大部分情况下这不是一个严重的问题。独享访问模式的好处可以在三个方面减少磁盘IO数量:
1) 不再需要在每个事务完成之后修改文件头部的变更计数器。这可以为回滚日志及数据库文件减少一次页写入。
2) 没有其他进程会修改数据库,所以不必在一个事务开始的时候去检查变更计数器或者清除掉用户空间的缓存。
3) 当一个事务完成之后,可以采用将日志文件头清零的方式,而不必去删除这个日志文件。这样就避免了修改日志文件的目录项,也不必释放日志文件对应的磁盘扇区。而且,下一个事务可以重写(overwrite)已有日志文件的内容,而不是在新的文件后追加新内容。在大多数的操作系统中,重写操作要远快于追加操作。
上述的第三点优化,将日志文件头清空而不是删除日志文件,不再依赖于一直持有一个独享锁。在理论上,我们可以在任何时刻做这项优化,并不是只有在独享访问模式时。This optimization can be set independently of exclusive lock modeusing the journal_mode
pragma asdescribed in section 7.6 below.
7.3 不必将空闲页写进日志
SQLite数据库的信息被删除之后,这些被删除的数据所使用的页会被加入到空页链表之中。后来的插入操作会尽量先使用空页链表中的页。
一些空白页包含紧要数据:特别是其他空百页的位置。但是大多数的空白页并不包含有用信息。这类页被称之为“叶子”页。我们可以随意修改这些叶子页的内容而不会影响数据库。
因为叶子页的内容是不重要的,SQLite避免保存这些叶子页的内容到回滚日志文件中去(3.5节)。如果一个叶子页的内容被修改了,那么在事务恢复过程中这些针对叶子页的修改并不会回滚。这不会对数据库产生伤害。同样的,新的空页链表的内容也从不会在节3.9中写回到到数据库,也不会在节3.3从数据库读入。当针对数据库文件的变化包含有空白页时,这种优化可以大量的减少磁盘io操作总数
7.4 单页更新及扇区原子写
从3.5.0开始,新的VFS接口包含了一个新的方法:xDeviceCharacteristics ,它能够读取实际的文件系统可能有的特性。xDeviceCharacteristics会报告是否文件系统能够支持扇区写原子操作。
回想前面,在一般情况下SQLite假定扇区写是线性的,但是非原子的。线性写从另一个扇区结束点开始一字节一字节进行修改,直到扇区的结束点。如果在写一个扇区时,线性写会将修改一个扇区的一部分,而另一部分是没有变动的。在一个扇区原子写的情况下,要么整个扇区被重写了,要么扇区没有发生变化。
我们相信大部分现代磁盘驱动器实现了原子写操作。当停电发生时,磁盘驱动器可以利用电容中的电能,同时(或者)利用盘片旋转的角动量来完成正在进行中的任何操作。然而,在系统写调用与磁盘电子器材之间,存在有太多的层次。因此在unix及win32上面的VFS实现比较安全的选择是,我们假定扇区写操作是非原子性的。On the otherhand, device manufactures with more control over their filesystems might wantto
consider enabling the atomic write property of xDeviceCharacteristics iftheir hardware really does do atomic writes.
当一个扇区写是原子性的,并且扇区大小与页大小是相同,并且一次数据库的变化只是某一个单独的页发生变化时,SQLite会跳过整个日志记录过程,直接简单地将被修改过的数据写回到数据库文件。数据库首页中的变更计数器将会被独立进行修改—因为不会对数据库产生任何影响—即使在计数器更新以前发生停电。.
7.5 FilesystemsWith Safe Append Semantics
SQLite3.5.0中介绍的另一个优化是利用实际磁盘的“安全追加”行为。回想上面,SQLite假定为一个文件追加数据时(特别是针对回滚日志文件),会先增大文件的大小,之后才会把数据内容写入。所以在文件的大小已经变化,而内容还没有写完的情况下发生掉电,那么文件新增部分将会有一些无效的垃圾数据。VFS的xDeviceCharacteristics可以用来指示文件系统是否实现了“安全追加”语义。这意味着在文件大小变大之前会先写入文件内容。这就防止当系统崩溃或掉电后,垃圾数据出现在回滚日志文件中
当文件系统有安全追加特性时,SQLite总是保存一个特别的值:-1来标明日志文件中页总数。页面数量为-1告诉任何尝试进行回滚操作程序页面数量需要从日志文件大小计算得来。同时,这-1值会从不进行修改。所以,在一个提交过程中,我们节省一个flsuh操作及日志文件首页的扇区写入操作。此外,当发生缓存溢出时,也不必要在日志文件后面增加一个新的日志头。我们能够简单的在一个现有的日志文件中添加一些新的页。
7.6持续的回滚日志
在许多系统中删除文件都是一个昂贵的操作。因此作为一个优化方案,SQLite可以通过配置避免3.11节中涉及到的删除操作。在事务提交时,通过将日志文件的文件头长度截为0或是用0重写文件头内容的方法来代替删除日志文件。将长度截为0的做法节省了必须要对文件的所在目录做的修改(因为文件依旧存在于这个目录中)。重写文件头的方案还有另外一个好处,不必更新文件(许多系统中的i节点)的长度,而且不需要处理新释放的磁盘扇区。更进一步讲,下一个事务的日志文件是通过重写已有内容而产生,而不是在文件末尾追加新内容,并且重写操作通常是要比追加操作更快的。
SQLite可以通过将日志模式设置为“PERSIST”使提交事务时使用用0重写日志文件头的方式来代替删除日志文件。例如:
PRAGMA journal_mode=PERSIST;
在很多系统中,使用持续的日志模式会带来显著的性能提升。当然,缺点就是事务提交很久以后,日志文件还会留在磁盘上,占用磁盘空间,导致目录杂乱。删除持续日志文件唯一安全的方法就是提交事务时将日志模式设置为DELETE:
PRAGMA journal_mode=DELETE;
BEGIN EXCLUSIVE;
COMMIT;
注意:因为日志文件可能依然在用(hot),如果使用其它途径删除持续日志文件会导致对应的数据库文件损坏。
从SQLite 3.6.4开始支持 TRUNCATE 日志模式:
PRAGMA journal_mode=TRUNCATE;
截断(truncate)日志模式中,事务提交时将日志文件长度置为0,而不是DELETE模式中的删除文件或是PERSIST模式中的清零文件头。 TRUNCATE模式也有PERSIST模式中不需要更新日志文件和数据库所在目录的好处。因此,截断一个文件通常比删除它要快。TRUNCATE还有一个好处就是它后面不跟系统调用(比如:fsync())来将更新同步回磁盘,当然如果做了会更安全。但是在很多现代的文件系统中,截断操作是原子的同步操作,并且我们认为在遇到断电情况时,截断操作也是安全的。如果你不确定截断操作在你的文件系统上的同步性和原子性,并且断电或宕机时的数据库安全对你很重要,那你应该考虑使用其他的日志模式。
在具有同步文件系统的嵌入式操作系统中,TRUNCATE会导致比PERSIST较慢的行为。提交操作的速度是相同的,但是TRUNCATE操作之后的事务会慢一些,因为重写已存在的内容比在文件尾追加新内容要快。TRUCATE之后新的日志文件总是使用追加操作,而PERSIST则是使用重写操作。
8.0 原子提交行为测试
SQLite的开发者对SQLite在面对电源故障及系统崩溃时所拥有健壮性具有足够的自信。因为自动化的测试过程做了大量的面对模拟的电源故障的SQLite恢复能力测试。我们称之为“崩溃测试”。
SQLite的崩溃测试是使用一个修改过的VFS,它能够模拟种种发生掉电或系统崩溃时文件系统发生的损坏。崩溃测试用的VFS能够模拟未完成的扇区写操作,未完成的写操作造成的页面垃圾,还有无序写操作,一个测试场景中各种种各样的变化。崩溃测试不停地执行事务,让模拟的掉电或系统崩溃发生在不同的各种时刻,造成不同的数据损坏。在模拟的事件之后,任何一次测试重新打开数据库之后,会检测事务是否完成或者没有完成,数据库状态是否正常。
SQLite的这些崩溃测试发现恢复机制的大量细微的BUG(现在都已经修复了)。其中一些BUG是非常模糊的,如果只是单单观察、分析代码所不能发现的。通通过这试验,SQLite的开发者感觉很自信,因为其他的数据库没有采用类似的崩溃测试,很可能他们都包含一些没有被检测出的bug,在一次掉电或者系统崩溃之后会导致数据库损坏。
9.0 会导致完蛋的事情
SQLite的原子提交机制已经被证明是健壮的。但它也可能被一些不完整的操作系统实现所陷害。本节描述一些会在掉电或系统崩溃下会导致SQLite数据损坏的情形
9.1 缺乏文件锁实现
SQLite通过文件系统的锁来实现在同一时刻只有一个进程及一个数据库联接能够修改数据库。文件锁机制由VFS层实现,不同的操作系统具有不同的实现方式。SQLite依赖于这种实现的正确性。如果在某种情况下,二个或更多进程能够在同一时间写同一个数据库文件,这将会没有什么好果子吃的。
我们已经接收到报告说windows的网络文件系统及NFS的锁存在一些微妙的缺陷。我们不能验证这些报告。但是因为网络文件系统本身实现锁很困难,所以我们没有理由怀疑这些报告。首先,既然性能不足,建议你不要在网络文件系统中使用SQLite。但是如果你不得不使用一个网络文件来保存SQLite的数据文件,那们考虑采用其他的锁机制来防止本身的文件锁机制出错时发生多个进程同时写一个数据文件的现象。
苹果MacOSX预装的SQLite版本已经扩展拥有一种可供选择的锁策略可以工作在苹果支持的所有网络文件系统上。这些苹果使用的扩展在多个进程在同时访问数据库文件时工作得很好。不幸的是,这些锁机制并不互相排斥,如果一个进程使用AFP锁去访问文件,而另一个进程(或许是另一台机器)使用dot-file锁去访问这个文件,那么这二个进程可能发生冲突,因为AFP锁并不排斥dot-file锁,反之亦然。
9.2 不完整的磁盘刷新
SQLite 在unix使fsysnc,在win32下面使用FlushFileBuffers,用来将文件内容同步到磁盘中(节3.7及节3.10)。不幸的是,我们也收到报告,在许多平台上,这二者都没有象广告中宣称的那样工作。我们听说FlushFileBuffersc在一些windows版本中,可以通过修改注册表,能够完全禁止其工作。我们也被告之,Linux的一些早先版本,他们的一些文件系统中的fsync完全是一个空操作。即使是FlushFileBuffers及fsync被告之可以工作的系统中,IDE硬盘经常会撒谎说数据已经写入到盘片中,其实还只是存在状态可变的磁盘控制器缓存中。
在Mac你可设置下面项:
PRAGMA fullfsync=ON;
在Mac上设置fullfsync能够保证数据通过flush会真实的写入到盘片中。但fullfsync会导致磁盘控制进行重设。这并不是一般意义上的慢,它还会导致其他磁盘IO降速,所以此项配置并不推荐。
9.3 文件部分地删除
SQLite假设从用户进程角度来看是一个原子操作。当删除过程中发生掉电,当电源恢复之后,SQLite希望看到文件要么完整的存在,要么根本找不到了。如果操作系统不能做到这一点,那事务就可能不是原子性的了。
9.4 写入到文件中的垃圾
SQLite的数据文件是一种普通的磁盘文件,可以由普通用户进行读写。一些流氓进程可能会打开一个SQLite文件,并在其中写入一些混乱的数据。混乱的数据也可能由于操作系统的BUG而写入到一个SQLite的数据文件中。对于这些情况,SQLite无能为力。
9.5 删除掉或更名了“hot”日志文件
如果掉电或系统崩溃导致留下了一个”hot”日志文件在磁盘上。实际上,原来的数据文件再加上留下来的“hot“日志文件, 是SQLite下回打开时发生回滚使用的,这可以恢复SQLite数据的正常状态(节4.2)。SQLite会在数据库所在同一目录下用打开的文件名来寻找可能存在的”hot”日志文件。如果数据文件或者日志文件被移动或者改名,或者删除掉了,那么这些日志文件将不会被回滚,数据库也就可能损坏,无法使用了。
我们常怀疑SQLite发生的恢复失败的例子是这样的:停电了,之后电又恢复了。一个好心的用户或者系统管理管理员开始查看磁盘损坏。他们看到名为"important.data"数据库文件,或许类似的文件。但由于停电,这里也同样有一个日志文件名为"important.data-journal".这个用户删除了这个“hot”日志文件,认为他是清理系统。那于这种情况,除了进行用户培训,没有其他办法。
如果有多个联接(硬或者符号联接)指向一个数据文件,这个日志文件会以被打开的联接文件名相关来创建的。如果系统崩溃之后,数据库以一个新的联接重新打开,这个“hot”日志文件就不会被找到,数据也不会发生回滚。
有时,电源问题会导致文件系统出现毛病,如最新修改的文件名被丢失了,并会转移至类似于"/lost+found"这样的目录中。当这种情况发生的时候,这个hot日志文件就不会被找到,同样恢复也不会发生。SQLite在同步一个日志文件时通过打开并同步日志文件所在目录来尝试阻止这类事件发生。然后,转移文件到"/lost+found"可能会由不相关的其他进程在相同的目录中产生与主数据库文件名相同的不相关文件。既然这都是SQLite所无法控制,所以SQLite没有什么好办法。如果你运行在一种易导致名称空间冲突的文件系统上,那么你最好把每一个SQLite的数据文件放在你私有的子目录中。
10.0 总结及未来的路
即使到了现在,还是有人发现了一些关于原子提交机制失败模式,开发者不得不为此做一些补丁。这样的事情发生得越来越少了,失败模型也变得越来越模糊了。但如果就认为SQLite的原子提交逻辑是没有任何bug,那是相当愚昧的。开发者承诺将尽可能快的修复被发现的bug。
开发者同时在考虑新的优化提交机制的办法。当前的linux,macOSX,win32的VFS实现使用这些系统之上的一些悲观设定。或许在与一些了解这些系统如何工作的专家交流之后,我们或许可能放松一些这些系统上的设定,使其跑得更快些。特别的,我们怀疑的大部分现代文件系统现在已经展现安全追加特性,或许他们都已经支持了扇区的原子操作。但是除非这些得到明确,SQLite仍将采用更安全、保守的方法,作最坏的打算。
关于SQLite原子提交的原理是什么就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。