php+redis實現排行榜demo

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);
        }
    }

}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章