豐富的數據結構使得redis的設計非常的有趣。不像關系型數據庫那樣,DEV和DBA需要深度溝通,review每行sql語句,也不像memcached那樣,不需要DBA的參與。redis的DBA需要熟悉數據結構,并能了解使用場景。
下面舉一些常見適合kv數據庫的例子來談談鍵值的設計,并與關系型數據庫做一個對比,發現關系型的不足之處。
用戶登錄系統
記錄用戶登錄信息的一個系統, 我們簡化業務后只留下一張表。
關系型數據庫的設計
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更新為當前時間。
REDIS的設計
關系型數據轉化為KV數據庫,我的方法如下:
key 表名:主鍵值:列名
value 列值
一般使用冒號做分割符,這是不成文的規矩。比如在php-admin for redis系統里,就是默認以冒號分割,于是user:1 user:2等key會分成一組。于是以上的關系數據轉化成kv數據后記錄如下:
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的映射關系,這里的設計與上面的有所不同。
set "login:ken thompson:id" ? 1
set "login:dennis ritchie:id"??? 2
set "login:?Joe Armstrong:id"? 3
這樣每次用戶登錄的時候業務邏輯如下(python版),r是redis對象,name是已經獲知的用戶名。
2
|
uid
=
r.get(
"login:%s:id"
%
name)
|
4
|
ret
=
r.incr(
"login:%s:login_times"
%
uid)
|
6
|
ret
=
r.
set
(
"login:%s:last_login_time"
%
uid, datetime.datetime.now())
|
如果需求僅僅是已知id,更新或者獲取某個用戶的最后登錄時間,登錄次數,關系型和kv數據庫無啥區別。一個通過btree pk,一個通過hash,效果都很好。
假設有如下需求,查找最近登錄的N個用戶。開發人員看看,還是比較簡單的,一個sql搞定。
1
|
select
*
from
login
order
by
last_login_time
desc
limit N
|
DBA了解需求后,考慮到以后表如果比較大,所以在last_login_time上建個索引。執行計劃從索引leafblock 的最右邊開始訪問N條記錄,再回表N次,效果很好。
過了兩天,又來一個需求,需要知道登錄次數最多的人是誰。同樣的關系型如何處理?DEV說簡單
1
|
select
*
from
login
order
by
login_times
desc
limit N
|
DBA一看,又要在login_time上建立一個索引。有沒有覺得有點問題呢,表上每個字段上都有素引。
關系型數據庫的數據存儲的的不靈活是問題的源頭,數據僅有一種儲存方法,那就是按行排列的堆表。統一的數據結構意味著你必須使用索引來改變sql的訪問路徑來快速訪問某個列的,而訪問路徑的增加又意味著你必須使用統計信息來輔助,于是一大堆的問題就出現了。
沒有索引,沒有統計計劃,沒有執行計劃,這就是kv數據庫。
redis里如何滿足以上的需求呢? 對于求最新的N條數據的需求,鏈表的后進后出的特點非常適合。我們在上面的登錄代碼之后添加一段代碼,維護一個登錄的鏈表,控制他的長度,使得里面永遠保存的是最近的N個登錄用戶。
2
|
ret
=
r.lpush(
"login:last_login_times"
, uid)
|
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
)
|
另外,求登錄次數最多的人,對于排序,積分榜這類需求,sorted set非常的適合,我們把用戶和登錄次數統一存儲在一個sorted set里。
zadd login:login_times 5 1
zadd login:login_times 1 2
zadd login:login_times 2 3
這樣假如某個用戶登錄,額外維護一個sorted set,代碼如此
2
|
ret
=
r.zincrby(
"login:login_times"
,
1
, uid)
|
那么如何獲得登錄次數最多的用戶呢,逆序排列取的排名第N的用戶即可
1
|
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方面的書籍,如果以關系型數據庫會怎么處理?
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表自關聯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和關系型的緊密結合。
來源:www.hoterran.info