1. 程式人生 > 其它 >Redis 在 Web 專案中的應用與實踐

Redis 在 Web 專案中的應用與實踐

  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,效果等同於setnxXX:當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知識的同學可參考以下連結進一步學習。