php+redis實現排行榜demo
本週上班因爲任務分配的原因,跑回去寫redis去了.本週繼續複習redis,感覺對於redis的實踐開始有了新的認識.核心依舊是作爲緩存,而不是拿關係型數據庫來用.扯遠了,本週又寫了一些模塊,其中比較好用的是使用redis來實現排行榜,確實好用,快的多,要比傳統上使用mysql,存入到數據庫中(當然如果能有效利用緩存也能吧速度提升一個數量級,但是還是不如redisdemo穩定),再利用mysql引擎排序來快的多,缺點是需要獨立一個zset來保存排行榜.
—
有序集合和跳躍表
提到排行榜的數據結構,最開始毫無疑問我是想到當初寫的大根堆,這樣的二叉樹實現的數據結構的堆排序,從而實現排行榜.不過在redis中使用了更爲簡單的跳躍表 (當然在工程上通常會更復雜一點,這一類算法,通常會在較小的集合上用另一種簡單的數據結構代替複雜數據結構)來實現有序集合.通過有序集合(zset)我們可以實現排行榜這樣高效排序.
功能實現和分析
通過有序集合保存對應的分數和節點,這是一個非常簡單的數據結構.redis的zset提供了衆多的接口函數來實現對應的功能.
在這裏我有兩個實現思路.
- 一個是對於需要所有節點都在排行榜裏面的,會將所有的節點都放入zset中
- 只需要特定數量的排行榜.比如我只需要前20件熱銷商品(或者說這一類需求最集中),這樣我只維護一個大小爲20的zset,對於最新的分數,會嘗試重新插入現有的集合中,併除去最低或者最高的分數.理論上這個算法,在總分數的數量極大之後會更快.
當然在這裏我先實現的第一種思路,這種思路比較簡單,所有的插入更新都直接使用zset更新.如果以後有新的需求我會嘗試使用第二個思路.
在這裏只實現兩個功能,
- 能夠查詢每個節點的分數和名次 通過zRevRange函數獲取;
- 能夠按名次查詢排名前N名的節點通過zRevRank函數獲取;
代碼
這裏使用phpredis擴展來實現redis的鏈接
<?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);
}
}
}