了解 Redis 的同窗都知道它是一个纯内存的数据库,凭借优秀的并发和易用性打下了互联网项的半壁江山。Redis 之因此高性能是由于它的纯内存访问特性,而这也成了它致命的弱点 —— 内存的成本过高。因此在绝大多数场合,它比较适合用来作缓存,长期不被访问的冷数据被淘汰掉,只有热的数据缓存在内存中,这样就不会浪费太多昂贵的内存空间。redis
可是 Redis 的诱惑太大了,用它来作持久存储使用起来太方便了。要是内存的价格低廉,真巴不得把全部的数据都堆到 Redis 中,可是技术的选择老是要考虑到现实世界的成本问题。那如何才能享受到 Redis 做为持久层易用性的同时还能够节省内存成本呢?算法
它是 Google 开源的 NOSQL 存储引擎库,是现代分布式存储领域的一枚原子弹。在它的基础之上,Facebook 开发出了另外一个 NOSQL 存储引擎库 RocksDB,沿用了 LevelDB 的先进技术架构的同时还解决了 LevelDB 的一些短板。你能够将 RocksDB 比喻成氢弹,它比 LevelDB 的威力更大一些。现代开源市场上有不少数据库都在使用 RocksDB 做为底层存储引擎,好比大名鼎鼎的 TiDB。数据库
可是为何我要讲 LevelDB 而不是 RocksDB 呢?其缘由在于 LevelDB 技术架构更加简单清晰易于理解。若是咱们先把 LevelDB 吃透了再去啃一啃 RocksDB 就会很是好懂了,RocksDB 也只是在 LevelDB 的基础上添砖加瓦进行了一系列优化而已。等到咱们攻破了 RocksDB 这颗氢弹,TiDB 核动力宇宙飞船已经在前方不远处等着咱们了。缓存
当咱们将 Redis 拿来作缓存用时,背后确定还有一个持久层数据库记录了全量的冷热数据。Redis 和持久层数据库之间的数据一致性是由应用程序本身来控制的。应用程序会优先去缓存中获取数据,当缓存中没有数据时,应用程序须要从持久层加载数据,而后再放进缓存中。当数据更新发生时,须要将缓存置为失效。bash
function getUser(String userId) User {
User user = redis.get(userId);
if user == null {
user = db.get(userId);
if user != null {
redis.set(userId, user);
}
}
return user;
}
function updateUser(String userId, User user) {
db.update(userId, user);
redis.expire(userId);
}
复制代码
有过这方面开发经验的朋友们就知道写这样的代码仍是挺繁琐的,全部的涉及到缓存的业务代码都须要加上这一部分逻辑。网络
严格来讲咱们还须要仔细考虑缓存一致性问题,好比在 updateUser 方法中,数据库正确执行了更新,可是缓存 redis 由于网络抖动等缘由置为失效没有成功,那么缓存中的数据就成了过时数据。若是你将设置缓存和更新持久存的前后顺序反过来,也仍是会有其它问题,这个读者能够自行思考一下。架构
在多进程高并发场合也会致使缓存不一致,好比一个进程对某个 userId 调用 getUser() 方法,由于缓存里没有,它须要从数据库里加载。结果刚刚加载出来,正准备要设置缓存,这时候发生了内存 fullgc 代码暂停了一会,而正在此时另外一个进程调用了 updateUser 方法更新了数据库,将缓存置为失效(其实缓存里原本就没有数据)。而后前面那个进程终于 fullgc 结束要开始设置缓存了,这时候进缓存的就是过时的数据。并发
LevelDB 将 Redis 缓存和持久层合二为一,一次性帮你搞定缓存和持久层。有了 LevelDB,你的代码能够简化成下面这样负载均衡
function getUser(String userId) User {
return leveldb.get(userId);
}
function updateUser(String userId, User user) {
leveldb.set(userId, user);
}
复制代码
并且你不再用小心缓存一致性问题了,LevelDB 的数据更新要么成功要么不成功,不存在中间薛定谔状态。LevelDB 的内部已经内置了内存缓存和持久层的磁盘文件,用户彻底不用操心内部是数据如何保持一致的。分布式
前面咱们说道它是一个 NOSQL 存储引擎,它和 Redis 不是一个概念。Redis 是一个完备的数据库,而 LevelDB 它只是一个引擎。若是将数据库比喻成一辆高级跑车,那么存储引擎就是它的发动机,是核心是心脏。有了这个发动机,咱们再给它包装上一系列的配件和装饰,就能够成为数据库。不过也不要小瞧了配件和装饰,作到极致那也是很是困难,将 LevelDB 包装成一个简单易用的数据库须要加上太多太多精致的配件。LevelDB 和 RocksDB 出来这么多年,可以在它的基础上作出很是一个完备的生产级数据库寥寥无几。
在使用 LevelDB 时,咱们还能够将它当作一个 Key/Value 内存数据库。它提供了基础的 Get/Set API,咱们在代码里能够经过这个 API 来读写数据。你还能够将它当作一个无限大小的高级 HashMap,咱们能够往里面塞入无限条 Key/Value 数据,只要磁盘能够装下。
正是由于它只能算做一个内存数据库,它里面装的数据没法跨进程跨机器共享。在分布式领域,LevelDB 要如何大显身手呢?
这就须要靠包装技术了,在 LevelDB 内存数据库的基础上包装一层网络 API。当不一样机器上不一样的进程要来访问它时,都统一走网络 API 接口。这样就造成了一个简易的数据库。若是在网络层咱们使用 Redis 协议来包装,那么使用 Redis 的客户端就能够读写这个数据库了。
若是要考虑数据库的高可用性,咱们在上面这个单机数据库的基础上再加上主从复制功能就能够变身成为一个主从结构的分布式 NOSQL 数据库。在主从数据库前面加一层转发代理(负载均衡器如 LVS、F5 等),就能够实现主从的实时切换。
若是你须要的数据容量特别大以致于单个机器的硬盘都容不下,这时候就须要数据分片机制将整个数据库的数据分散到多台机器上,每台机器只负责一部分数据的读写工做。数据分片的方案很是多,能够像 Codis 那样经过转发代理来分片,也能够像 Redis-Cluster 那样使用客户端转发机制来分片,还可使用 TiDB 的 Raft 分布式一致性算法来分组管理分片。最简单最易于理解的仍是要数 Codis 的转发代理分片。
当数据量继续增加须要新增节点时,就必须将老节点上的数据部分迁移到新节点上,管理数据的均衡和迁移的又是一个新的高级配件 —— 数据均衡器。
看到这里我相信读者从总体上理解了分布式数据库中 LevelDB 所处的地位。下一节咱们开始全面了解一下 LevelDB 的内存数据库特性。