[toc]
## 不同數據類型的內存管理介紹
![03152154_EKe5.jpg-22kB][1]
[1]: http://static.zybuluo.com/a5635268/546w4jsygkvzirmw3fk6iixm/03152154_EKe5.jpg
如上圖所示,首先Redis內部使用一個redisObject對象來表示所有的key和value,redisObject最主要的信息如上圖所示:type 代表一個value對象具體是何種數據類型,encoding是不同數據類型在redis內部的存儲方式,比如:type=string代表value存儲的是一個普通字符串,那么對應的encoding可以是raw或者是int,如果是int則代表實際redis內部是按數值型類存儲和表示這個字符串的,當然前提是這個字符串本身可以用數值表示,比如:"123" "456"這樣的字符串。
我們可以發現Redis使用redisObject來表示所有的key/value數據是比較浪費內存的,當然這些內存管理成本的付出主要也是為了給Redis不同數據類型提供一個統一的管理接口。
## 不同的數據類型對應的使用場景
### string
- **防重復提交(分布式鎖)**
規定某個key值在某段時間內只能出現一次
```php
# 有一致性問題
function redisLock($syncKey,$expire=1000){
if (!$this->redis->setnx ( $syncKey, 1 )) {
return false;
}
$this->redis->pexpire($syncKey, $expire);
return true;
}
# 2.6版本以后
/**
* redis排重鎖
* @param $key
* @param $expires
* @param int $value
* @return mixed
*/
public function redisLock($key, $expires, $value = 1)
{
//在key不存在時,添加key并$expires秒過期
return $this->redis->set($key, $value, ['nx', 'ex' => $expires]);
}
```
- **原子計數器**
Redis的命令都是原子性的,你可以輕松地利用INCR,DECR命令來構建計數器系統。
- **普通緩存json數據**
### Hash
- **存儲、讀取、修改用戶屬性**
在 Memcached 中,我們經常將一些結構化的信息打包成 hashmap,在客戶端序列化后存儲為一個字符串的值(一般是 JSON 格式),比如用戶的昵稱、年齡、性別、積分等。這時候在需要修改其中某一項時,通常需要將字符串(JSON)取出來,然后進行反序列化,修改某一項的值,再序列化成字符串(JSON)存儲回去。簡單修改一個屬性就干這么多事情,消耗必定是很大的,也不適用于一些可能并發操作的場合(比如兩個并發的操作都需要修改積分)。而 Redis 的 Hash 結構可以使你像在數據庫中 Update一個屬性一樣只修改某一項屬性值。
### List
- **構建隊列系統**
使用`rpush/lpush`操作入隊列,使用`lpop 和 rpop`來出隊列。如果隊列空了,客戶端就會陷入 pop 的死循環,不停地 pop。通常我們使用 sleep 來解決這個問題,讓線程睡一會,睡個 1s 鐘就可以了。但是有個小問題,那就是睡眠會導致消息的延遲增大。如果只有 1 個消費者,那么這個延遲就是 1s。
用`blpop/brpop`替代前面的`lpop/rpop`,阻塞讀在隊列沒有數據的時候,會立即進入休眠狀態,一旦數據到來,則立刻醒過來。消息的延遲幾乎為零。但如果線程一直阻塞在哪里,Redis 的客戶端連接就成了閑置連接,閑置過久,服務器一般會主動斷開連接,減少閑置資源占用。這個時候`blpop/brpop`會拋出異常來。所以編寫客戶端消費者的時候要小心,注意捕獲異常,還要重試。
- **實現冷熱數據交換,并取最新的N條熱數據**
比如處理某篇文章的評論時,我們可以先把最新的評論通過Hash和List的方式來存儲;Hash中以ID為鍵,然后把這個ID存入List中,寫定時腳本把冷數據存入數據庫。
假設后五千條評論為冷數據,我們將最新的5000條評論從List中取出來:
- 使用LPUSH latest.comments<ID>命令,向list集合中插入數據
- 定時腳本判斷(或者在插入的時候判斷一下)是否超過五千,如果超過五千,就把其數據保存數據庫并清掉
- 如果有不同的篩選維度,比如某個分類的最新N條,那么你可以再建一個按此分類的List,只存ID的話,Redis是非常高效的。
```
function get_latest_comments(start,num_items):
id_list = redis.lrange("latest.comments",start,start+num_items-1)
IF id_list.length < num_items
id_list = SQL_DB("SELECT ... ORDER BY time LIMIT ...")
END
RETURN id_list
END
```
### zset
**排行榜應用**
與取最新數據的不同之處在于,前面操作以時間為權重(list就可以滿足),這個是以某個條件為權重,比如按頂的次數排序,這時候就需要我們的sorted set出馬了,將你要排序的值設置成sorted set的score,將具體的數據設置成相應的value,每次只需要執行一條ZADD命令即可。
zset 還可以用來存儲學生的成績,value 值是學生的 ID,score 是他的考試成績。我們可以對成績按分數進行排序就可以得到他的名次。
**粉絲列表**
zset 可以用來存粉絲列表,value 值是粉絲的用戶 ID,score 是關注時間。我們可以對粉絲列表按關注時間進行排序。
**構建優先級隊列系統**
**延時隊列**
我們將消息序列化成一個字符串作為 zset 的`value`,這個消息的到期處理時間作為`score`,**然后用多個線程輪詢 zset 獲取到期的任務進行處理,多個線程是為了保障可用性,萬一掛了一個線程還有其它線程可以繼續處理。**因為有多個線程,所以需要考慮并發爭搶任務,確保任務不能被多次執行。
```
def delay(msg):
msg.id = str(uuid.uuid4()) # 保證 value 值唯一
value = json.dumps(msg)
retry_ts = time.time() + 5 # 5 秒后重試
redis.zadd("delay-queue", retry_ts, value)
def loop():
while True:
# 最多取 1 條
values = redis.zrangebyscore("delay-queue", 0, time.time(), start=0, num=1)
if not values:
time.sleep(1) # 延時隊列空的,休息 1s
continue
value = values[0] # 拿第一條,也只有一條
success = redis.zrem("delay-queue", value) # 從消息隊列中移除該消息
if success: # 因為有多進程并發的可能,最終只會有一個進程可以搶到消息
msg = json.loads(value)
handle_msg(msg)
```
### set
**數據排重**
關于 redis的無序集合有三個特點: 無序性, 確定性(描述準確) , 唯一性。只需要不斷地將數據往set中扔,得到的數據肯定是uniq的。
**集合應用**
Redis 非常人性化的為集合提供了求交集、并集、差集等操作,那么就可以非常方便的實現如共同關注、共同喜好、二度好友等功能,對上面的所有集合操作,你還可以使用不同的命令選擇將結果返回給客戶端還是存集到一個新的集合中。
- 共同好友、二度好友
- 利用唯一性,可以統計訪問網站的所有獨立 IP
- 好友推薦的時候,根據 tag 求交集,大于某個 threshold 就可以推薦
### Pub/Sub
**Pub/Sub構建實時消息系統** (一般不用)
### Geo
附近的XXX