瞭解 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 的內存數據庫特性。