今天星期一,满血回归。每天开心的工作,愉快的生活。
继上一篇Redis实战运用之SET更新有两天了,周末浪去了。今天接着上一篇分享下SortedSet,上一篇分享了SET的一些基本操作,例如像集合中添加元素、删除元素、取出元素,取交集,取并集等操作,(不了解的SET的,建议可以先去看下Redis实战运用之SET。^_^)。
其实呢,今天分享的SortedSet,顾名思义就是一个有序的集合。既然是集合,那么和上一篇分享的SET其实是相似的,有很多类似的操作,取交、取并、添加、删除等功能。主要的区别在于“有序”二字上,既然有序那必然得有一个排序的依据,在SortedSet里面这个排序的依据名称叫做:Score,每一个有序集合的元素都有一个对应的Score值。
那么设置到添加有序集合时,添加元素的时候比之前SET添加元素的时候多一个Score参数就很能理解了。有了这个Score的值,那么这个集合有有了序列,有了序列就又了排名。既然如此,SortedSet最主要的一些方法,例如:ZRANGEBYSCORE根据score范围获取元素,ZRANK根据排名获取元素,ZREMRANGEBYRANK,ZREMRANGEBYSCORE根据分数、根据排名来删除元素,ZSCORE获取元素socre,ZREVRANK、ZRANK根据排名升序、倒序获取元素,ZRANGE、ZREVRANGE根据Score升序、倒序获取元素等,这些理所当然的方法是不是就很容易理解了,都是基于score这个值来确定的。具体详细的方法依然贴在本位最后的位置。
至此,有了上面的理解,那么上一篇些分享SET留下的问题,就迎刃而解了。上一篇提及的多个标签关联查询可以SET来实现交集,但是不能实现排序分页等功能,所以是使用SortedSet来实现的。
是时候上一波代码了:
/** * @desc 有序集合获取分页数据 * @param string $redisKey * @param int $currentPage * @param int $perPage * @param string $sort * @return mixed */ public static function pageData($redisKey = '',$currentPage = 1, $perPage = 10,$sort = 'desc'){ $pagination['currentPage'] = $currentPage; $pagination['perPage'] = $perPage; $pagination['totalPage'] = $pagination['totalCount'] = 0; $rs['data'] = []; $redis = Yii::$app->redis; $pagination['totalCount'] = $redis->ZCard($redisKey); $pagination['totalPage'] = ceil($pagination['totalCount'] / $perPage); //这里的$start $end 就相当于mysql里面:limit start end $start = ($currentPage - 1) * $perPage; $end = $currentPage * $perPage - 1; if(strtolower($sort) == 'desc'){ $rs['data'] = $redis->ZRevrange($redisKey,$start,$end); }else{ $rs['data'] = $redis->zRange($redisKey, $start, $end); } $rs['pagination'] = $pagination; return $rs; } //用的是https://github.com/yiisoft/yii2-redis这个扩展重新处理了哈zrange这个方法 withScores就是放回元素的时候同时返回其score public static function zRange($redisKey = '',$start = 0,$end = -1,$withScores = true,$sort = SORT_DESC){ $data = []; if($withScores){ $rs = []; $data = Yii::$app->redis->ZRange($redisKey,$start,$end,'WITHSCORES'); if($data){ $count = count($data); for($i = 0 ; $i < $count ; $i++){ if($i % 2 != 0){ $rs[$data[$i - 1]] = $data[$i]; } } array_multisort($rs,$sort); } return $rs; }else{ $data = Yii::$app->redis->ZRange($redisKey,$start,$end); } return $data; } /** * @desc 获取交集数据 获取并集基本一样的操作 名字换成zUnionStore即可 * @param array $redisKeys 键值 必须大于一个 * @param int $expire 缓存时间 默认2个小时 * @return string */ public static function zInterStore($redisKeys = [],$expire = 2 * 60 * 60){ if(empty($redisKeys)) return ''; $keyCount = count($redisKeys); //最少需要2个集合才能取交 if($keyCount < 2) return $redisKeys[0]; //最多只能是5个集合取交 $keyCount > 5 && $keyCount = 5; $destination = 'xjj:'.md5(json_encode($redisKeys)); //xjj=>小JJ 并集 djj=>大JJ if((int)Yii::$app->get('redis')->exists($destination) == 1) return $destination; //有缓存直接读取缓存 $rs = 0; switch($keyCount){ case 2: $rs = Yii::$app->get('redis')->zinterstore($destination,$keyCount,$redisKeys[0],$redisKeys[1]); break; case 3: $rs = Yii::$app->get('redis')->zinterstore($destination,$keyCount,$redisKeys[0],$redisKeys[1],$redisKeys[2]); break; case 4: $rs = Yii::$app->get('redis')->zinterstore($destination,$keyCount,$redisKeys[0],$redisKeys[1],$redisKeys[2],$redisKeys[3]); break; case 5: $rs = Yii::$app->get('redis')->zinterstore($destination,$keyCount,$redisKeys[0],$redisKeys[1],$redisKeys[2],$redisKeys[3],$redisKeys[4]); break; default: break; } if(intval($rs) === 0) return ''; //交集直接就木有数据 TODO 标记取交集没有结果的$destination 避免重复取交集运算 这个很好解决自己去搞吧 Yii::$app->get('redis')->expire($destination,$expire); return $destination; }

有了上面的代码那么实现这个搜索具体的实现如下:
$page = (int)Yii::$app->request->get('page') ?: 1; //从tag的hash中读取的数据 $tagInfoList = [ ['id' => 1,'name' => '韩国美女','alias' =>'hanguo'], ['id' => 2,'name' => '狮子座','alias' =>'sz'] ['id' => 3,'name' => '平面模特','alias' =>'mote'] ['id' => 4,'name' => '清纯','alias' =>'qincun'] ['id' => 5,'name' => '正妹','alias' =>'zhengmei'] ]; //指定字段获取明星列表数据 $needFields = ['id','name','nick_name','header','view','ranking']; //查询的字段 //获取数据就是这么简单,依然没有查询数据库。 $result = Star::getStarByTags($tagInfoList,$page,20,$needFields); /** * getStarByTags 具体实现 * 逻辑说明 多个标签的时候 取明星数据通过“全部交集”的方式获取 * @param array $tags * @param int $currentPage * @param int $perPage * @param array $fields * @return array|mixed */ public static function getStarByTags($tags = [],$currentPage = 1, $perPage = 10,$fields = []){ $tagCount = count($tags); //没有查询条件 直接读取总的明星列表 if($tagCount === 0) return static::getMaxViewStar($currentPage,$perPage,$fields); $redisKey = ''; //只有一个标签,直接读取该标签的数据即可 if(count($tags) === 1){ $redisKey = 'tagZset:'.trim($tags[0]['alias']); }else{ $alias = array_column($tags,'alias'); $alias = array_map(function($value){ return 'tagZset:'.trim($value); },$alias); $redisKey = RedisService::zInterStore($alias); //上一段代码中有 } //上面代码中有 分页获取SortedSet中的数据 $rs = RedisService::pageData($redisKey,$currentPage,$perPage); //根据ID从hash中获取具体信息,前面写hash的时候分享过。 $rs['data'] = static::formatStars($rs['data'],$fields); return $rs; } /** * @desc 获取浏览量最高的明星数据 starZset也是一个有序结合,以view作为分数。 * @param int $currentPage * @param int $perPage * @param array $fields * @return array */ public static function getMaxViewStar($currentPage = 1, $perPage = 10,$fields = []){ $rs = RedisService::pageData('starZset',$currentPage,$perPage); $rs['data'] = static::formatStars($rs['data'],$fields); return $rs; }
以上代码当然也是有序结合的基本运用。借此就会发现sort设置用于做一个排行榜就很容易了。例如说就我自己的果图网:http://www.guotuw.com/ranking/这个页面包含了各种排行榜,例如大陆美女榜单,韩国美女榜,混血美女榜单等等,甚至以后扩展各种各样的榜单设置都完全不用该代码就能完全兼容。具体实现也很简单,就只用有序集合就能实现了。例如:大陆美女榜单包含薛丽、于欣彤,韩国美女榜包含최별하,차정아等无论有哪些明星。就相当于一个分类的明星ID作为元素,浏览量view作为score,就能实现各种榜单。很简单吧。^_^
在例如,很多网站都在做日榜单,月榜单这个功能,这里在来贴一段代码具体实现非常之简单。
/** * @desc 更新明星访问量同时记录明星每天每月的访问量 * @param int $starId */ public static function incrView($starId = 0){ $redis = Yii::$app->get('redis'); $starId = intval($starId); if($starId == 0) return; $increment = rand(1,3); $redis->hincrby('star:'.$starId,'view',$increment);//明星hash $redis->zincrby('starZset',$increment,$starId); //明星总榜 //为各种明星榜单做基础数据 记录每个明星每天的访问量 $starDayView = 'starDayView:'.date('Ymd',time()); //记录每个明星每月的访问量 $starMonthView = 'starMonthView:'.date('Ym',time()); /* * 例如: * starDayView:20171012 = [starA=>3,starB=>4,c=>5] * starDayView:20171013 = [starA=>5,starB=>4,c=>0] * 直接取并集 score默认求和 然后和各类型的榜单的明星ID取交集即可 明星的score默认为0 * 交集结果缓存到每天晚上的12点过期 即可实现榜单每天自动更新 * * 月榜:直接从starMonthView取值即可 */ $redis->zincrby($starDayView,$increment,$starId); $redis->zincrby($starMonthView,$increment,$starId); }
这篇有点长,如果你都能看到这里了,那就谢谢你了。最后在啰嗦下有序结合取交集和并集的时候关于score的问题,默认求和SUM,可以将所有集合中某个成员的 score 值之 和 作为结果集中该成员的 score 值;使用参数 MIN ,可以将所有集合中某个成员的 最小 score 值作为结果集中该成员的 score 值;而参数 MAX 则是将所有集合中某个成员的 最大 score 值作为结果集中该成员的 score 值。这个看具体的需求运用即可。
如果你看累了,可以下我的果图网,打哈望,美女多多哟。下一篇分享:Redis Pub/Sub(发布/订阅)
有序集合具体方法如下:
序号 | 命令及描述 |
1 | ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员,或者更新已存在成员的分数 |
2 | ZCARD key 获取有序集合的成员数 |
3 | ZCOUNT key min max 计算在有序集合中指定区间分数的成员数 |
4 | ZINCRBY key increment member 有序集合中对指定成员的分数加上增量 increment |
5 | ZINTERSTORE destination numkeys key [key ...] 计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 key 中 |
6 | ZLEXCOUNT key min max 在有序集合中计算指定字典区间内成员数量 |
7 | ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合成指定区间内的成员 |
8 | ZRANGEBYLEX key min max [LIMIT offset count] 通过字典区间返回有序集合的成员 |
9 | ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT] 通过分数返回有序集合指定区间内的成员 |
10 | ZRANK key member 返回有序集合中指定成员的索引 |
11 | ZREM key member [member ...] 移除有序集合中的一个或多个成员 |
12 | ZREMRANGEBYLEX key min max 移除有序集合中给定的字典区间的所有成员 |
13 | ZREMRANGEBYRANK key start stop 移除有序集合中给定的排名区间的所有成员 |
14 | ZREMRANGEBYSCORE key min max 移除有序集合中给定的分数区间的所有成员 |
15 | ZREVRANGE key start stop [WITHSCORES] 返回有序集中指定区间内的成员,通过索引,分数从高到底 |
16 | ZREVRANGEBYSCORE key max min [WITHSCORES] 返回有序集中指定分数区间内的成员,分数从高到低排序 |
17 | ZREVRANK key member 返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序 |
18 | ZSCORE key member 返回有序集中,成员的分数值 |
19 | ZUNIONSTORE destination numkeys key [key ...] 计算给定的一个或多个有序集的并集,并存储在新的 key 中 |
20 | ZSCAN key cursor [MATCH pattern] [COUNT count] 迭代有序集合中的元素(包括元素成员和元素分值) |