ThinkPhp排行榜的设计
需求分析
排行功能
点击量+评分 = 权重 -> 进行排名
定时更新排行榜数据
排行数据按照分类进行划分, 全部分类包含所有分类数据
提升排名
置顶功能 :直接提升排名到 第一 或者 前面
直接修改排名分数
访问量、阅读量增多则自动提升排名
取消排名
- 降权, 直接不在排行榜进行显示
关键字搜索
需求实现
首先,我们来对比一下 Redis
和 mysql
的区别
Redis
支持 string 、 hash 、 list 、 set 、 sort set
等多种数据结构,并以内存的形式存储数据,且可持久化到本地中
mysql
则是存放在数据库中,在高并发的场景下需要极度依赖于硬件及其他各种优化情况
具体的对比我就不加分析
同时根据需求,我们需要存储的数据有 排名数据、折扣信息 这两样, 所以我们需要寻找合适的结构来保存
数据结构的选择
- hash
Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象。
Redis 中每个 hash 可以存储 232 - 1 键值对(40多亿)。
使用存储对象、可以保存大量的 键值对、可以当个修改 字段信息值,这些都是保存对象和数组的好玩意
用来保存折扣信息
- 有序集合(sorted set)
每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。
分析: 排行需要进行自动排行,减少查找等操作、需要进行保存大量的成员key值, 同时可以根据自定义的分数进行修改排行位置,所以 sorted set
是最适合的结构
功能刨析分块实现
由于 排行榜 、 redis 是扩展方法,所以采用 server 层来保存数据
- 安装 phpredis
这里使用的由于是 tp5 所以采用的是phpredis
获取
redis
实例子namespace app\common\server; /**
* 这是一个耦合 redis 的实例
*
* Class Redis
*/ classRedis{ protected $redis = null; protected $host; protected $port; protected $db = 5; public function__construct(){ $this->setConfig(); # 加载配置 $redis = new \Redis(); $redis->connect($this->host, $this->port); $redis->select($this->db); # 选择redis数据库 $this->redis = $redis; } # 单例模式实例化自身 public static functiongetInstance(){ $redis = new self(); return $redis->redis; } /**
* 加载配置文件
*
*@throws\think\Exception
*/ public functionsetConfig(){ $host = config('redis.host'); $port = config('redis.port'); if( !$host || !$port ) { throw new \think\Exception('请检查是否存在redis配置,目标 config/extra/redis.php'); } $this->host = $host; $this->port = $port; } }排行榜的初始化
分类的关联查询, 查询出折扣的详情、分类名称、发布人、以及最重要的阅读量、评论量
# namespace app\common\server; # class Rank /**
* 获取分类对应的文章信息
*/ public functiongetDiscountByCategory(){ $categories = $this->getCategoryField(['id', 'category_name']); $data = []; foreach ($categories as $key => $category) { $data[$key]['category_name'] = $category['category_name']; $data[$key]['category_id'] = $category['id']; $data[$key]['discounts'] = $this->getDiscount($category['id']); } return $data; } # 获取分类下的折扣信息 public functiongetDiscount($cid){ $disconts = Discount::all(function($query)use($cid){ $query->where('category_id', $cid); $query->field($this->field); $query->with(['user' => function($query){ $query->field(['id', 'username']); }]); $query->order('page_view', 'desc'); $query->order('comment_count', 'desc'); $query->order('create_time', 'desc'); $query->limit(0, 100); }); foreach ($disconts as &$discont) { $discont['username'] = $discont->user->username ?? null; unset($discont['user']); } return $disconts; }取出数据后,计算权重,并且写入到 redis 中
# 获取到了排行榜的数据 $ranks = $rank->getData(); $isCached = $rank->init_rank($ranks); public functiongetData(){ # 获取有阅读量有访问量的折扣 $discounts = $this->getDiscountByCategory(); return $discounts; } * 初始化排行榜数据 * @param $data 排行的数据 */ public functioninit_rank($data){ $redis = Redis::getInstance(); $data = $this->init_calcute_data($data); # 清除已知的key $this->clearKey(); # 保存所有键值的key $akey = $this->rank_c_all_key; foreach ($data as $key => $discounts) { # 取出 key 标识 $key = (int)$discounts['category_id']; # sortset 保存的是分类的key, 用于分类排行 $ckey = $this->rank_ckey_prefix . $key; # 开始缓存的是 / 分类下面的单个数据 foreach ($discounts['discounts'] as $discount) { # $discount 是单个数据 $dkey = $this->rank_dkey_prefix . $discount->id; $redis->hMSet($dkey, $discount->toArray()); # 单个数据缓存完成之后, 得到了这个数据的键 / 开始写入到 set sortset 数据中 $score = $discount['score']; $redis->zAdd($akey, $score, $dkey); $redis->zAdd($ckey, $score, $dkey); } # 推送到list中 / 告知我已经缓存了这个数据 $redis->lPush($this->rank_list, $ckey); } $len = $redis->lLen($this->rank_list); if( $len === count($data) ) { return true; } return false; }
初始化完成
获取单个折扣的数据
防止缓存穿透
/**
* 请求缓存中的折扣信息数据,防止数据已经失效,针对获取redis失败后重新缓存起来
*
*@param$key
*@param$ckey
*/ public functiongetDiscountByRedis($key, $ckey = null){ $redis = Redis::getInstance(); $discount = $redis->hGetAll($key); # 检查是否带正确的key 名称, 不是的话拼接为正确的key名称 if( !preg_match("/$this->rank_dkey_prefix/", $key) ) { $key = $this->rank_dkey_prefix . $key; } if( !preg_match("/$this->rank_ckey_prefix/", $ckey) ) { $ckey = $this->rank_ckey_prefix . $ckey; } if ( !$discount || empty($discount) ) { # 为空的情况下重新缓存 $did = (int)str_replace($this->rank_dkey_prefix, '', $key); $discount = (new Discount())->field($this->field)->find($did); if( $discount ) { # 当数据库中的折扣信息也被删除之后 $discount = $discount->toArray(); $discount['score'] = $this->calclute_weight($discount['page_view'], $discount['comment_count']); $redis->hMSet($key, $discount); }else{ if( !is_null($ckey) ) { # 从redis zset 中移除这个key $redis->zRem($ckey, $key); return null; } } } return $discount;修改权重
/**
* 插入排名或者更新排名
*@param$discount
*@param$score 分数
*@returnbool|string
*@throws\think\Exception
*/ public functioninsert_rank($discount, $score){ # 插入到指定的分类中 $redis = Redis::getInstance(); # 检查分类的 检查 自己是否存在于 redis 中 $key = $this->rank_dkey_prefix . $discount->id; $ckey = $this->rank_ckey_prefix . $discount->category_id; $discount = $this->getDiscountByRedis($key, $ckey); $discount['score'] = $score; if( !is_array($discount) ) { $discount = $discount->toArray(); } $redis->hMSet($key, $discount); # 重新设置 score if( $discount && !empty($discount) ) { # 添加到集合中去 $redis->zAdd($ckey, $score, $key); # 添加到全部 * 中, 用户全部分类的排行更新 $redis->zAdd($this->rank_c_all_key, $score, $key); # 添加自定义的 排行数据key 中, 用于更新了排行之后进行写入 $redis->zAdd($this->user_defined, $score, $key); $rankInAllIndex = $redis->zRevRank($this->rank_c_all_key, $key) + 1; $rankInCategoryIndex = $redis->zRevRank($ckey, $key) + 1; return "更新成功,在分类中的排行现在是 {$rankInCategoryIndex}, 在全部分类中的排行是: $rankInAllIndex"; } return false; }
- 置顶功能
/**
* * 置顶功能
*@parammixed $mix_id 折扣的id,可数组可int
*@parambool $isCache 是否为取消排行
*@returnint 返回的是置顶成功的条数
*@throws\think\Exception
*@throws\think\db\exception\DataNotFoundException
*@throws\think\db\exception\ModelNotFoundException
*@throws\think\exception\DbException
*/
public functionrank_stick($ids, $cid ='*', $isCache = false){
$ids = \json_decode($ids, true);
$susses_len = 0;
foreach ($ids as $id) {
# 分类id
$discount = Discount::where('id', $id)->field('category_id')->find();
if( $discount ) { # 确保数据存在更新分数
$discount = $this->getDiscountByRedis($id, $cid); # 从缓存中读取redis信息
$score = $this->getScorestByCategory($cid); # 当前分类最高的分数
if( !is_null($discount) && $score ) {
# 取消置顶
if( $isCache ) {
$this->update_rank($discount);
++ $susses_len;
}else{
$this->insert_rank($discount, $score + 1); # 在基础上加1
++ $susses_len;
}
}
}
}
return $susses_len;
}
本文由 邓尘锋 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: May 4, 2019 at 11:37 am