php+redis实现排行榜(demo)
比如讲数据存储在了 Redis 的有序集合 user_score 中,使用 Redis 来统计玩家排行榜的数据。
首先我们需要思考的是,一个典型的游戏排行榜都包括哪些功能呢?
统计全部玩家的排行榜
按名次查询排名前 N 名的玩家
查询某个玩家的分数
查询某个玩家的排名
对玩家的分数和排名进行更新
查询指定玩家前后 M 名的玩家
增加或移除某个玩家,并对排名进行更新
在 Redis 中实现上面的功能非常简单,只需要使用 Redis 我们提供的方法即可,针对上面的排行榜功能需求,我们分别来看下 Redis 是如何实现的。
统计全部玩家的排行榜
在 Redis 里,统计全部玩家的排行榜的命令格式为 ZREVRANGE 排行榜名称 起始位置 结束为止 [WITHSCORES] 。
我们使用这行命令即可:
ZREVRANGE user_score 0 -1 WITHSCORES
我们对玩家排行榜 user_score 进行统计,其中 -1 代表的是全部的玩家数据, WITHSCORES 代表的是输出排名的同时也输出分数。
按名次查询排名前 N 名的玩家
同样我们可以使用 ZREVRANGE 完成前 N 名玩家的排名,比如我们想要统计前 10 名玩家,可以使用: ZREVRANGE user_score 0 9 。
查询某个玩家的分数
命令格式为 ZSCORE 排行榜名称 玩家标识 。
时间复杂度为 O(1) 。
如果我们想要查询玩家 10001 的分数可以使用: ZSCORE user_score 10001 。
查询某个玩家的排名
命令格式为 ZREVRANK 排行榜名称 玩家标识 。
时间复杂度为 O(log(N)) 。
如果我们想要查询玩家 10001 的排名可以使用: ZREVRANK user_score 10001 。
对玩家的分数进行更新,同时排名进行更新
如果我们想要对玩家的分数进行增减,命令格式为 ZINCRBY 排行榜名称 分数变化 玩家标识 。
时间复杂度为 O(log(N)) 。
比如我们想对玩家 10001 的分数减 1,可以使用: ZINCRBY user_score -1 10001 。
然后我们再来查看下玩家 10001 的排名,使用: ZREVRANK user_score 10001
。
你能看到排名由 17153 降到了 18036 名。
查询指定玩家前后 M 名的玩家
比如我们想要查询玩家 10001 前后 5 名玩家都是谁,当前已知玩家 10001 的排名是 18036,那么可以使用: ZREVRANGE user_score 18031 18041
。
这样就可以得到玩家 10001 前后 5 名玩家的信息。
增加或删除某个玩家,并对排名进行更新
如果我们想要删除某个玩家,命令格式为 ZREM 排行榜名称 玩家标识
。
时间复杂度为 O(log(N))
。
比如我们想要删除玩家 10001,可以使用: ZREM user_score 10001
。
这样我们再来查询下排名在 18031 到 18041 的玩家是谁,使用: ZREVRANGE user_score 18031 18041
。
你能看到玩家 10001 的信息被删除,同时后面的玩家排名都向前移了一位。
如果我们想要增加某个玩家的数据,命令格式为 ZADD 排行榜名称 分数 玩家标识
。
时间复杂度为 O(log(N))
。
这里,我们把玩家 10001 的信息再增加回来,使用: ZADD user_score 93.1504697596 10001
。
然后我们再来看下排名在 18031 到 18041 的玩家是谁,使用: ZREVRANGE user_score 18031 18041
。
你能看到插入了玩家 10001 的数据之后,排名又回来了。
下面封装的一个demo,摘自https://blog.csdn.net/u011822516/article/details/82734992
<?php
namespace Leaderboard;
/**
* 使用rediszset的的商品排行榜
* @author yiwang
*
*/
class RedisLeaderboard
{
/**
*
* @var object redis client
*/
private $redis;
/**
*
* @var string 放置排行榜的key
*/
private $leaderboard;
/**
* 构造函数
* @param object $redis 已连接redis的phpredis的对象
* @param string $leaderboard 字符串,排行榜的key名
*/
public function __construct($redis = [], $leaderboard = '')
{
if ($redis) {
$this->redis = $redis;
} else {
$this->redis = new \Redis();
$this->redis->connect('127.0.0.1');
}
if ($leaderboard) {
//这里不会检查当前的key值是否存在,是为了方便重新访问对应的排行榜
$this->leaderboard = $leaderboard;
} else {
$this->leaderboard = 'leaderboard:' . mt(1, 100000);
while (!empty($this->redis->exists($this->leaderboard))) {
$this->leaderboard = 'leaderboard:' . mt(1, 100000);
}
}
}
/**
* 获取当前的排行榜的key名
* @return string
*/
public function getLeaderboard()
{
return $this->leaderboard;
}
/**
* 将对应的值填入到排行榜中
* @param $node 对应的需要填入的值(比如商品的id)
* @param number $count 对应的分数,默认值为1
* @return Long 1 if the element is added. 0 otherwise.
*/
public function addLeaderboard($node, $count = 1)
{
return $this->redis->zAdd($this->leaderboard, $count, $node);
}
/**
* 给出对应的排行榜
* @param int $number 需要给出排行榜数目
* @param bool $asc 排序顺序 true为按照高分为第0
* @param bool $withscores 是否需要分数
* @param callback $callback 用于处理排行榜的回调函数
* @return [] 对应排行榜
*/
public function getLeadboard($number, $asc = true, $withscores = false,$callback = null)
{
if ($asc) {
$nowLeadboard = $this->redis->zRevRange($this->leaderboard, 0, $number -1, $withscores);//按照高分数顺序排行;
} else {
$nowLeadboard = $this->redis->zRange($this->leaderboard, 0, $number -1, $withscores);//按照低分数顺序排行;
}
if ($callback) {
//使用回调处理
return $callback($nowLeadboard);
} else {
return $nowLeadboard;
}
}
/**
* 获取给定节点的排名
* @param string $node 对应的节点的key名
* @param string $asc 是否按照分数大小正序排名, true的情况下分数越大,排名越高
* @return 节点排名,根据$asc排序,true的话,第一高分为0,false的话第一低分为0
*/
public function getNodeRank($node, $asc = true)
{
if ($asc) {
//zRevRank 分数最高的排行为0,所以需要加1位
return $this->redis->zRevRank($this->leaderboard, $node);
} else {
return $this->redis->zRank($this->leaderboard, $node);
}
}
}