1 参考资料
参见:https://github.com/facebook/rocksdb/wiki/Basic-Operations
rocksdb库提供了一个持久的键值存储。键和值是任意字节数组。并根据用户指定的比较器函数在键值存储中对键进行排序。
2 数据库操作
2.1 打开数据库
要操作rocksdb数据库,必须先打开数据库。数据库的名称与文件系统目录相对应。数据库的所有内容都存储在此目录中。打开数据库的样例代码如下:
#include <cassert> #include “rocksdb/db.h” rocksdb::DB* db; rocksdb::Options options; options.create_if_missing = true; rocksdb::Status status = rocksdb::DB::Open(options, “/tmp/testdb”, &db); assert(status.ok()); … |
如果要在数据库已存在时抛出错误,请在rocksdb::DB::Open调用之前添加以下行:
options.error_if_exists = true; |
如果要将代码从leveldb移植到rocksdb,可以使用rocksdb::LevelDBOptions将leveldb::Options对象转换为rocksdb::Options对象,该对象具有与leveldb::Options相同的功能:
#include “rocksdb/utilities/leveldb_options.h” rocksdb::LevelDBOptions leveldb_options; leveldb_options.option1 = value1; leveldb_options.option2 = value2; … rocksdb::Options options = rocksdb::ConvertOptions(leveldb_options); |
2.1.1 RocksDB Options
用户可以选择始终在代码中显式设置选项字段,如上所示。或者,也可以通过字符串到字符串映射或选项字符串来设置它。请参见选项字符串和选项映射:https://github.com/facebook/rocksdb/wiki/Option-String-and-Option-Map。
某些选项可以在DB运行时动态更改。例如:
rocksdb::Status s; s = db->SetOptions({{“write_buffer_size”, “131072”}}); assert(s.ok()); s = db->SetDBOptions({{“max_background_flushes”, “2”}}); assert(s.ok()); |
RocksDB会自动将数据库中使用的选项保存在DB目录下的options xxxx文件中。用户可以通过从这些选项文件中提取选项来选择在DB重新启动后保留选项值。
2.1.2 Status
您可能已经注意到上面的rocksdb::Status类型。rocksdb中大多数可能遇到错误的函数都会返回这种类型的值。您可以检查这样的结果是否正常,还可以打印相关的错误消息:
rocksdb::Status s = …; if (!s.ok()) cerr << s.ToString() << endl; |
2.2 关闭数据库
处理完数据库后,有两种方法可以正常关闭数据库:
- 只需删除数据库对象。这将释放数据库打开时保存的所有资源。但是,如果在释放任何资源时遇到错误,例如关闭info_log时出错,则该文件将丢失;
- 调用DB::Close(),然后删除数据库对象。DB::Close()返回状态,可以检查状态以确定是否存在任何错误。不管出现什么错误,DB::Close()都将释放所有资源,并且是不可逆的。
样例:
delete db; |
或者:
Status s = db->Close(); delete db; |
3 数据操作
3.1 读操作
数据库提供Put、Delete、Get和MultiGet方法来修改/查询数据库。例如,下面的代码将存储在key1下的值移动到key2。
std::string value; rocksdb::Status s = db->Get(rocksdb::ReadOptions(), key1, &value); if (s.ok()) s = db->Put(rocksdb::WriteOptions(), key2, value); if (s.ok()) s = db->Delete(rocksdb::WriteOptions(), key1); |
现在,值大小必须小于4GB。RocksDB还允许Single Delete,这在某些特殊情况下很有用。
每一次Get结果,从源到值字符串至少一次memcpy。如果源在块缓存中,可以使用PinnableSlice来避免额外的拷贝。
PinnableSlice pinnable_val; rocksdb::Status s = db->Get(rocksdb::ReadOptions(), key1, &pinnable_val); |
一旦pinnable_val被销毁或对其调用::Reset,源将被释放。
从数据库中读取多个key时,可以使用MultiGet。MultiGet有两种变体:
- 以更高效的方式读取单个列族中的多个键,也就是说,它可以比在循环中调用Get更快;
- 读取多个列族中相互一致的键。
例子,
std::vector<Slice> keys; std::vector<PinnableSlice> values; std::vector<Status> statuses; for … { keys.emplace_back(key); } values.resize(keys.size()); statuses.resize(keys.size()); db->MultiGet(ReadOptions(), cf, keys.size(), keys.data(), values.data(), statuses.data()); |
为了避免内存分配的开销,上面的keys、values和statuses可以是std::array类型,也可以是提供连续存储的任何其他类型。
或者
std::vector<ColumnFamilyHandle*> column_families; std::vector<Slice> keys; std::vector<std::string> values; for … { keys.emplace_back(key); column_families.emplace_back(column_family); } values.resize(keys.size()); std::vector<Status> statuses = db->MultiGet(ReadOptions(), column_families, keys, values); |
3.2 写操作
3.2.1 原子更新
请注意,如果该进程在输入key2之后但在删除key1之前终止,则相同的值可能会存储在多个键下。通过使用WriteBatch类以原子方式应用一组更新,可以避免此类问题:
#include “rocksdb/write_batch.h” … std::string value; rocksdb::Status s = db->Get(rocksdb::ReadOptions(), key1, &value); if (s.ok()) { rocksdb::WriteBatch batch; batch.Delete(key1); batch.Put(key2, value); s = db->Write(rocksdb::WriteOptions(), &batch); } |
WriteBatch保存了对数据库所做的一系列编辑,批中的这些编辑是按顺序应用的。注意,我们在Put之前调用Delete,这样如果key1与key2相同,我们就不会完全错误地删除该值。
除了原子性的好处外,WriteBatch还可以通过将大量的个体变化放入同一批来加速批量更新。
3.2.2 非同步写操作
对于非同步写入,RocksDB只缓冲WAL写入OS缓冲区或内部缓冲区(设置options.manual_wal_flush = true时)。这通常比同步写入快得多。非同步写入的缺点是,机器崩溃可能会导致最后几个更新丢失。请注意,仅写入进程的崩溃(即,不重新启动)不会导致任何损失,因为即使同步设为false,更新也会在被认为完成之前从进程内存推送到操作系统中。
非同步写入通常可以安全地使用。例如,将大量数据加载到数据库时,可以通过在崩溃后重新启动批量加载来处理丢失的更新。在由单独线程调用DB::SyncWAL()的情况下,也可以使用混合方案。
我们还提供了一种完全禁用WAL的方法。如果你设置write_options.disableWAL为true,则写操作根本不会进入日志,并且在进程崩溃时可能会丢失。
RocksDB默认使用fdatasync()同步文件,在某些情况下可能比fsync()更快。如果要使用fsync(),可以将Options::use_fsync设置为true。您应该在像ext3这样的文件系统上设置为true,以防在重新启动后可能会丢失文件。
3.2.3 高级写操作
有Pipelined Write、Write Stalls,请参见https://github.com/facebook/rocksdb/wiki/Pipelined-Write、https://github.com/facebook/rocksdb/wiki/Write-Stalls。
3.3 并发操作
一次只能由一个进程打开数据库。rocksdb实现从操作系统获取锁以防止误用。在单个进程中,同一rocksdb::DB对象可以由多个并发线程安全地共享。也就是说,不同的线程可以在没有任何外部同步的情况下写入或获取迭代器,或者在同一个数据库上调用Get(rocksdb实现将自动执行所需的同步)。但是,其他对象(如Iterator和WriteBatch)可能需要外部同步。如果两个线程共享这样一个对象,那么它们必须使用自己的锁定协议来保护对该对象的访问。公共头文件中提供了更多详细信息。
3.4 Merge操作
Merge合并运算符为读-修改-写操作提供了有效的支持。
3.4.1 合并运算符
参见:https://github.com/facebook/rocksdb/wiki/Merge-Operator。
3.4.2 合并运算符实现
参见:https://github.com/facebook/rocksdb/wiki/Merge-Operator-Implementation。
3.4.3 获取合并操作数
参见:https://github.com/facebook/rocksdb/wiki/Merge-Operator#get-merge-operands。
3.5 Iteration迭代器
下面的示例演示如何打印数据库中的所有(键、值)对。
rocksdb::Iterator* it = db->NewIterator(rocksdb::ReadOptions()); for (it->SeekToFirst(); it->Valid(); it->Next()) { cout << it->key().ToString() << “: ” << it->value().ToString() << endl; } assert(it->status().ok()); // Check for any errors found during the scan delete it; |
以下变体显示了如何仅处理范围[start,limit]中的键:
for (it->Seek(start); it->Valid() && it->key().ToString() < limit; it->Next()) { … } assert(it->status().ok()); // Check for any errors found during the scan |
也可以按相反顺序处理条目。(注意:反向迭代可能比正向迭代慢一些。)
for (it->SeekToLast(); it->Valid(); it->Prev()) { … } assert(it->status().ok()); // Check for any errors found during the scan |
这是从一个特定键以相反顺序处理范围(限制,开始)中的条目的示例:
for (it->SeekForPrev(start); it->Valid() && it->key().ToString() > limit; it->Prev()) { … } assert(it->status().ok()); // Check for any errors found during the scan |
4 高级操作
4.1 Snapshots快照
快照为键值存储的整个状态提供一致的只读视图。ReadOptions::snapshot可以为非NULL,以指示读取操作应在DB state的特定版本上进行。
如果ReadOptions::snapshot为NULL,则读取操作将在当前状态的隐式快照上进行。
快照由DB::GetSnapshot()方法创建:
rocksdb::ReadOptions options; options.snapshot = db->GetSnapshot(); … apply some updates to db … rocksdb::Iterator* iter = db->NewIterator(options); … read using iter to view the state when the snapshot was created … delete iter; db->ReleaseSnapshot(options.snapshot); |
请注意,当不再需要快照时,应该使用DB::ReleaseSnapshot接口释放快照。这允许实现摆脱维护的状态,这些状态只是为了支持从快照开始的读取。
4.2 Slice
上面的it->key()和it->value()调用的返回值是rocksdb::Slice类型的实例。Slice是一个简单的结构,它包含一个长度和一个指向外部字节数组的指针。返回一个Slice比返回std::字符串更高效,因为我们不需要复制可能很大的键和值。此外,rocksdb方法不返回以null结尾的C样式字符串,因为rocksdb键和值允许包含“\0”字节。
C++字符串和空终止的C样式字符串可以很容易地转换成Slice:
rocksdb::Slice s1 = “hello”; std::string str(“world”); rocksdb::Slice s2 = str; |
Slice可以很容易地转换回C++字符串:
std::string str = s1.ToString(); assert(str == std::string(“hello”)); |
使用Slice时要小心,因为Slice在使用时,由调用者来确保Slice所指向的外部字节数组保持存活状态。例如,以下就有bug:
rocksdb::Slice slice; if (…) { std::string str = …; slice = str; } Use(slice); |
当if语句超出范围时,str将被销毁,slice的后备存储将消失。
4.3 事务
RocksDB现在支持多操作事务。参见:https://github.com/facebook/rocksdb/wiki/Transactions。
4.4 Comparators比较器
前面的示例使用key的默认排序函数,该函数按字典顺序对字节排序。但是,您可以在打开数据库时提供自定义比较器。例如,假设每个数据库键由两个数字组成,我们应该按第一个数字排序,按第二个数字断开连接。首先,定义rocksdb::Comparator的适当子类,该子类表示以下规则:
class TwoPartComparator : public rocksdb::Comparator { public: // Three-way comparison function: // if a < b: negative result // if a > b: positive result // else: zero result int Compare(const rocksdb::Slice& a, const rocksdb::Slice& b) const { int a1, a2, b1, b2; ParseKey(a, &a1, &a2); ParseKey(b, &b1, &b2); if (a1 < b1) return -1; if (a1 > b1) return +1; if (a2 < b2) return -1; if (a2 > b2) return +1; return 0; } // Ignore the following methods for now: const char* Name() const { return “TwoPartComparator”; } void FindShortestSeparator(std::string*, const rocksdb::Slice&) const { } void FindShortSuccessor(std::string*) const { } }; |
现在使用此自定义比较器创建数据库:
TwoPartComparator cmp; rocksdb::DB* db; rocksdb::Options options; options.create_if_missing = true; options.comparator = &cmp; rocksdb::Status status = rocksdb::DB::Open(options, “/tmp/testdb”, &db); … |
4.5 列族
列族提供了一种逻辑分区数据库的方法。用户可以跨多个列族提供多个键的原子写入,并从它们中读取一致的视图。参见:https://github.com/facebook/rocksdb/wiki/Column-Families。
4.6 批量加载
您可以创建和接收SST文件,将大量数据直接批量加载到DB中,对实时通信量的影响最小。参见:https://github.com/facebook/rocksdb/wiki/Creating-and-Ingesting-SST-files。
4.7 备份和检查点
备份允许用户在远程文件系统(想想HDFS或S3)中创建定期的增量备份,并从中恢复。参见:https://github.com/facebook/rocksdb/wiki/How-to-backup-RocksDB%3F。
检查点提供了在一个单独的目录中对正在运行的RocksDB数据库进行快照的能力。如果可能的话,文件是硬链接的,而不是复制的,所以这是一个相对轻量级的操作。
参见:https://github.com/facebook/rocksdb/wiki/Checkpoints。
4.8 IO
默认情况下,RocksDB的I/O通过操作系统的页面缓存。设置速率限制器可以限制RocksDB文件写入的速度,为读I/o腾出空间。
用户还可以选择绕过操作系统的页面缓存,使用直接I/O。
详见:https://github.com/facebook/rocksdb/wiki/IO。
4.9 MemTable and Table factories内存表和表工厂
默认情况下,我们将内存中的数据保存在skiplist memtable中,将磁盘上的数据保存在这里描述的表格式中:RocksDB table format。参见:https://github.com/facebook/rocksdb/wiki/Rocksdb-Table-Format。
由于RocksDB的目标之一是使系统的不同部分易于插拔,因此我们支持memtable和table格式的不同实现。您可以通过设置Options::memtable_factory来提供自己的memtable工厂,通过设置Options::table_factory来提供自己的表工厂。对于可用的memtable工厂,请参考rocksdb/memtablerep.h;对于表工厂,请参考rocksdb/table.h。这些功能都处于开发阶段,请注意任何可能破坏应用程序的API更改。
memtables的更多信息参见:https://github.com/facebook/rocksdb/wiki/RocksDB-Basics#memtables、https://github.com/facebook/rocksdb/wiki/MemTable。
5 性能相关
性能调优,参见:https://github.com/facebook/rocksdb/wiki/Setup-Options-and-Basic-Tuning。
5.1 块大小block size
rocksdb将相邻的key分组到同一个块中,这样的块是与持久存储之间的传输单元。默认块大小约为4096个未压缩字节。主要对数据库内容进行批量扫描的应用程序可能希望增加此大小。如果性能测量结果表明性能有所改善,那么执行大量小值点读取的应用程序可能希望切换到较小的块大小。使用小于1千字节或大于几兆字节的块没有什么好处。还请注意,压缩对大块更高效。要更改块大小参数,请使用Options::block_size。
5.2 写缓存
Options::write_buffer_size指定在转换为已排序的磁盘文件之前要在内存中建立的数据量。较大的值会提高性能,尤其是在批量加载期间。在内存中可以同时保留max_write_buffer_number个写入缓冲区,因此您可能希望调整此参数以控制内存使用。另外,更大的写缓冲区将导致下次打开数据库时的恢复时间更长。
相关的选项是Options::max_write_buffer_number,这是内存中建立的最大写入缓冲区数。默认值为2,这样当一个写入缓冲区被刷新到存储器时,新的写入可以继续到另一个写入缓冲区。刷新操作在线程池中执行。
Options::min_write_buffer_number_to_merge是写入到存储之前合并在一起的最小写入缓冲区数。如果设置为1,则所有写缓冲区都作为单个文件刷新到L0,这会增加读取放大,因为get请求必须检查所有这些文件。此外,如果每个单独的写入缓冲区中都有重复的记录,内存中的合并可能会导致向存储器写入较少的数据。默认值:1。
5.3 压缩
在写入持久存储器之前,每个块都会被单独压缩。默认情况下,压缩处于启用状态,因为默认的压缩方法非常快,并且对于不可压缩的数据自动禁用。在极少数情况下,应用程序可能希望完全禁用压缩,但只有在基准测试显示性能有所改善时才应该这样做:
rocksdb::Options options; options.compression = rocksdb::kNoCompression; … rocksdb::DB::Open(options, name, …) …. |
字典压缩也可用,参见:https://github.com/facebook/rocksdb/wiki/Dictionary-Compression。
5.4 Cache
数据库的内容存储在文件系统中的一组文件中,每个文件存储一系列压缩块。如果options.block_cache为非空,用于缓存常用的未压缩块内容。我们使用操作系统文件缓存来缓存压缩的原始数据。因此,文件缓存充当压缩数据的缓存。
#include “rocksdb/cache.h” rocksdb::BlockBasedTableOptions table_options; table_options.block_cache = rocksdb::NewLRUCache(100 * 1048576); // 100MB uncompressed cache rocksdb::Options options; options.table_factory.reset(rocksdb::NewBlockBasedTableFactory(table_options)); rocksdb::DB* db; rocksdb::DB::Open(options, name, &db); … use the db … delete db |
在执行大容量读取时,应用程序可能希望禁用缓存,以便大容量读取处理的数据不会最终替换大部分缓存内容。可以使用每个迭代器选项来实现这一点:
rocksdb::ReadOptions options; options.fill_cache = false; rocksdb::Iterator* it = db->NewIterator(options); for (it->SeekToFirst(); it->Valid(); it->Next()) { … } |
也可以通过设置options.no_block_cache为true来禁用block cache。
5.5 Key layout
请注意,磁盘传输和缓存的单位是块。相邻key(根据数据库排序顺序)通常放置在同一块中。因此,应用程序可以通过将一起访问的key放置在彼此附近并将不常使用的key放置在key空间的单独区域中来提高其性能。
例如,假设我们正在rocksdb之上实现一个简单的文件系统。我们可能希望存储的条目类型有:
filename -> permission-bits, length, list of file_block_ids file_block_id -> data |
我们可能希望在文件名key的前缀中加上一个字母(例如“/”)和file_block_id key加上不同的字母(例如“0”)进行前缀,这样只扫描元数据就不会迫使我们获取和缓存大量的文件内容。
5.6 Filter过滤器
由于rocksdb数据在磁盘上的组织方式,一个Get()调用可能涉及从磁盘的多次读取。可选的FilterPolicy机制可用于大幅减少磁盘读取次数。
rocksdb::Options options; rocksdb::BlockBasedTableOptions bbto; bbto.filter_policy.reset(rocksdb::NewBloomFilterPolicy( 10 /* bits_per_key */, false /* use_block_based_builder*/)); options.table_factory.reset(rocksdb::NewBlockBasedTableFactory(bbto)); rocksdb::DB* db; rocksdb::DB::Open(options, “/tmp/testdb”, &db); … use the database … delete db; delete options.filter_policy; |
前面的代码将基于Bloom过滤器的过滤策略与数据库相关联。基于bloomfilter的过滤依赖于每个键在内存中保留一定数量的数据位(在本例中,每个键10位,因为这是我们传递给NewBloomFilter的参数)。此筛选器将使Get()调用所需的不必要磁盘读取次数减少大约100倍。增加每个key的位将导致更大的减少,但代价是更多的内存使用。我们建议那些工作集不适合内存并且执行大量随机读取的应用程序设置一个过滤策略。
如果使用的是自定义比较器,则应确保所使用的筛选策略与比较器兼容。例如,考虑在比较键时忽略尾随空格的比较器。过滤器不能与这样的比较器一起使用。相反,应用程序应该提供一个自定义筛选策略,该策略也会忽略尾部空格。
例如:
class CustomFilterPolicy : public rocksdb::FilterPolicy { private: FilterPolicy* builtin_policy_; public: CustomFilterPolicy() : builtin_policy_(NewBloomFilter(10, false)) { } ~CustomFilterPolicy() { delete builtin_policy_; } const char* Name() const { return “IgnoreTrailingSpacesFilter”; } void CreateFilter(const Slice* keys, int n, std::string* dst) const { // Use builtin bloom filter code after removing trailing spaces std::vector<Slice> trimmed(n); for (int i = 0; i < n; i++) { trimmed[i] = RemoveTrailingSpaces(keys[i]); } return builtin_policy_->CreateFilter(&trimmed[i], n, dst); } bool KeyMayMatch(const Slice& key, const Slice& filter) const { // Use builtin bloom filter code after removing trailing spaces return builtin_policy_->KeyMayMatch(RemoveTrailingSpaces(key), filter); } }; |
高级应用程序可以提供一个过滤器策略,该策略不使用bloom过滤器,而是使用一些其他机制来汇总一组key。详见rocksdb/filter_policy.h。
5.7 校验码Checksums
rocksdb将校验和与它存储在文件系统中的所有数据相关联。对于这些校验和的验证力度有两个单独的控制:
- ReadOptions::verify_checksums强制对代表特定读取的文件系统读取的所有数据进行校验和验证。默认情况下,此选项处于启用状态。
- Options::paranoid_checks在打开数据库之前,可以设置为true,以使数据库实现在检测到内部损坏时立即引发错误。根据数据库的哪个部分已损坏,在打开数据库时或稍后由另一个数据库操作引发错误。默认情况下,检查处于启用状态。
也可以通过调用DB::VerifyChecksum()手动触发校验和验证。该API遍历所有列族的所有级别的所有SST文件,并对每个SST文件验证嵌入元数据和数据块中的校验和。目前,只支持BlockBasedTable格式。文件是串行验证的,因此API调用可能需要花费大量时间才能完成。此API可用于分布式系统中数据完整性的主动验证,例如,如果发现数据库已损坏,则可以创建新的副本。
如果数据库已损坏(可能在启用检查时无法打开),可以使用rocksdb::RepairDB函数恢复尽可能多的数据。
5.8 压实Compaction
RocksDB不断重写现有数据文件。这是为了清理过时的key版本,并保持数据结构的最佳读取。
有关压实的信息已移至压实。用户在操作RocksDB之前不必了解压实的内部情况。
5.9 近似大小Approximate Sizes
GetApproximateSizes方法可用于获取一个或多个键范围使用的文件系统空间的近似字节数。
rocksdb::Range ranges[2]; ranges[0] = rocksdb::Range(“a”, “c”); ranges[1] = rocksdb::Range(“x”, “z”); uint64_t sizes[2]; db->GetApproximateSizes(ranges, 2, sizes); |
前面的调用将size[0]设置为key范围[a..c]使用的文件系统空间的近似字节数,size[1]设置为key范围[x..z]使用的近似字节数。
6 其他操作
6.1 环境自定义
rocksdb实现发出的所有文件操作(和其他操作系统调用)都通过rocksdb::Env对象路由。成熟的客户可能希望提供自己的Env实现,以获得更好的控制。例如,应用程序可以在文件IO路径中引入人工延迟,以限制rocksdb对系统中其他活动的影响。
class SlowEnv : public rocksdb::Env { .. implementation of the Env interface … }; SlowEnv env; rocksdb::Options options; options.env = &env; Status s = rocksdb::DB::Open(options, …); |
6.2 移植跨平台
rocksdb可以通过提供rocksdb/port/port.h导出的类型/方法/函数的特定于平台的实现来移植到新平台。有关更多详细信息,请参阅rocksdb/port/port_example.h。
此外,新平台可能需要一个新的默认rocksdb::Env实现。有关示例,请参见rocksdb/util/env_posix.h。
6.3 可管理能力
为了能够有效地优化应用程序,如果您可以访问使用情况统计信息,那么它总是很有帮助的。您可以通过设置Options::table_properties_collectors 或者Options::statistics来收集这些统计信息。有关更多信息,请参阅rocksdb/table_properties.h和rocksdb/statistics.h。这些不应给应用程序增加大量开销,我们建议将它们导出到其他监视工具。参见统计数据。您还可以使用Perf Context和IO Stats Context分析单个请求。用户可以为某些内部事件的回调注册EventListener。
6.4 清除WAL文件
默认情况下,当旧的预写日志超出范围并且应用程序不再需要它们时,会自动删除它们。有一些选项允许用户归档日志,然后以TTL方式或基于大小限制的方式延迟删除它们。
选项有:Options::WAL_ttl_seconds和Options::WAL_size_limit_MB。以下是它们的使用方法:
- 如果两者都设置为0,日志将尽快删除,并且永远不会进入存档;
- 如果Options::WAL_ttl_seconds为0,Options::WAL_size_limit_MB不为0,则每隔10分钟检查一次WAL文件,如果总大小大于Options::WAL_size_limit_MB,则从最早的开始删除它们,直到满足size_limit。将删除所有空文件;
- 如果Options::WAL_ttl_seconds不是0并且Options::WAL_size_limit_MB是0,那么每隔WAL_ttl_seconds/2检查一次WAL文件,并且删除那些早于WAL_ttl_seconds的文件;
- 如果两者都不为0,则每10分钟检查一次WAL文件,并在ttl为第一个的情况下执行两次检查。