說明
原書中的例子使用的語言是python,這裡使用php對該例子進行改寫、註釋並測試執行結果。
例子要實現的功能:類似Reddit、Stack Overflow社群的vote up功能,使用者對文章進行投票,文章根據釋出日期和投票數計算得分,根據得分對文章從高分到低分進行排序;同時可以給文章新增分類,並實現訪問不同分類也是按高分到低分排序。
約束條件:
- 一個使用者對一篇文章只能投一票
- 一篇文章釋出7天后不能再進行投票
- 文章得分計算規則:釋出時間+得票數X倍數(這裡的倍數計算規則為:86400/200=432,86400為一天的秒數,200為一天時間對應的投票數(原書這麼假設))
資料結構
- 文章
使用hash型別來儲存文章的資訊,結構如下:
儲存了對應ID文章的title、link、poster、time和votes。
- 文章釋出時間
使用zset有序集合來儲存,結構如下:
time:
為key,裡面的元素中,member為文章ID(article:xxx的形式),score為時間戳。
- 文章得分
同樣使用zset有序集合,原理同上。
- 文章投票使用者集合
使用set無序集合,因無序集合的元素是不重複的,因此其本身就可以滿足一個使用者對一篇文章只能投一票,其結構如下:
key
為voted:
+文章ID
程式實現
Redis連線和例項獲取、常量設定
const ONE_WEEK_IN_SECONDS = 7 * 86400;
const VOTE_SCORE = 432;
const ARTICLES_PER_PAGE = 25;
$redis = new Redis();
$redis->connect('127.0.0.1', '6379') || exit('連線失敗!');
釋出文章
程式碼如下:
/**
* @param Redis $redis Redis例項,下文同樣
*/
function postArticle($redis, $user, $title, $link)
{
$article_id = $redis->incr('article:'); //自增1,不存在key則賦值1
$voted = 'voted:' . $article_id;
//將作者設為已投票使用者
$redis->sAdd($voted, $user);
//文章投票資訊設定為一週後自動失效
$redis->expire($voted, ONE_WEEK_IN_SECONDS);
$now = time();
//新增文章
$article = 'article:' . $article_id; //作為文章hash的key值
$redis->hMSet($article, [ //批量設定hash鍵值對
'title' => $title,
'link' => $link,
'poster' => $user,
'time' => $now,
'votes' => 1,
]);
//注意zadd第二個引數為score,第三個為member
$redis->zAdd('score:', $now + VOTE_SCORE, $article); //設定文章初始分數
$redis->zAdd('time:', $now, $article); //記錄文章發表時間
return $article_id;
}
使用者對文章投票
程式碼如下:
function articleVote($redis, $user, $article)
{
$cutoff = time() - ONE_WEEK_IN_SECONDS;
//對發表時間超過一週的文章投票不生效
//獲取 time: 有序集合對應 member 的 score
if ($redis->zScore('time:', $article) < $cutoff) {
return;
}
$article_id = explode(':', $article)[1];
//無序集合,新增記錄,如果記錄存在,返回0(說明使用者已對該文章投票)
//反之,返回1,執行if條件下的程式碼,計算分數
if ($redis->sAdd('voted:' . $article_id, $user)) {
//暫不考慮事務操作
$redis->zIncrBy('score:' , VOTE_SCORE, $article); //增加文章的分數
$redis->hIncrBy($article, 'votes', 1); //增加文章的投票數
}
}
文章列表
程式碼如下:
function getArticles($redis, $page, $order = 'score:')
{
$start = ($page - 1) * ARTICLES_PER_PAGE;
$end = $start + ARTICLES_PER_PAGE - 1;
//獲取指定範圍內的member值,按$order分數遞減排序
//zrevrange、zrange有withscores引數才返回score值,否則只返回member值
//這裡的member值是`article:`+ 文章ID
$ids = $redis->zRevRange($order, $start, $end);
$articles = [];
foreach ($ids as $id) {
//取出文章hash中對應ID的資料
$article_data = $redis->hGetAll($id);
$article_data['id'] = $id;
$articles[] = $article_data;
}
return $articles;
}
文章分組並排序輸出
-
給文章新增/移除分組,程式碼如下:
function addRemoveGroups($redis, $article_id, $to_add = [], $to_remove = []) { $article = 'article:' . $article_id; foreach ($to_add as $group) { $redis->sAdd('group:' . $group, $article); } foreach ($to_remove as $group) { $redis->sRem('group:' . $group, $article); } }
這裡每個分組為一個無序集合set,
key
為group:
+分組名稱,集合中的元素形式為article:
+ 文章ID - 給分組中的文章新增得分
僅有第一步的set,我們無法給文章進行排序。可以使用zInterstore求分組集合和所有文章得分集合的交集,來獲得一個分組文章得分有序列表。實現程式碼如下:function getGroupArticles($redis, $group, $page, $order = 'score:') { $key = $order . $group; if (!$redis->exists($key)) { //獲得對應分組下,文章-分數的有序集合 $redis->zInterStore($key, //$key 為求交集結果存放資料的鍵 ['group:' . $group, $order], //兩個要求交集的集合 [1, 1], //兩個集合對應的權重 'max'); //score的計算方式,還有min、sum,這裡使用max求最大值 $redis->expire($key, 60); //設定60s後過期 } //使用$key(即求得的文章得分集合)作為排序資料 return getArticles($redis, $page, $key); }
需要注意的是,這裡的'group:' . $group,即文章分組資料,為無序集合,相比於zset有序集合是沒有score值的,
zinterscore
運算會預設其score值為1,所以這裡使用max
作為聚合運算方式,即取出文章得分集合中的分數合併到計算結果中。計算過程如圖所示:
如圖,兩個集合通過交集運算最後的到第三個集合。
程式執行
釋出若干文章
postArticle($redis, 'user:1', '測試文章1', 'article-link-1');
postArticle($redis, 'user:2', '測試文章2', 'article-link-2');
postArticle($redis, 'user:3', '測試文章3', 'article-link-3');
投票
//使用者10對文章1進行投票
articleVote($redis, 'user:10', 'article:1');
//輸出投票結果
echo "article:1 的投票使用者:" . PHP_EOL;
$result = $redis->sMembers('voted:1');
print_r($result);
/**
輸出:
article:1 的投票使用者:
Array
(
[0] => user:10
[1] => user:1
)
*/
文章列表
echo "文章列表:" . PHP_EOL;
$articles = getArticles($redis, 1);
print_r($articles);
/**
輸出:
Array
(
[0] => Array
(
[title] => 測試文章1
[link] => article-link-1
[poster] => user:1
[time] => 1559925634
[votes] => 2
[1] => article:1
)
[1] => Array
(
//此處省略...
)
[2] => Array
(
//此處省略...
)
)
*/
文章分類列表
//給文章新增分類
addRemoveGroups($redis, '1', ['php', 'redis']);
addRemoveGroups($redis, '2', ['python', 'redis']);
//獲取‘redis’分組下的文章
$redisGroupArticles = getGroupArticles($redis, 'redis', 1);
echo "redis分類的文章列表:" . PHP_EOL;
print_r($redisGroupArticles);
/**
輸出:
redis分類的文章列表:
Array
(
[0] => Array
(
[title] => 測試文章1
[link] => article-link-1
[poster] => user:1
[time] => 1559925634
[votes] => 2
[1] => article:1
)
[1] => Array
(
[title] => 測試文章2
[link] => article-link-2
[poster] => user:2
[time] => 1559925634
[votes] => 1
[1] => article:2
)
)
*/
與前面新增的資料一致。
後記
- 參考資料:https://redislabs.com/ebook/part-1-getting...
- 完整程式碼:https://github.com/HubQin/redis-in-action-...
- 執行環境要求及程式執行方法:安裝redis和phpredis擴充套件,下載程式碼檔案,在檔案所在的目錄執行:
php article_vote.php
即可。 - 做完這一個例子,對我說挺開拓思路的,正如作者所說的,你不再只會往資料庫裡塞東西,而是還會用Redis來實現業務需求。