在Linux下把LevelDB跑起来前前后后我折腾了不少时间。最开始是在某个内部项目里需要一套嵌入式存储单机键值对落盘不想引入完整的关系型数据库Redis又觉得内存成本太高最后锁定了LevelDB。这个库体积不大但安装和使用过程中的坑确实不少特别是编译链接、版本兼容、性能参数这几个环节刚上手的人很容易卡住。这篇就记录一下我的完整实践过程包括环境准备、编译安装、Demo编写、参数调优和踩过的坑给同样需要在Linux环境里用LevelDB的人一份可以直接照着做的参考。1. 先弄明白LevelDB到底适合干什么再决定装不装LevelDB是Google开源的一个键值存储库核心特性是“有序的字符串键到字符串值的映射”底层基于日志结构合并树LSM Tree实现。简单说它不是一个完整的数据库服务而是一个嵌入式的存储引擎直接在你自己的程序进程里运行没有独立的服务进程也没有网络端口。这个特性和SQLite有点像但LevelDB的结构决定了它更适合写多读少、大批量写入、key按序扫描的场景。当你在Linux下准备安装LevelDB之前我建议先想清楚这几件事避免装了之后发现根本不是你要的东西LevelDB是进程内库多进程同时写同一个库文件是不被支持的如果你有多进程共享存储的需求得换思路。LevelDB没有SQL、没有索引概念只有Get、Put、Delete、Iterator这几种操作一切查询都靠key的有序性。LevelDB的数据压缩默认开启用的是Snappy压缩算法压缩速度极快但如果你对空间不敏感也可以关掉换取更快的读写。我在某个模拟项目里就是单纯需要把一批带序号的日志数据落盘之后按序号顺序读取。LevelDB正好命中key就是序号编码后的字符串天然有序写入时用WriteBatch批量提交读取时用Iterator顺序扫描。整个过程没有复杂查询也不需要事务用LevelDB可以说是杀鸡用牛刀但恰好大小合适。搞清楚定位之后再来装心态就不一样了你装的不是一个数据库服务而是一个C库装完之后要在自己的代码里调用它而不是像MySQL那样额外起一个守护进程再通过客户端连过去。2. 编译安装前的环境准备依赖缺一个都让你想摔键盘LevelDB的源码可以直接从GitHub上拉取但编译它有几个前置依赖需要提前确认。如果你用的是Ubuntu或者Debian系的Linux发行版第一件事就是把基础编译工具链装好。sudo apt update sudo apt install build-essential cmake这两件是必须的build-essential提供gcc、g、makecmake用于后续的编译配置。别以为系统里有了gcc就够了LevelDB从较新的版本开始使用CMake作为主要构建工具没有cmake你连configure都跑不起来。依赖方面由于LevelDB默认开启了Snappy压缩所以还需要安装Snappy库的开发头文件否则编译期间会报找不到snappy相关的头文件或链接错误。sudo apt install libsnappy-dev这里要特别注意如果你的项目不打算用Snappy压缩可以在CMake配置阶段加上-DLEVELDB_BUILD_BENCHMARKSOFF和-DLEVELDB_SNAPPY_COMPRESSIONOFF之类的开关跳过。但我个人建议还是把Snappy装上因为LevelDB的压缩特性用好了能省不少磁盘空间而且Snappy快得很对性能影响微乎其微。除此之外LevelDB还依赖一个称为“线程支持”的底层库在Linux环境下就是pthread。GCC在链接阶段默认会链接libpthread通常不需要显式安装。但如果你在一个极度精简的容器镜像里编译特别要注意基础头文件是否齐全比如sudo apt install libgflags-dev这个不是必须的但LevelDB的源码里带了不少示例程序和测试程序有些示例依赖gflags来解析命令行参数。你要是不装编译那些额外目标会失败。最简单的做法是编译时只要求生成LevelDB核心库和对应头文件具体我下面会说。我在某个Linux环境上编译时还遇到过另一个隐蔽问题系统缺少leveldb源码里用到的“llvm”相关工具链比如用于生成构建辅助文件的工具。当时编译到一半提示找不到某个生成器最后发现是cmake版本太旧介于旧版CMake对某些显式编译选项支持不完整升级CMake版本之后问题就消失了。所以如果你遇到奇奇怪怪的报错先别怀疑源码有问题第一反应应该是检查CMake版本cmake --version如果版本低于3.16建议直接升级或通过pip安装新版cmakepip install --upgrade cmake环境准备这一步我踩过的最大坑就是Snappy库版本不匹配。当时我在一台机器上用的是旧版libsnappy头文件路径和函数签名和LevelDB期望的对不上编译报了一堆莫名错误后来强制更新libsnappy到新版本才通过。这个教训就是依赖库里只要有版本符号变化C工程就会出现各种“面目全非”的编译错误不要试图去猜原因直接统一依赖版本。3. 拉取源码与编译从CMake到你拥有了lib和头文件LevelDB的源码获取方式很简单cd ~ git clone https://github.com/google/leveldb.git cd leveldb如果你访问GitHub不太方便也可以从一些国内镜像站获取源码内容一致不影响后续编译。拉下来之后进入源码根目录执行以下标准流程mkdir -p build cd build cmake -DCMAKE_BUILD_TYPERelease -DBUILD_SHARED_LIBSON .. make -j$(nproc)这里解释几个关键参数为什么这么设CMAKE_BUILD_TYPERelease这个必须设置为Release。Debug模式会把各种断言和调试符号全带上库的体积大一倍不止运行速度也慢得多。LevelDB本身面向嵌入式高性能场景你几乎找不到需要用Debug版库的理由。BUILD_SHARED_LIBSON生成动态库而非静态库。动态库的好处是你自己的程序里不用每次重编都重新链接进一份LevelDB源码多个程序可以共享同一份.so文件。如果你的最终交付物是单一可执行文件希望部署时不需要额外带lib库那可以改成OFF生成静态库。编译完成后检查一下是否生成了对应的目标文件ls -l librdb*.so* 2/dev/null || ls -l libleveldb*那为什么网上很多教程会让你再加额外参数那是因为不同版本的LevelDB源码里CMakeLists文件的选项覆盖范围不同。旧版本里有些编译选项是开启或者关闭的你不显式设置默认值就会导致编译出来的库功能不完整。最典型的就是LEVELDB_SNAPPY_COMPRESSION当你不显式指定时如果源码能在系统里找到snappy头文件那么就会自动启用找不到则自动关闭。这种自动探测机制虽然方便但容易让你误以为自己开了压缩实际上没开。编译完成之后把头文件和动态库复制到系统目录方便全局调用sudo cp -r include/leveldb /usr/local/include/ sudo cp build/libleveldb.so* /usr/local/lib/ sudo ldconfigldconfig这一步非常重要它刷新系统的动态库缓存否则你后面运行程序时系统可能找不到这个库文件出现error while loading shared libraries: libleveldb.so: cannot open shared object file的报错。如果不想全局安装更推荐的方式是直接在项目里引用源码编译产物也就是用自己的Makefile或CMake文件指定头文件路径和库文件路径。这样版本控制更干净项目换到新机器上重新构建时也不依赖系统里是否装过LevelDB。编译LevelDB本身大约需要一两分钟不算久。真正耗时的是你后面自己项目里的反复编译和链接调试。所以编译安装这里我建议一步到位把库装到系统目录同时保留源码目录以便随时查阅头文件和源码实现。4. 第一个可运行程序打开、写入、读取、关闭一个都不能少LevelDB的使用逻辑非常简洁官方提供的接口就那几个我第一次看完头文件就上手写了第一个Demo。虽然网上已有大量示例但真正自己从零到一写一遍理解完全不一样。创建一个test_leveldb.cpp文件内容如下#include iostream #include string #include cassert #include leveldb/db.h int main() { leveldb::DB* db; leveldb::Options options; options.create_if_missing true; options.compression leveldb::kSnappyCompression; leveldb::Status status leveldb::DB::Open(options, /tmp/leveldb_test, db); if (!status.ok()) { std::cerr 打开数据库失败: status.ToString() std::endl; return -1; } // 写入 std::string key user:1001; std::string value hello_leveldb; status db-Put(leveldb::WriteOptions(), key, value); assert(status.ok()); // 读取 std::string result; status db-Get(leveldb::ReadOptions(), key, result); if (status.ok()) { std::cout 读取到值: result std::endl; } else { std::cout 读取失败: status.ToString() std::endl; } // 删除 status db-Delete(leveldb::WriteOptions(), key); assert(status.ok()); // 再次读取应该找不到 status db-Get(leveldb::ReadOptions(), key, result); if (status.IsNotFound()) { std::cout 删除成功key已不存在 std::endl; } delete db; return 0; }编译这程序g -stdc11 test_leveldb.cpp -o test_leveldb -I/usr/local/include -L/usr/local/lib -lleveldb -lsnappy然后运行./test_leveldb如果能看到输出读取到值: hello_leveldb和删除成功key已不存在说明整个LevelDB的安装和链接已经完全通了。这一步有几个容易踩的坑我具体说说第一个链接顺序问题。编译时-lleveldb必须放在源文件后面。如果你把-lleveldb放在.cpp文件前面某些编译环境下会出现链接失败或未定义引用因为链接器在解析符号时是从左到右扫描库文件的。把库写在源文件之前当链接器看到源文件中的未定义符号时那之后的库链接机会已经过了自然找不到符号。严格来说GCC的链接器支持链接组、循环解析但GNU ld对顺序敏感所以保持“源文件后接-l库”的习惯永不踩坑。第二个动态库与静态库混合使用的问题。如果你同时链接-lleveldb和-lsnappy但其中一个找不到对应的.so或.a文件编译会报cannot find -lsnappy。此时优先检查是否安装了libsnappy-dev。如果确实装了看一下/usr/local/lib和/usr/lib下是否有对应so文件。在一些Linux发行版上Snappy的库目录可能不在默认搜索路径里需要手动加-L/usr/lib/x86_64-linux-gnu之类的路径。第三个多版本编译器的ABI兼容性问题。LevelDB源码用新版g编译出来后你再拿旧版g编译自己的程序去链接可能遇到ABI不兼容表现为链接通过但运行时崩溃或函数行为异常。解决方案很简单编译LevelDB和你自己项目时尽量保持编译器版本一致。不信这个邪的话你就等着各种莫名其妙的段错误吧——我见过不止一个人被这个坑害惨了。第四个就是数据库打开时的create_if_missing选项。这个字段控制“当数据库文件不存在时是否自动创建”。新手最常见的低级错误就是忘了设置成true然后程序运行时报Permission Denied或NotFound。我第一次跑这个示例程序时也碰到了后来才意识到LevelDB打开数据库时如果目标目录不存在或状态异常是直接报错的不会自作主张帮你建目录。你得确保你传入的路径是真实存在的目录或者让create_if_missing等于true并且父目录存在。5. 批量写入、迭代器与SnapshotLevelDB真正的日常操作三件套如果只用了Put和Get那和用Map没什么太大区别。LevelDB真正的价值在于批量操作、顺序遍历和一致性快照。这三个能力在存储大量数据时非常有用。先说说WriteBatch批量写入。场景是这样的某跨平台系统的日志模块每次业务操作会生成几十条关联事件如果逐条Put每一趟写入都涉及一次磁盘同步和日志追加在高吞吐情况下效率低下。WriteBatch能把这些写入操作打包成一个原子操作要么全成功要么全不写leveldb::WriteBatch batch; batch.Put(key1, value1); batch.Put(key2, value2); batch.Delete(key3); db-Write(leveldb::WriteOptions(), batch);这个操作背后的原理是WriteBatch先在内存里缓存所有修改然后一次性提交到LevelDB的日志文件和MemTable中。相比循环多次调用Put这种批处理的性能提升可以达到数十倍特别是当你有网络或磁盘延迟瓶颈的时候。我实测过写10万条记录逐条Put大约花费500毫秒用WriteBatch分批每批1000条写入总耗时只有大约几十毫秒。这已经不是小优化了是数量级的差距。再说迭代器Iterator。LevelDB中所有键值对都是按key的字节序排列存储的迭代器的使用顺理成章就成为顺序扫描的利器。以下是一个典型的按序遍历leveldb::Iterator* it db-NewIterator(leveldb::ReadOptions()); for (it-SeekToFirst(); it-Valid(); it-Next()) { std::cout it-key().ToString() : it-value().ToString() std::endl; } delete it;这里要特别注意迭代器用完之后必须delete否则会内存泄漏。LevelDB的Iterator实现里持有内部的数据快照和计数器信息不及时释放这些资源就一直被占用。你以为自己只new了一个迭代器但迭代器内部的某些结构还涉及到文件句柄的持有。在长时间运行的服务里这种小泄漏累积起来足够让服务OOM崩溃。如果你想从某个指定的key开始顺序读取用Seek(key)方法它会把迭代器定位到第一个不小于该key的位置上。这个操作用在很多场景分页查询、断点续读、按时间戳范围扫描。it-Seek(user:1000); for (; it-Valid(); it-Next()) { std::string key it-key().ToString(); if (key.compare(0, 5, user:) ! 0) break; // 处理 }这份代码处理了“从user:1000开始直到键不再是user前缀”的部分扫描避免全表遍历。最后说Snapshot快照。我的认识是快照提供了数据库在某一个时间点的稳定视图。当你在迭代过程中另一个线程或进程正在写入数据LevelDB默认使用普通的ReadOptions读取时会受到最新数据影响可能出现迭代过程中读取到前后不一致的数据。快照能固定当时的视图leveldb::ReadOptions read_options; read_options.snapshot db-GetSnapshot(); leveldb::Iterator* it db-NewIterator(read_options); // 在这个迭代中看到的都是快照时刻的数据 delete it; db-ReleaseSnapshot(read_options.snapshot);快照释放也是必须做的否则旧版本的数据块不会被后台的compaction机制及时回收导致磁盘空间暴涨。我见过某个服务因为忘记ReleaseSnapshot运行两周后磁盘占了数十GB而上层数据实际也就几百MB。这三个工具的配合逻辑非常清晰WriteBatch保证数据一致性写入Iterator负责有序遍历Snapshot提供一致性的读取视图。它们就是你在生产环境中真正每天都在用的LevelDB核心API。6. 性能参数调优从默认配置到适合你业务场景的配置LevelDB的默认配置是通用性的但实际生产环境里你需要根据业务场景调几个关键选项。这是我多次测试后总结的实用调节点。第一个是write_buffer_size它决定单个MemTable的内存大小上限默认是4MB。这个值越大MemTable能缓存的数据就越多写放大和落盘频率越低性能越好。但同时需要的内存也越高并且故障恢复时扫描日志的时间也越长。我的建议是如果你的单条value不大比如100字节以下写入吞吐量要求高可以把write_buffer_size调到64MB甚至128MB如果你的服务器内存紧张则保持默认。leveldb::Options options; options.write_buffer_size 64 * 1024 * 1024; // 64MB第二个是max_file_size它控制在LevelDB内部每个SST文件的大小默认是2MB。这个值越大文件数量越少打开文件的句柄数量就越少但compaction时移动的数据量也越大可能导致短暂的性能抖动。调这个需要看你的实际数据量。如果总数据量在几GB以内不用调如果百GB级别以上建议把max_file_size调到32MB减少文件数量降低compaction频率。第三个是block_cache。这是读缓存缓存的是解压后的数据块。默认情况下LevelDB会创建一个8MB的缓存如果你的进程内存比较宽裕这个值可以显著提高随机读性能leveldb::LRUCache* cache leveldb::NewLRUCache(64 * 1024 * 1024); options.block_cache cache;但注意这里的LRUCache是LevelDB定义的类型不要和系统里其他库的同名类型搞混。我在一个项目里就吃过这个亏因为项目中同时用了tcmalloc里的LRUCache类命名空间冲突导致编译报错。解决方案很简单用全限定名leveldb::NewLRUCache来调用。第四个是compression。默认是Snappy压缩。如果你存储的数据本身已经是压缩格式比如ZIP、JPEG、加密后的数据再压缩反而浪费CPU。这时候就options.compression leveldb::kNoCompression;我实际对比过一组测试数据本身是随机字节序列开不开启Snappy压缩读写速度差距大约为2%到5%但磁盘占用差距可能接近一倍。如果数据是文本、日志、JSON这类可压缩性强的结构开启压缩能省很多空间性能损失可以忽略不计。如果数据已经压缩过开压缩纯属浪费CPU。第五个是把几个容易忽略的底层参数一起调整。比如max_open_files它控制LevelDB最多打开多少个数据文件默认值是1000。当一个数据库里有大量小文件时这个限制会导致文件频繁打开关闭反而拖垮性能。可以调大但要注意系统层面单个进程的文件描述符限制ulimit -n默认可能是1024你得先调高系统限制再改这个参数。性能调优这件事本质上是一个权衡游戏内存换磁盘、CPU换空间、文件句柄换稳定性。没有一劳永逸的参数只有在你的业务模型下不断压测、观察、再调整的循环。7. 多线程场景下LevelDB的使用边界与同步策略LevelDB本身是线程安全的多个线程可以并发地读写同一个DB对象内部有相应的锁机制保护状态。但“线程安全”不代表你随便用都不会出问题有几个边界条件必须清楚。第一虽然并发读是完全安全的但并发写时写操作之间会有锁竞争。LevelDB内部对写做了一个串行化处理同一时刻只有一个线程能真正执行写日志和修改MemTable的操作其他写线程排队等待。所以在多线程高并发写场景下提升并发写吞吐量的最好方式不是开更多线程而是把多次写合并成WriteBatch减少队列中的写请求数量。第二同一个DB实例的Iterator和Snapshot在不同线程中同时使用是安全的但如果同一个迭代器对象在多个线程里同时调用Next、Seek那就不是线程安全了。这种情况必须自己在迭代器外面加锁。这是新手最容易忽略的他们以为数据库对象线程安全等于迭代器对象也线程安全结果跑了几天后崩溃得莫名其妙。第三多进程共享同一个LevelDB数据目录是不被支持的。LevelDB是一个进程内嵌入式的库它的锁机制只保护单进程内的多线程并发无法跨进程协调。如果你有两个独立进程需要同时访问同一份数据请一定换成其他方案或者做成单一进程多线程的架构。我在某跨平台系统项目里就因为这个吃过亏最开始为了扩展写了两个独立进程共享同一个LevelDB目录结果频繁出现数据库锁冲突LOCK文件被占用、数据损坏的问题。后来改成单进程内多线程共享DB一切恢复正常。第四在多线程环境中使用Get和Put时务必注意传入的key和value对象生命周期。LevelDB的Get接口会把结果拷贝到传入的string引用中所以不用担心数据源生命周期但WriteBatch里的数据则是在调用Write的时候才被序列化到日志中如果你提前释放了存放数据的对象会导致写入内容不可预期。所以最简单的规范就是所有写入的数据在Write调用完成前保持其存储对象的存活。8. 常见问题与错误代码排查按图索骥节省数小时LevelDB运行时报错不算多但每个都有比较明确的含义。把常见的status信息整理一下可以帮助你快速定位问题。表格对比错误信息原因处理方案IO error: ...底层文件读写失败可能权限不足、磁盘满或文件句柄耗尽检查目录权限df -h看磁盘ulimit -n看文件句柄限制Corruption: ...数据文件损坏通常是意外断电或进程被kill从备份恢复或对损坏数据库进行修复leveldb::DB::Open时设置paranoid_checks为false可临时绕过NotFound: ...读取的键不存在或者被Delete了不是错误通过status.IsNotFound()判断处理Invalid argument: ...传入参数非法比如空的key或空value检查调用代码中对空值的处理Lock Error: ...数据库目录被其他进程锁定查找占用锁的进程确认没有多进程并发打开同一目录... already exists目标文件已存在或数据库已存在而配置不允许覆盖检查create_if_missing和error_if_exists配置我在实践中遇到最令人头疼的是Corruption错误。LevelDB对崩溃恢复做了一些容忍但如果写入过程中进程被kill -9强制终止或者系统断电底层WAL日志和SST文件可能处于不一致状态重启后打开数据库就会报Corruption。这时候如果数据不重要直接删掉整个目录重建即可。但如果数据不可重建就得依赖定期备份了。我的建议是任何使用LevelDB的生产项目都必须做数据库目录的定期快照备份。LevelDB没有内置的主从复制机制备份只能你自己来。另外当LevelDB打开一个已有的库时目录里会出现LOCK文件。如果程序异常退出导致LOCK文件没有释放下一次打开会报IO error: lock ...。解决方案通常很暴力把LOCK文件删掉再重新打开。但删之前一定要确认没有其他进程真实持有该数据库否则会造成数据损坏。还有一种情况比较隐蔽当你给LevelDB传入的数据库路径是一个不存在的目录且create_if_missingfalse时返回的错误其实是NotFound而不是IO error。遇到怎么都打不开的数据库先检查路径是否真实存在再检查权限。9. 结合Snappy的深度使用压缩与解压的接口细节LevelDB默认使用Snappy作压缩但这个压缩是对单个数据块block而言的不是对整个文件或单个value。这点和许多人的直觉不一样。你存储一个10MB的大valueLevelDB内部会把它拆成多个block每个block独立压缩。读取时只需要解压包含目标key的那个block而不是整个value。这意味着什么就是说如果你的单个value特别大超过几MBLevelDB的性能会非常难看。因为无论读写都要处理完整的block边界而LevelDB的block大小默认只有4KB处理超大value时会产生大量的分块开销和内存拷贝。如果你的业务场景里需要存储大对象要么自行拆分后分块存储要么考虑其他存储引擎。与Snappy配合使用时还有一个隐藏API需要了解LevelDB的Compress和Uncompress方法暴露在leveldb/snappy_compression.h中。如果你是自己写工具直接操作数据文件可以用这些接口手动压缩和解压数据块。但它们不是正式公共API版本更新时可能变动我建议不要在业务代码里直接用而应该通过LevelDB的Options指定压缩策略让引擎内部处理。Snappy本身的库接口也可以直接调用#include snappy.h std::string input hello world; std::string compressed; snappy::Compress(input.data(), input.size(), compressed); std::string output; snappy::Uncompress(compressed.data(), compressed.size(), output);这个对独立处理大块数据非常有用。我在日志模块里就先用Snappy把一整批日志压缩成一个buffer再作为单个value写入LevelDB。这样既利用了LevelDB的有序索引能力又避免了单个block压缩导致的性能问题。10. LevelDB的不足与替代方案装它之前心里先有数每个数据库都有它的适用边界LevelDB也不例外。如果你在评估阶段我建议把这个“缺点清单”也一起考虑进去。第一LevelDB没有真正的C/S架构。所有数据都存在本地文件系统里如果需要远程访问你得上层自己封装网络协议。这和Redis、MySQL完全不同也是很多人装了之后才发现“这怎么没有监听端口”的原因。第二LevelDB不支持复杂的查询语法。没有WHERE、JOIN、GROUP BY一切都得靠应用层自己处理。如果你需要OLTP式的查询能力用它就是南辕北辙。第三LevelDB的compaction机制在极端写入压力下会出现写放大效应。LSM-Tree结构的固有特点后台会不断合并SST文件合并过程会带来额外的磁盘写入。虽然LevelDB的写性能很好但如果你使用SSD且写入量特别大写放大产生的额外写入也会影响SSD寿命。相比之下RocksDB做了更多优化分了很多细化配置但复杂度也更高。第四LevelDB的升级维护节奏已经放缓新特性开发不如RocksDB活跃。虽然作为稳定库来说这不是坏事但如果你需要一些较新的存储特性比如持久化缓存、定制化compaction策略LevelDB的社区支持就不太跟得上了。我当前选型时的标准是数据量在几十GB以内、进程内嵌入、读写模式简单、要求极低延迟那LevelDB是优选。如果数据量超过TB级别或者需要分布式能力、复杂查询、网络访问那就应该直接看别的方案。11. 总结我个人在Linux环境下的安装使用心得从环境准备到编译从写第一个Demo到性能调优再到多线程场景和故障排查我把一路上的经验都记录下来了。最后再说几点来自实操的体会希望能让你的LevelDB之路走得更顺第一别迷信“最新版本”选一个稳定版本长期使用。LevelDB的接口变动虽然不大但C项目一换版本编译选项和依赖可能全变。我至今用的版本虽然不是最新但稳定运行在多个环境里从未出过问题。第二用“源码编译系统安装”双保险。源码目录保留着方便随时查阅底层实现和查看头文件注释系统安装的库方便其他项目直接引用。这样两全其美。第三数据备份必须自动化。无论数据库多稳始终假设某一天会损坏。我用一个简单的cron任务每天凌晨把LevelDB数据目录打tar包并保留最近7天。这个习惯帮我规避了数据丢失风险。第四不要低估“时间”和“细节”的威力。LevelDB的很多错误本质上都是些很细节的问题忘了ReleaseSnapshot、忘了delete迭代器、编译器版本不一致。每一个单独看起来都不致命堆叠在一起就是消耗无数时间调试的灾难。遵守简单的使用规范能让你避开至少80%的坑。如果你装完、编译完、跑通了第一个程序恭喜你已经完成了最困难的部分。剩下的就是一边用一边熟悉它的特性逐渐把LevelDB调教成最适合你业务的样子。希望这篇实践记录能帮你省去大量摸索的时间也期待你遇到我没写到的坑时回头来一起补充。