Redis In Action 笔记(二):文章投票

说明

原书中的例子使用的语言是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
            )
    )
*/

与前面添加的数据一致。

后记

Was mich nicht umbringt, macht mich stärker

本帖由系统于 2个月前 自动加精
讨论数量: 2

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!