既生 Redis 何生 LevelDB ?
了解 Redis 的同学都知道它是一个纯内存的数据库,凭借优秀的并发和易用性打下了互联网项的半壁江山。Redis 之所以高性能是因为它的纯内存访问特性,而这也成了它致命的弱点 —— 内存的成本太高。所以在绝大多数场合,它比较适合用来做缓存,长期不被访问的冷数据被淘汰掉,只有热的数据缓存在内存中,这样就不会浪费太多昂贵的内存空间。
但是 Redis 的诱惑太大了,用它来做持久存储使用起来太方便了。要是内存的价格低廉,真恨不得把所有的数据都堆到 Redis 中,但是技术的选择总是要考虑到现实世界的成本问题。那如何才能享受到 Redis 作为持久层易用性的同时还可以节省内存成本呢?
LevelDB 来了!
它是 Google 开源的 NOSQL 存储引擎库,是现代分布式存储领域的一枚原子弹。在它的基础之上,Facebook 开发出了另一个 NOSQL 存储引擎库 RocksDB,沿用了 LevelDB 的先进技术架构的同时还解决了 LevelDB 的一些短板。你可以将 RocksDB 比喻成氢弹,它比 LevelDB 的威力更大一些。现代开源市场上有很多数据库都在使用 RocksDB 作为底层存储引擎,比如大名鼎鼎的 TiDB。
但是为什么我要讲 LevelDB 而不是 RocksDB 呢?其原因在于 LevelDB 技术架构更加简单清晰易于理解。如果我们先把 LevelDB 吃透了再去啃一啃 RocksDB 就会非常好懂了,RocksDB 也只是在 LevelDB 的基础上添砖加瓦进行了一系列优化而已。等到我们攻破了 RocksDB 这颗氢弹,TiDB 核动力宇宙飞船已经在前方不远处等着我们了。
Redis 缓存有什么问题?
当我们将 Redis 拿来做缓存用时,背后肯定还有一个持久层数据库记录了全量的冷热数据。Redis 和持久层数据库之间的数据一致性是由应用程序自己来控制的。应用程序会优先去缓存中获取数据,当缓存中没有数据时,应用程序需要从持久层加载数据,然后再放进缓存中。当数据更新发生时,需要将缓存置为失效。
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 是如何解决的?
LevelDB 将 Redis 缓存和持久层合二为一,一次性帮你搞定缓存和持久层。有了 LevelDB,你的代码可以简化成下面这样
function getUser(String userId) User {
return leveldb.get(userId);
}
function updateUser(String userId, User user) {
leveldb.set(userId, user);
}
复制代码
而且你再也不用当心缓存一致性问题了,LevelDB 的数据更新要么成功要么不成功,不存在中间薛定谔状态。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 所处的地位。
既生 Redis 何生 LevelDB ?的更多相关文章
- 既生瑜何生亮 access_token VS refresh_token
中国有句老话, 既生瑜何生亮, 既然有我周瑜在世, 为什么老天还要一个诸葛亮啊? 同样的, 众所周知, 在 OAuth 2.0 授权协议中, 也有两个令牌 token , 分别是 access_tok ...
- 鸿蒙内核源码分析(特殊进程篇) | 龙生龙,凤生凤,老鼠生儿会打洞 | 百篇博客分析OpenHarmony源码 | v46.02
百篇博客系列篇.本篇为: v46.xx 鸿蒙内核源码分析(特殊进程篇) | 龙生龙凤生凤老鼠生儿会打洞 | 51.c.h .o 进程管理相关篇为: v02.xx 鸿蒙内核源码分析(进程管理篇) | 谁 ...
- 既生瑜何生亮?ASP.NET MVC VS ASP.NET Web API
Asp.net MVC 与 Asp.net Web API 区别 在我们开发一些web应用时,我们一样可以在MVC Framework 中使用JsonResult 来返回JSON数据,同样也可以处理一 ...
- 既生list何生tuple
python中list是可以修改的;若要创建一系列不可修改的元素时,就需要tuple. 用 tuple 的好处: 1.tuple 比 list 操作速度快.如果您定义了一个值的常量集,并且唯一要用它做 ...
- vuex 之既生‘mutation’何生‘action’
vuex 中,action 及 mutation 均为操作数据的作用而存在,既然二者均可改变数据,为什么要分成两个方法来处理呢,因为: Mutation 必须是同步函数 mutations: { so ...
- “既生 ExecutorService, 何生 CompletionService?”
前言 在 我会手动创建线程,为什么要使用线程池? 中详细的介绍了 ExecutorService,可以将整块任务拆分做简单的并行处理: 在 不会用Java Future,我怀疑你泡茶没我快 中又详细的 ...
- NoSQL--leveldb
什么是leveldb: leveldb它是一个 NOSQL 存储引擎,它和 Redis 不是一个概念.Redis 是一个完备的数据库,而 LevelDB 它只是一个引擎. LevelDB 还可以将它看 ...
- xcode 自动添加注释,生成文档
一.自动生成注释代码 添加一个快捷键,生成 注释代码 ThisService 下载连接:http://wafflesoftware.net/thisservice/ ...
- Redis的发布订阅及.NET客户端实现
序言 发布订阅在设计模式中也可以说是观察者模式,针对这个模式是处理对象间一对多的依赖关系的,当一个对象发生变化,其它依赖他的对象都要得到通知并更新. 然而它也有自己的缺点,就是当主题发生一系列的变化时 ...
随机推荐
- graphviz 的节点形状
graphviz 的节点可以定义不同的外形,比如下面的定义, digraph tt1{ a[shape=box]; c[shape=lpromoter]; d[shape=do ...
- fzu 2139 久违的月赛之二
Problem 2139 久违的月赛之二 Accept: 42 Submit: 106Time Limit: 1000 mSec Memory Limit : 32768 KB Probl ...
- JVM之---Java源码编译机制
Sun JDK中采用javac将Java源码编译为class文件,这个过程包含三个步骤: 1.分析和输入到符号表(Parse and Enter) Parse过程所做的工作有词法和语法分 ...
- javascript面向对象的常见写法与优缺点
我们通过表单验证的功能,来逐步演进面向对象的方式. 对于刚刚接触javascript的朋友来说,如果要写一个验证用户名,密码,邮箱的功能, 一般可能会这么写: //表单验证 var checkUs ...
- xib Nib IB 可视化编程详解
简单的说,Xib就是拖控件编程,也可以说是可视化编程. 相对于代码,使用IB和xib文件来组织UI,可以省下大量代码和时间,从而得到更快的开发速度. 如果你曾经受到过微软家Visual Basic或者 ...
- drupal7设置不含www的url跳转到含www的url
打开drupal的.htaccess文件 找到 If your site can be accessed both with and without the 'www.' prefix 将下面对应的三 ...
- 详解php 获取文件名basename()函数的用法
PHP 中basename()函数给出一个包含有指向一个文件的全路径的字符串,此函数返回基本的文件名,本篇文章收集了关于使用PHP basename()函数获取文件名的几篇文章,希望对大家理解使用PH ...
- FineReport中如何自定义登录界面
在登录平台时,不希望使用FR默认的内置登录界面,想通过自定义登录界面实现登录操作,内置登录界面如下图: 登录界面,获取到用户名和密码的值,发送到报表系统,报表服务带着这两个参数访问认证地址进行认证. ...
- Pwn with File结构体(二)
前言 本文由 本人 首发于 先知安全技术社区: https://xianzhi.aliyun.com/forum/user/5274 最新版的 libc 中会对 vtable 检查,所以之前的攻击方式 ...
- LeetCode题解Transpose Matrix
1.题目描述 2.题目描述 直接申请内存,转置即可. 3.代码 vector<vector<int>> transpose(vector<vector<int> ...