淺談REDIS數據庫的鍵值設計

豐富的數據結構使得redis的設計很是的有趣。不像關係型數據庫那樣,DEV和DBA須要深度溝通,review每行sql語句,也不像memcached那樣,不須要DBA的參與。redis的DBA須要熟悉數據結構,並能瞭解使用場景。php

下面舉一些常見適合kv數據庫的例子來談談鍵值的設計,並與關係型數據庫作一個對比,發現關係型的不足之處。python

用戶登陸系統

記錄用戶登陸信息的一個系統, 咱們簡化業務後只留下一張表。mysql

關係型數據庫的設計

mysql> select * from login;
+---------+----------------+-------------+---------------------+
| user_id | name           | login_times | last_login_time     |
+---------+----------------+-------------+---------------------+
|       1 | ken thompson   |           5 | 2011-01-01 00:00:00 |
|       2 | dennis ritchie |           1 | 2011-02-01 00:00:00 |
|       3 | Joe Armstrong  |           2 | 2011-03-01 00:00:00 |
+---------+----------------+-------------+---------------------+

user_id表的主鍵,name表示用戶名,login_times表示該用戶的登陸次數,每次用戶登陸後,login_times會自增,而last_login_time更新爲當前時間。web

REDIS的設計

關係型數據轉化爲KV數據庫,個人方法以下:redis

key 表名:主鍵值:列名sql

value 列值數據庫

通常使用冒號作分割符,這是不成文的規矩。好比在php-admin for redis系統裏,就是默認以冒號分割,因而user:1 user:2等key會分紅一組。因而以上的關係數據轉化成kv數據後記錄以下:ruby

Set login:1:login_times 5
Set login:2:login_times 1
Set login:3:login_times 2

Set login:1:last_login_time 2011-1-1
Set login:2:last_login_time 2011-2-1
Set login:3:last_login_time 2011-3-1

set login:1:name 」ken thompson「
set login:2:name 「dennis ritchie」
set login:3:name 」Joe Armstrong「

這樣在已知主鍵的狀況下,經過get、set就能夠得到或者修改用戶的登陸次數和最後登陸時間和姓名。數據結構

通常用戶是沒法知道本身的id的,只知道本身的用戶名,因此還必須有一個從name到id的映射關係,這裏的設計與上面的有所不一樣。oracle

set "login:ken thompson:id"      1
set "login:dennis ritchie:id"    2
set "login: Joe Armstrong:id"    3

這樣每次用戶登陸的時候業務邏輯以下(python版),r是redis對象,name是已經獲知的用戶名。

#得到用戶的id
uid = r.get("login:%s:id" % name)
#自增用戶的登陸次數
ret = r.incr("login:%s:login_times" % uid)
#更新該用戶的最後登陸時間
ret = r.set("login:%s:last_login_time" % uid, datetime.datetime.now())

若是需求僅僅是已知id,更新或者獲取某個用戶的最後登陸時間,登陸次數,關係型和kv數據庫無啥區別。一個經過btree pk,一個經過hash,效果都很好。

假設有以下需求,查找最近登陸的N個用戶。開發人員看看,仍是比較簡單的,一個sql搞定。

select * from login order by last_login_time desc limit N

DBA瞭解需求後,考慮到之後表若是比較大,因此在last_login_time上建個索引。執行計劃從索引leafblock 的最右邊開始訪問N條記錄,再回表N次,效果很好。

過了兩天,又來一個需求,須要知道登陸次數最多的人是誰。一樣的關係型如何處理?DEV說簡單

select * from login order by login_times desc limit N

DBA一看,又要在login_time上創建一個索引。有沒有以爲有點問題呢,表上每一個字段上都有素引。

關係型數據庫的數據存儲的的不靈活是問題的源頭,數據僅有一種儲存方法,那就是按行排列的堆表。統一的數據結構意味着你必須使用索引來改變sql的訪問路徑來快速訪問某個列的,而訪問路徑的增長又意味着你必須使用統計信息來輔助,因而一大堆的問題就出現了。

沒有索引,沒有統計計劃,沒有執行計劃,這就是kv數據庫。

redis裏如何知足以上的需求呢? 對於求最新的N條數據的需求,鏈表的後進後出的特色很是適合。咱們在上面的登陸代碼以後添加一段代碼,維護一個登陸的鏈表,控制他的長度,使得裏面永遠保存的是最近的N個登陸用戶。

#把當前登陸人添加到鏈表裏
ret = r.lpush("login:last_login_times", uid)
#保持鏈表只有N位
ret = redis.ltrim("login:last_login_times", 0, N-1)

這樣須要得到最新登陸人的id,以下的代碼便可

last_login_list = r.lrange("login:last_login_times", 0, N-1)

另外,求登陸次數最多的人,對於排序,積分榜這類需求,sorted set很是的適合,咱們把用戶和登陸次數統一存儲在一個sorted set裏。

zadd login:login_times 5 1
zadd login:login_times 1 2
zadd login:login_times 2 3

這樣假如某個用戶登陸,額外維護一個sorted set,代碼如此

#對該用戶的登陸次數自增1
ret = r.zincrby("login:login_times", 1, uid)

那麼如何得到登陸次數最多的用戶呢,逆序排列取的排名第N的用戶便可

ret = r.zrevrange("login:login_times", 0, N-1)

能夠看出,DEV須要添加2行代碼,而DBA不須要考慮索引什麼的。

TAG系統

tag在互聯網應用裏尤爲多見,若是以傳統的關係型數據庫來設計有點不三不四。咱們以查找書的例子來看看redis在這方面的優點。

關係型數據庫的設計

兩張表,一張book的明細,一張tag表,表示每本的tag,一本書存在多個tag。

mysql> select * from book;
+------+-------------------------------+----------------+
| id   | name                          | author         |
+------+-------------------------------+----------------+
|    1 | The Ruby Programming Language | Mark Pilgrim   |
|    1 | Ruby on rail                  | David Flanagan |
|    1 | Programming Erlang            | Joe Armstrong  |
+------+-------------------------------+----------------+

mysql> select * from tag;
+---------+---------+
| tagname | book_id |
+---------+---------+
| ruby    |       1 |
| ruby    |       2 |
| web     |       2 |
| erlang  |       3 |
+---------+---------+

假若有如此需求,查找便是ruby又是web方面的書籍,若是以關係型數據庫會怎麼處理?
select b.name, b.author  from tag t1, tag t2, book b
where t1.tagname = 'web' and t2.tagname = 'ruby' and t1.book_id = t2.book_id and b.id = t1.book_id

tag表自關聯2次再與book關聯,這個sql仍是比較複雜的,若是要求即ruby,但不是web方面的書籍呢?

關係型數據其實並不太適合這些集合操做。

REDIS的設計

首先book的數據確定要存儲的,和上面同樣。

set book:1:name    」The Ruby Programming Language」
Set book:2:name     」Ruby on rail」
Set book:3:name     」Programming Erlang」

set book:1:author    」Mark Pilgrim」
Set book:2:author     」David Flanagan」
Set book:3:author     」Joe Armstrong」

tag表咱們使用集合來存儲數據,由於集合擅長求交集、並集

sadd tag:ruby 1
sadd tag:ruby 2
sadd tag:web 2
sadd tag:erlang 3

那麼,即屬於ruby又屬於web的書?

inter_list = redis.sinter("tag.web", "tag:ruby")

即屬於ruby,但不屬於web的書?

inter_list = redis.sdiff("tag.ruby", "tag:web")

屬於ruby和屬於web的書的合集?

inter_list = redis.sunion("tag.ruby", "tag:web")

簡單到不行阿。

從以上2個例子能夠看出在某些場景裏,關係型數據庫是不太適合的,你可能可以設計出知足需求的系統,但老是感受的怪怪的,有種生搬硬套的感受。

尤爲登陸系統這個例子,頻繁的爲業務創建索引。放在一個複雜的系統裏,ddl(建立索引)有可能改變執行計劃。致使其它的sql採用不一樣的執行計劃,業務複雜的老系統,這個問題是很難預估的,sql千奇百怪。要求DBA對這個系統裏全部的sql都瞭解,這點太難了。這個問題在oracle裏尤爲嚴重,每一個DBA估計都碰到過。對於MySQL這類系統,ddl又不方便(雖然如今有online ddl的方法)。碰到大表,DBA凌晨爬起來在業務低峯期操做,這事我沒少幹過。而這種需求放到redis裏就很好處理,DBA僅僅對容量進行預估便可。

將來的OLTP系統應該是kv和關係型的緊密結合。

相關文章
相關標籤/搜索