豐富的數(shù)據(jù)結(jié)構(gòu)使得redis的設(shè)計(jì)非常的有趣。不像關(guān)系型數(shù)據(jù)庫(kù)那樣,DEV和DBA需要深度溝通,review每行sql語(yǔ)句,也不像memcached那樣,不需要DBA的參與。redis的DBA需要熟悉數(shù)據(jù)結(jié)構(gòu),并能了解使用場(chǎng)景。
下面舉一些常見(jiàn)適合kv數(shù)據(jù)庫(kù)的例子來(lái)談?wù)勬I值的設(shè)計(jì),并與關(guān)系型數(shù)據(jù)庫(kù)做一個(gè)對(duì)比,發(fā)現(xiàn)關(guān)系型的不足之處。
用戶登錄系統(tǒng)
記錄用戶登錄信息的一個(gè)系統(tǒng), 我們簡(jiǎn)化業(yè)務(wù)后只留下一張表。
關(guān)系型數(shù)據(jù)庫(kù)的設(shè)計(jì)
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表示該用戶的登錄次數(shù),每次用戶登錄后,login_times會(huì)自增,而last_login_time更新為當(dāng)前時(shí)間。
REDIS的設(shè)計(jì)
關(guān)系型數(shù)據(jù)轉(zhuǎn)化為KV數(shù)據(jù)庫(kù),我的方法如下:
key 表名:主鍵值:列名
value 列值
一般使用冒號(hào)做分割符,這是不成文的規(guī)矩。比如在php-admin for redis系統(tǒng)里,就是默認(rèn)以冒號(hào)分割,于是user:1 user:2等key會(huì)分成一組。于是以上的關(guān)系數(shù)據(jù)轉(zhuǎn)化成kv數(shù)據(jù)后記錄如下:
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“
這樣在已知主鍵的情況下,通過(guò)get、set就可以獲得或者修改用戶的登錄次數(shù)和最后登錄時(shí)間和姓名。
一般用戶是無(wú)法知道自己的id的,只知道自己的用戶名,所以還必須有一個(gè)從name到id的映射關(guān)系,這里的設(shè)計(jì)與上面的有所不同。
set "login:ken thompson:id" ? 1 set "login:dennis ritchie:id"??? 2 set "login:?Joe Armstrong:id"? 3
這樣每次用戶登錄的時(shí)候業(yè)務(wù)邏輯如下(python版),r是redis對(duì)象,name是已經(jīng)獲知的用戶名。
1
|
#獲得用戶的id
|
2
|
uid
=
r.get(
"login:%s:id"
%
name)
|
3
|
#自增用戶的登錄次數(shù)
|
4
|
ret
=
r.incr(
"login:%s:login_times"
%
uid)
|
5
|
#更新該用戶的最后登錄時(shí)間
|
6
|
ret
=
r.
set
(
"login:%s:last_login_time"
%
uid, datetime.datetime.now())
|
如果需求僅僅是已知id,更新或者獲取某個(gè)用戶的最后登錄時(shí)間,登錄次數(shù),關(guān)系型和kv數(shù)據(jù)庫(kù)無(wú)啥區(qū)別。一個(gè)通過(guò)btree pk,一個(gè)通過(guò)hash,效果都很好。
假設(shè)有如下需求,查找最近登錄的N個(gè)用戶。開(kāi)發(fā)人員看看,還是比較簡(jiǎn)單的,一個(gè)sql搞定。
1
|
select
*
from
login
order
by
last_login_time
desc
limit N
|
DBA了解需求后,考慮到以后表如果比較大,所以在last_login_time上建個(gè)索引。執(zhí)行計(jì)劃從索引leafblock 的最右邊開(kāi)始訪問(wèn)N條記錄,再回表N次,效果很好。
過(guò)了兩天,又來(lái)一個(gè)需求,需要知道登錄次數(shù)最多的人是誰(shuí)。同樣的關(guān)系型如何處理?DEV說(shuō)簡(jiǎn)單
1
|
select
*
from
login
order
by
login_times
desc
limit N
|
DBA一看,又要在login_time上建立一個(gè)索引。有沒(méi)有覺(jué)得有點(diǎn)問(wèn)題呢,表上每個(gè)字段上都有素引。
關(guān)系型數(shù)據(jù)庫(kù)的數(shù)據(jù)存儲(chǔ)的的不靈活是問(wèn)題的源頭,數(shù)據(jù)僅有一種儲(chǔ)存方法,那就是按行排列的堆表。統(tǒng)一的數(shù)據(jù)結(jié)構(gòu)意味著你必須使用索引來(lái)改變sql的訪問(wèn)路徑來(lái)快速訪問(wèn)某個(gè)列的,而訪問(wèn)路徑的增加又意味著你必須使用統(tǒng)計(jì)信息來(lái)輔助,于是一大堆的問(wèn)題就出現(xiàn)了。
沒(méi)有索引,沒(méi)有統(tǒng)計(jì)計(jì)劃,沒(méi)有執(zhí)行計(jì)劃,這就是kv數(shù)據(jù)庫(kù)。
redis里如何滿足以上的需求呢? 對(duì)于求最新的N條數(shù)據(jù)的需求,鏈表的后進(jìn)后出的特點(diǎn)非常適合。我們?cè)谏厦娴牡卿洿a之后添加一段代碼,維護(hù)一個(gè)登錄的鏈表,控制他的長(zhǎng)度,使得里面永遠(yuǎn)保存的是最近的N個(gè)登錄用戶。
1
|
#把當(dāng)前登錄人添加到鏈表里
|
2
|
ret
=
r.lpush(
"login:last_login_times"
, uid)
|
3
|
#保持鏈表只有N位
|
4
|
ret
=
redis.ltrim(
"login:last_login_times"
,
0
, N
-
1
)
|
這樣需要獲得最新登錄人的id,如下的代碼即可
1
|
last_login_list
=
r.lrange(
"login:last_login_times"
,
0
, N
-
1
)
|
另外,求登錄次數(shù)最多的人,對(duì)于排序,積分榜這類(lèi)需求,sorted set非常的適合,我們把用戶和登錄次數(shù)統(tǒng)一存儲(chǔ)在一個(gè)sorted set里。
zadd login:login_times 5 1 zadd login:login_times 1 2 zadd login:login_times 2 3
這樣假如某個(gè)用戶登錄,額外維護(hù)一個(gè)sorted set,代碼如此
1
|
#對(duì)該用戶的登錄次數(shù)自增1
|
2
|
ret
=
r.zincrby(
"login:login_times"
,
1
, uid)
|
那么如何獲得登錄次數(shù)最多的用戶呢,逆序排列取的排名第N的用戶即可
1
|
ret
=
r.zrevrange(
"login:login_times"
,
0
, N
-
1
)
|
可以看出,DEV需要添加2行代碼,而DBA不需要考慮索引什么的。
TAG系統(tǒng)
tag在互聯(lián)網(wǎng)應(yīng)用里尤其多見(jiàn),如果以傳統(tǒng)的關(guān)系型數(shù)據(jù)庫(kù)來(lái)設(shè)計(jì)有點(diǎn)不倫不類(lèi)。我們以查找書(shū)的例子來(lái)看看redis在這方面的優(yōu)勢(shì)。
關(guān)系型數(shù)據(jù)庫(kù)的設(shè)計(jì)
兩張表,一張book的明細(xì),一張tag表,表示每本的tag,一本書(shū)存在多個(gè)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方面的書(shū)籍,如果以關(guān)系型數(shù)據(jù)庫(kù)會(huì)怎么處理?
1
|
select
b.
name
, b.author?
from
tag t1, tag t2, book b
|
2
|
where
t1.tagname =
'web'
and
t2.tagname =
'ruby'
and
t1.book_id = t2.book_id
and
b.id = t1.book_id
|
tag表自關(guān)聯(lián)2次再與book關(guān)聯(lián),這個(gè)sql還是比較復(fù)雜的,如果要求即ruby,但不是web方面的書(shū)籍呢?
關(guān)系型數(shù)據(jù)其實(shí)并不太適合這些集合操作。
REDIS的設(shè)計(jì)
首先book的數(shù)據(jù)肯定要存儲(chǔ)的,和上面一樣。
set book:1:name?? ?”The Ruby Programming Language” Set book:2:name?? ? ”Ruby on rail” Set book:3:name?? ? ”P(pán)rogramming Erlang” set book:1:author?? ?”Mark Pilgrim” Set book:2:author?? ? ”David Flanagan” Set book:3:author?? ? ”Joe Armstrong”
tag表我們使用集合來(lái)存儲(chǔ)數(shù)據(jù),因?yàn)榧仙瞄L(zhǎng)求交集、并集
sadd tag:ruby 1 sadd tag:ruby 2 sadd tag:web 2 sadd tag:erlang 3
那么,即屬于ruby又屬于web的書(shū)?
inter_list = redis.sinter("tag.web", "tag:ruby")
即屬于ruby,但不屬于web的書(shū)?
inter_list = redis.sdiff("tag.ruby", "tag:web")
屬于ruby和屬于web的書(shū)的合集?
inter_list = redis.sunion("tag.ruby", "tag:web")
簡(jiǎn)單到不行阿。
從以上2個(gè)例子可以看出在某些場(chǎng)景里,關(guān)系型數(shù)據(jù)庫(kù)是不太適合的,你可能能夠設(shè)計(jì)出滿足需求的系統(tǒng),但總是感覺(jué)的怪怪的,有種生搬硬套的感覺(jué)。
尤其登錄系統(tǒng)這個(gè)例子,頻繁的為業(yè)務(wù)建立索引。放在一個(gè)復(fù)雜的系統(tǒng)里,ddl(創(chuàng)建索引)有可能改變執(zhí)行計(jì)劃。導(dǎo)致其它的sql采用不同的執(zhí)行計(jì) 劃,業(yè)務(wù)復(fù)雜的老系統(tǒng),這個(gè)問(wèn)題是很難預(yù)估的,sql千奇百怪。要求DBA對(duì)這個(gè)系統(tǒng)里所有的sql都了解,這點(diǎn)太難了。這個(gè)問(wèn)題在oracle里尤其嚴(yán) 重,每個(gè)DBA估計(jì)都碰到過(guò)。對(duì)于MySQL這類(lèi)系統(tǒng),ddl又不方便(雖然現(xiàn)在有online ddl的方法)。碰到大表,DBA凌晨爬起來(lái)在業(yè)務(wù)低峰期操作,這事我沒(méi)少干過(guò)。而這種需求放到redis里就很好處理,DBA僅僅對(duì)容量進(jìn)行預(yù)估即可。
未來(lái)的OLTP系統(tǒng)應(yīng)該是kv和關(guān)系型的緊密結(jié)合。
來(lái)源:www.hoterran.info