最新消息:

leveldb学习记录

leveldb admin 12081浏览 0评论
  • 关于leveldb
  • 性能对比
  • 安装
  • 使用
  • 性能调整

关于leveldb

leveldb是google开发的一套用于持久化数据的高性能类库。其特性有:

  • key-value方式存取
  • key-value都是二进制数据流
  • 数据以key排序存储
  • 操作简单: Get,Put,Delete,同时支持原子操作.
  • 支持快照(snapshot),读不受写的影响.
  • 自动压缩
  • 高性能

需要注意的是,不同于redis或者mangodb,LevelDB并不是NoSQL.其不支持sql语句,也不支持索引. 此外,只有单进程能访问数据库.另外,leveldb并不是一种服务,用户需要自行实现server.

根据上面的描述,LevelDB还是有很多限制.但是关键在于相比于sqlite等其他数据库,其具有非常高的写性能的同时,也保持了不错的读性能(参考下面的性能对比表). 在一些需要持久化key-value数据的场景下具有应用价值.这也弥补了memcached不能持久化的劣势.

 

性能对比

环境:

      OS:  Mac OSX 10.9.3 Darwin Kernel Version 13.2.0
 Machine:  retina MacBook ME664
     CPU:  Intel Core i7 2.4 GHz
  Memory:  8 GB
 Storage:  APPLE SSD SD256E
 LevelDB:  version 1.15
    Keys:  16 bytes each
  Values:  100 bytes each (50 bytes after compression)
 Entries:  1000000
 RawSize:  110.6 MB (estimated)
FileSize:  62.9 MB (estimated)

  SQLite:  version 3.7.13
    Keys:  16 bytes each
  Values:  100 bytes each
 Entries:  1000000
 RawSize:  110.6 MB (estimated)

leveldb_performance

从表中可以看出,leveldb的写性能相对于sqlite要高的多.但读性能较低.至于原因,之后可以通过对源码分析就可以看出.

安装

前面已经说明了,leveldb为一套类库.所以相对来说,安装过程更为简单.

首先下载压缩包:

https://code.google.com/p/leveldb/downloads/list

或使用git:

git clone https://code.google.com/p/leveldb/

解压缩之后,执行make,即可生成静态及动态库.

summertekiMacBook-Pro:leveldb summer$ ls -l

total 4880

-rw-r–r–+ 1 summer staff 590552 Jun 5 11:13 libleveldb.a

lrwxr-xr-x 1 summer staff 21 Jun 5 11:13 libleveldb.dylib -> libleveldb.dylib.1.15

lrwxr-xr-x 1 summer staff 21 Jun 5 11:13 libleveldb.dylib.1 -> libleveldb.dylib.1.15

-rwxr-xr-x+ 1 summer staff 315656 Jun 5 11:13 libleveldb.dylib.1.15

…..

如果想要在系统全局都可以使用,执行:

sudo cp -r include/leveldb /usr/include
sudo cp libleveldb.dy* /usr/lib

linux的动态链接库的后缀会有所不同,请酌情修改.

That’s it.安装完成了.

使用:

第一个例子

先写代码test.cpp:

#include <assert.h>
#include <string.h>
#include <leveldb/db.h>
#include <iostream>

int main(){
    leveldb::DB* db;
    leveldb::Options options;
    options.create_if_missing = true;
    leveldb::Status status = leveldb::DB::Open(options,"/tmp/testdb", &db);
    assert(status.ok());

    //write key1,value1
    std::string key="key";
    std::string value = "value";


    status = db->Put(leveldb::WriteOptions(), key,value);
    assert(status.ok());

    status = db->Get(leveldb::ReadOptions(), key, &value);
    assert(status.ok());
    std::cout<<value<<std::endl;
    std::string key2 = "key2";

    //move the value under key to key2

    status = db->Put(leveldb::WriteOptions(),key2,value);
    assert(status.ok());
    status = db->Delete(leveldb::WriteOptions(), key);

    assert(status.ok());

    status = db->Get(leveldb::ReadOptions(),key2, &value);

    assert(status.ok());
    std::cout<<key2<<"==="<<value<<std::endl;

    status = db->Get(leveldb::ReadOptions(),key, &value);

    if(!status.ok()) std::cerr<<key<<"  "<<status.ToString()<<std::endl;
    else std::cout<<key<<"="<<value<<std::endl;

    delete db;
    return 0;
}

编译命令:

g++ -o test test.cpp -I/PATH/TO/LEVELDB/include -L/PATH/TO/libleveldb -lleveldb

如果前面将库拷贝到/usr/include及/usr/lib中,则可以省略后面的-I及-L

执行效果:

summertekiMacBook-Pro:leveldb summer$ ./test
value
key2=value
key  NotFound:

在上述代码中,我们执行了:

  1. 打开leveldb
  2. 对key写入了value
  3. 对key2写入了value
  4. 删除了key所对应的值
  5. 查看了key2对应的值
  6. 查看了key对应的值

可以看到输出结果和我们预料的相同.

各种操作

Status

大部分函数返回值为Status.可以通过s.ok()来查看是否正常,也可以通过s.toString()来查看对应错误.

关闭数据库

直接删除数据库对象即可将数据库关闭

... open the db as described above ...
... do something with db ...
delete db;

读写

leveldb提供了三个基本操作: Get,Put,Delete.

std::string value;
leveldb::Status s = db->Get(leveldb::ReadOptions(), key1, &value);
if (s.ok()) s = db->Put(leveldb::WriteOptions(), key2, value);
if (s.ok()) s = db->Delete(leveldb::WriteOptions(), key1);

原子更新(事务)

leveldb提供了原子更新的功能.通过使用WriteBatch,可以将多个操作绑定,使得多个操作在一个batch内完成.类似于关系型数据库中的事务.

#include "leveldb/write_batch.h"
...
std::string value;
leveldb::Status s = db->Get(leveldb::ReadOptions(), key1, &value);
if (s.ok()) {
  leveldb::WriteBatch batch;
  batch.Delete(key1);
  batch.Put(key2, value);
  s = db->Write(leveldb::WriteOptions(), &batch);
}

同步写

默认情况下,leveldb的写是异步写(asynchronous).即其调用了write交给内核之后就直接返回.由于磁盘速度很慢,内核会对write做一定的缓冲,在合适的时候存到磁盘中.

通常来说这种方式执行的效果很好.不过如果在数据存到磁盘前,主机掉电了,那数据就丢失了.所以如果对数据很敏感,可以设置sync标志,强制操作系统写入磁盘中.(在write返回前,调用fsync或者fdatasyncmsync)

leveldb::WriteOptions write_options;
write_options.sync = true;
db->Put(write_options, ...);

由于磁盘的限制,同步是一个很慢的过程(参考上面的表格).考虑到效率,可以将多个写操作放在一个writebatch中,然后执行同步写.这样同步的cost就可以被分担到多个写操作中.

并发性

leveldb是单进程的,只有一个进程能访问数据库.leveldb使用了文件锁(fcntl)来保证不会有多个进程访问同一个数据库.从而导致数据库内容被破坏.另外,其也不允许在进程内多次打开相同数据库.所以下列的代码中,第二次打开会失败.另外,由于DB的复制构造函数operator=都为私有,所以也不能对DB进行复制.

leveldb::DB* db;
leveldb::Options options;
options.create_if_missing = true;
leveldb::Status status = leveldb::DB::Open(options,"/tmp/testdb", &db);
assert(status.ok());

leveldb::DB* adb;
status = leveldb::DB::Open(options, "/tmp/testdb", &adb);
assert(status.ok());

leveldb的实现提供了同步处理,因此,多个线程操作同一个DB对象时是不需要加锁来同步的.而对于IteratorWriteBatch,对非const函数操作时需要注意.

Iteration

leveldb提供了迭代器,使得可以遍历数据库中的所有key-value值.

leveldb::Iterator* it = db->NewIterator(leveldb::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;

也可以遍历范围内的值[‘aa’,’cc’):

for (it->Seek(start);
      it->Valid() && it->key().ToString() < limit;
      it->Next()) {
   ...
 }

当然也可以逆序遍历

for (it->SeekToLast(); it->Valid(); it->Prev()) {
  ...
}

Slice

使用迭代器时,其返回的it->value,it->key的类型都为Slice,甚至Seek使用的也是Slice.Slice的实现很简单:

class Slice {
 public:
  ...
  // Create a slice that refers to the contents of "s"
  Slice(const std::string& s) : data_(s.data()), size_(s.size()) { }

  // Create a slice that refers to s[0,strlen(s)-1]
  Slice(const char* s) : data_(s), size_(strlen(s)) { }
  …
 private:
  const char* data_;
  size_t size_;
};

仅仅就是一个指针及一个size_t.从构造函数上来看,使用起来也很容易.当然也包含了坑.其仅仅把string或者const char* 的指针复制到data_中,没有其他的控制,所以一不小心容易出现问题.

leveldb::Slice s1 = "hello";
std::string str("world");
std::string str = s1.ToString();

leveldb::Slice slice;
if (...) {
  std::string str = ...;
  slice = str;
}
Use(slice);//ops! Memory which data_ points is gone!

使用Slice是基于性能考虑,可以减少拷贝的发生.

比较函数

leveldb默认的比较是基于字母序,同时也支持自定义比较器,用于比较key的大小.只需要继承leveldb::Comparator即可.

class TwoPartComparator : public leveldb::Comparator {
 public:
  // Three-way comparison function:
  //   if a < b: negative result
  //   if a > b: positive result
  //   else: zero result
  int Compare(const leveldb::Slice& a, const leveldb::Slice& b) const {
      //do comparison,return -1, 0 or 1
  }

  // Ignore the following methods for now:
  const char* Name() const { return "TwoPartComparator"; }
  void FindShortestSeparator(std::string*, const leveldb::Slice&) const { }
  void FindShortSuccessor(std::string*) const { }
};

快照(Snapshot)

在leveldb中,可以获得某个时刻的快照,使得读数据不受到写数据的影响.

leveldb::ReadOptions options;
options.snapshot = db->GetSnapshot();
... apply some updates to db ...
leveldb::Iterator* iter = db->NewIterator(options);
... read using iter to view the state when the snapshot was created ...
delete iter;
db->ReleaseSnapshot(options.snapshot);

在使用完毕后,一定要记得调用ReleaseSnapshot,否则该数据就会一直留在磁盘中.

性能调整

leveldb提供了一些选项及接口,改善leveldb在不同环境下的性能.所有的选项都定义在include/leveldb/options.h中.

blocksize

leveldb将相邻key的数据保存在一个block中,其默认大小为4096B.可以通过options.block_size进行修改.如果应用中的数据较大,则可以调大数值,数据较小则相反. 不过太大太小的都不大好.可以通过leveldb提供的benchmark来测试修改的结果.

压缩

leveldb默认采用也是google出品的snappy库进行压缩.据称有很高的性能.在数据被持久化之前,数据将会被压缩,而后放入磁盘.当然,压缩不是强制的,可以被关闭.

leveldb::Options options;
options.compression = leveldb::kNoCompression;
... leveldb::DB::Open(options, name, ...) ....

缓存

leveldb将数据存储在文件中,为了改善性能,其将未压缩的数据缓存在内存中.缓存的大小可以设置.

#include "leveldb/cache.h"

leveldb::Options options;
options.cache = leveldb::NewLRUCache(100 * 1048576);  // 100MB cache
leveldb::DB* db;
leveldb::DB::Open(options, name, &db);
... use the db ...
delete db
delete options.cache;

Environment

leveldb的实现中,将所有的文件操作接口及系统调用都封装在了leveldb::Env对象中.所以使用自己编写的evn是完全有可能的.

class SlowEnv : public leveldb::Env {
  .. implementation of the Env interface ...
};

SlowEnv env;
leveldb::Options options;
options.env = &env;
Status s = leveldb::DB::Open(options, ...);

具体实现可以参考util/env_posix.cc.

后记

我很早之前已经听过leveldb,当时还是想找好的c++代码来学习的时候得知了这个强大的类库.就名声而言,leveldb的名气远没有其兄弟-BigTable tablet来得大.不过这仍然掩盖不了其的闪光点:key-value支持,持久化,高性能的写以及并不差的读性能.

key-value的数据库现在已经有很多,常见的就有:

除此之外,各个公司也在开源项目的基础上分支了很多出来.之前在腾讯实习的时候,就听说了多款内部在使用的memcached的分布式版本.除此之外,还有部门针对业务开发的kv型数据库.

可以说,在大数据时代下,NoSQL渐渐的成为了主流.

其实,了解leveldb的目的还是为了能更好的跟上实习的节奏,毕竟马上就要二进宫了,估计会比第一次去忙一些.同时,也学习下真正的大牛们设计及编写的优秀代码,也算是个提高的过程.接下来,将会阅读源代码,分析leveldb的实现原理及其中精妙的实现.

最后祝自己实习顺利~(≧▽≦)/~

也祝各位工作顺利,早早完成工资N级跳,参考v2ex的帖子.

话说世界杯马上就开始了,中立球迷表示谁赢都可以,嘿嘿~不过历史在预兆着三喵军团有夺冠的可能?anyway,从数据上看,中国队在世界杯上绝对是顶尖强队!

转载请注明:爱开源 » leveldb学习记录

您必须 登录 才能发表评论!