Redis In Action 筆記(二):文章投票(PHP 版)

tsin發表於2019-06-08

說明

原書中的例子使用的語言是python,這裡使用php對該例子進行改寫、註釋並測試執行結果。

例子要實現的功能:類似Reddit、Stack Overflow社群的vote up功能,使用者對文章進行投票,文章根據釋出日期和投票數計算得分,根據得分對文章從高分到低分進行排序;同時可以給文章新增分類,並實現訪問不同分類也是按高分到低分排序。

約束條件:

  1. 一個使用者對一篇文章只能投一票
  2. 一篇文章釋出7天后不能再進行投票
  3. 文章得分計算規則:釋出時間+得票數X倍數(這裡的倍數計算規則為:86400/200=432,86400為一天的秒數,200為一天時間對應的投票數(原書這麼假設))

資料結構

  1. 文章
    使用hash型別來儲存文章的資訊,結構如下:

Redis In Action 筆記(二):文章投票(PHP 版)
儲存了對應ID文章的title、link、poster、time和votes。

  1. 文章釋出時間
    使用zset有序集合來儲存,結構如下:

Redis In Action 筆記(二):文章投票(PHP 版)

time:為key,裡面的元素中,member為文章ID(article:xxx的形式),score為時間戳。

  1. 文章得分
    同樣使用zset有序集合,原理同上。

Redis In Action 筆記(二):文章投票(PHP 版)

  1. 文章投票使用者集合
    使用set無序集合,因無序集合的元素是不重複的,因此其本身就可以滿足一個使用者對一篇文章只能投一票,其結構如下:

Redis In Action 筆記(二):文章投票(PHP 版)

keyvoted:+文章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;
}

文章分組並排序輸出

  1. 給文章新增/移除分組,程式碼如下:

    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,keygroup:+分組名稱,集合中的元素形式為article:+ 文章ID

  2. 給分組中的文章新增得分
    僅有第一步的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作為聚合運算方式,即取出文章得分集合中的分數合併到計算結果中。計算過程如圖所示:

Redis In Action 筆記(二):文章投票(PHP 版)
如圖,兩個集合通過交集運算最後的到第三個集合。

程式執行

釋出若干文章

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來實現業務需求。

Was mich nicht umbringt, macht mich stärker

相關文章