- 关于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的写性能相对于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:
在上述代码中,我们执行了:
- 打开leveldb
- 对key写入了value
- 对key2写入了value
- 删除了key所对应的值
- 查看了key2对应的值
- 查看了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
或者fdatasync
或msync
)
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对象时是不需要加锁来同步的.而对于Iterator
及WriteBatch
,对非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学习记录