Redis In Action 笔记(二):文章投票
说明
原书中的例子使用的语言是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来实现业务需求。
本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 5年前 自动加精
推荐文章: