Redis作為一個開源的(BSD)基於內存的高性能存儲系統,已經被各大互聯網公司廣泛使用,並且有著諸多的應用場景。本篇文章將基於PHP來詳細講解Redis在Web項目中的主要應用與實踐。
緩存
這裡所介紹的緩存是指可以丟失或過期的數據。常用的命令有 set, hset, get, hget,使用redis作為緩存時需要注意一下幾個問題:
- 由於redis的可用內存是有限的,不能容忍redis內存的無限增長,建議設置
maxmemory最大內存。 - 在開啟maxmemory的情況下,可以啟用lru機制,設置key的expire,當到達Redis最大內存時,Redis會根據最近最少用算法對key進行自動淘汰。
- Redis的持久化策略和Redis故障恢復時間是一個博弈的過程,如果你希望在發生故障時能夠盡快恢復,應該啟用dump備份機制,但這樣需要更多的可用內存空間來進行持久化。如果能夠容忍Redis漫長的故障恢復時間,可以使用AOF持久化機制,同時關閉dump機制,這樣不需要額外的內存空間。
存儲
在web項目中,redis可存儲讀寫非常頻繁的數據來緩解MySQL等數據庫的壓力。redis如果作為存儲系統的話,為了防止數據丟失,持久化必須開啟。
典型場景
計數器
計數器的需求非常普遍,例如微博點贊數、帖子收藏數、文章分享數、用戶關注數等。
社交列表
比如使用Sets結構存儲關注列表、收藏列表、點贊列表等。
Session
借助redis高性能的key-value存儲,可將用戶登錄狀態保存到redis中。
…
隊列
簡單隊列
一般使用redis的list結構作為隊列,rpush 生產消息,lpop 消費消息,當 lpop 沒有消息的時候,要進行適當的sleep操作。
$queueKey = "queue" ;
// 生產者
$redis->rpush($queueKey, $data)
//消費者
while ( true ) {
$data = $redis->lpop($queueKey);
if ( null === $data) {
usleep( 100000 );
continue ;
}
// 業務邏輯
...
}
由於沒有消息時使用的sleep事件不好控制,生產環境盡量不要使用sleep來休眠,可使用 blpop 來消費消息,在沒有新消息的時候它會阻塞到消息到來。
延時隊列
延時隊列可使用redis的 sorted set 數據結構,使用時間戳作為 score ,消息內容作為 member,使用 zadd 命令來生產消息,消費者使用 zrangebyscore 命令獲取指定時間之前的消息數據輪詢進行處理。
$queueKey = "queue" ;
// 生產消息
//消費時間,這裡設置為1小時候
$consumeTimestamp = time() + 3600 ;
// $data需要添加隨機串前綴(or後綴),防止出現重複member被丟棄
$data = $data . md5(uniqid(rand (), true ));
$redis->zadd($queueKey, $consumeTimestamp, $data);
//消費消息
while (tue) {
$arrData = $redis->zrangebyscore($queueKey, 0 , time());
if (!$arrData) {
usleep( 100000 );
continue ;
}
//業務邏輯
foreach ($arrData as $data) {
$data = substr($data, 0 , strlen($data) - 32 );
// 消費$data
}
}
多消費者
使用pub/sub主題訂閱者模式,可以實現1:N的消息隊列。這種模式中在消費者下線的情況下,生產的消息會丟失,在這裡不推薦使用。
需要強調的是不推薦使用redis作為消息隊列服務,這不是redis的設計目標。如果一定要用可考慮 disque,是由redis的作者開發。
分佈式鎖
分佈式鎖主要解決的幾個問題:
- 互斥性: 同一時刻只能有一個服務(或應用)訪問資源
- 安全性: 鎖只能被持有該鎖的服務(或應用)釋放
- 容錯: 在持有鎖的服務crash時,鎖仍能得到釋放
- 避免死鎖
方案1
我們可能會考慮使用 setnx 和 expire 命令來實現加鎖,即當沒有key存在時才會成功寫入value:
$lockStatus = $redis->setnx($lockKey, 1 );
if ( 1 === $lockStatus) {
//加鎖成功,為鎖設置超時時間
$redis->expire($lockKey, 300 );
// 進行後續操作
} elseif ( 0 === $lockStatus) {
//加鎖失敗
} else {
//其他異常
}
但這種操作不是原子性的,如果在進行setnx時服務崩潰,沒有來得及對Key進行超時設置,該鎖將一直無法釋放。
方案2
我們推薦 set key value [EX seconds] [PX milliseconds] [NX|XX] 命令來進行加鎖
- EX: key在多少秒之後過期
- PX:key在多少毫秒之後過期
- NX: 當key不存在的時候,才創建key,效果等同於setnx
- XX:當key存在的時候,覆蓋key
$lockStatus = $this ->redis->set($lockKey, 1 , "EX" , 30 , "NX" );
if ( "OK" === $lockStatus) {
//加鎖成功,可進行後續操作
//業務邏輯執行完畢,釋放鎖
$this ->redis->del($lockKey);
} elseif ( null === $lockStatus) {
//加鎖失敗
}
如上代碼所示,如果 set 命令返回OK,那麼客戶端就可以獲得鎖(如果返回null,那麼應用服務可以在一段時間之後重新嘗試獲取鎖),並且可以通過 del 命令來釋放鎖。
此方法需要注意的問題:
- a服務獲得的鎖(鍵key)已經由於已到過期時間被redis服務器刪除,但是這個時候a服務還去執行DEL命令。而b服務經在a設置的過期時間之後重新獲取了這個同樣key的鎖,那麼a執行
del就會釋放了b服務加好的鎖。 - 當同一時刻有大量的key過期的時候,刪除key時會增加redis壓力,會影響服務穩定。
可以通過如下優化使得上面的鎖系統變得更加健壯:
- 不要設置固定的字符串,而是設置為隨機的大字符串,可以稱為token。
- 通過腳本刪除指定鎖的key,而不是
del命令。 - 在設置key過期時間的時候加上一個隨機值。
優化後的代碼可參考如下:
$lockToken = md5(uniqid(rand(), true ));
//此處超時時間根據具體業務邏輯配置
$expire = rand( 280 , 320 );
$lockStatus = $this ->redis->set($lockKey, $lockToken, "EX" , $expire, "NX" );
if ( "OK" === $lockStatus) {
//加鎖成功,可進行後續操作
//業務邏輯執行完畢,釋放鎖
//刪除鎖之前需要判斷是否是自己上的鎖
$currentToken = $this ->redis->get($lockKey);
if ($currentToken === $lockToken) {
$ this ->redis->del($lockKey);
}
} elseif ( null === $lockStatus) {
//加鎖失敗
}
計算
redis提供的原子自增減方法以及有序集合結構等可以承擔一些計算任務,例如瀏覽量統計等。
瀏覽計數
文章瀏覽量+1
$redis->incr($postsKey);
批量獲取文章瀏覽量
$arrPostsKey = [
//...
];
$arrPostsViewNum = $redis->mget($arrPostsKey);
排行榜
可以使用redis的有序集合來實現排行榜的功能,score作為權重排序並取前n條記錄。
//存儲數據 $sortKey = "sort_key" ; $redis->zadd($sortKey, 100 , "tom" ); $redis->zadd($sortKey, 80 , "Jon" ); $redis->zadd($sortKey, 59 , "Lilei" ); $redis->zadd($sortKey, 87 , "Hanmeimei" ); // 獲取排行 //由大到小排序 $arrRet = $redis->zrevrange($sortKey, 0 , -1 , true ); //由小到大排序 $arrRet = $redis->zrange($sortKey, 0 , -1 , true );
總結
redis涉及的應用實踐非常繁多的,由於篇幅所限無法全部顧及,本文只針對web應用中最常用的幾個場景進行了展開介紹,渴望進一步拓展redis知識的同學可參考以下鏈接進一步學習。