排行榜实战演练--redis的zset实现
-
实时处理思路:
redis实现排行榜 zset
zset实现原理,跳跃表,自己说不好,百度谷歌吧
zset常用命令
先用小例子模拟一下,然后体验它的威力之后,请看后面实践演练 -
小例子
- 添加用户分数到zset中
zadd class_score 89 lisi
zadd class_score 60 zhangsan
zadd class_score 77 wangwu
zadd class_score 90 zhouliu
zadd class_score 70 zhengqi - 查询前3名
zrevrange class_score 0 2
zrevrange class_score 0 2 withscores - 查询所有排名
zrevrange class_score 0 -1
zrevrange class_score 0 -1 withscores - 更改分数,用的也是添加
zadd class_score 100 zhengqi - 继续前面查询命令
- 添加用户分数到zset中
由上小例子可知,zset做排行榜真是太轻松了,简直就是为排行榜而生
还有一个问题,就是分数一样的时候,该怎么排序
理想的思路是:id在前的排在前面,可自己测试一下,是后面添加的排在了前面,这个问题可以先想想,我后面会通过实践
说出自己的实现方式,不完美但满足公司需求,可以自己想其它的实现方式
需求:做一个律师排行榜,有律师的服务指数(分数),需要月榜,季度榜,年榜
分数计算规则:排行榜分数 = 服务时长(s) + 好评时 该订单服务时长(s) + 中评时 该订单服务时长(s) * 0.5 - 300s * 接单超时未接洽次数
实现效果图:
下面给出实现思路:
初始数据导入(定时任务,所有的律师导入redis中的3个key中)
定时任务:每个月初 “0 0 0 1* ?” 每月第一日0时执行,注意:新加key不清零,之前数据可以不用动,之后的业务还需要用,这里就相当于把redis当数据库用了
key的规则:month_rank_2019_1、quarter_rank_2019_1、year_rank_2019
月排行榜:月份计算,月初执行,新加key
季度排行榜:根据月份判断,遇到下个季度,新加key
第一季度:1月-3月
第二季度:4月-6月
第三季度:7月-9月
第四季度:10月-12月
实际计算的时机
散列在各个节点
节点位置:45星好评,3星中评
订单完成时--状态为4时
订单结束时--状态为5时(已评价)
订单超时未接洽--律师已接单的--状态为-2 --扣300s
-
难点解决,分数一样时,如何解决排名问题:
- 下面设计,为了达到相同分数,时间小的排在前面
- 时间以秒为单位转化为long的位数是10位,所以我用9999999999做为基数,10000000000做为乘数
- 带时间戳的分数 = 实际分数*10000000000 + (9999999999 – ctime)
- 初始导入 带时间戳的分数=9999999999 - ctime
- 实际分数 = (带时间戳的分数 - 9999999999 + ctime)/10000000000
- 根据功能需要,我们的分数这么算下之后不会超过long的最大值,所以可以这样使用。
- 如果这样的方式满足不了,可以用BigDecimal,或者double(实际分数.9999999999-ctime),这样组成double数据来排序
往后翻,往后翻,会有需求变动,设计实现方式又变了,核心不变
上面思路给的差不多了,下面贴出关键代码:
//上层业务逻辑处理
/**
* 初始化排行榜
* 这只是关键实现,如年、月、日自己实现
* redis的key上面有规则,这里就不贴出真实key了
* baseNum=9999999999
*
**/
private void initRank(boolean judge) {
int year = SysTimeUitl.getCurrentYear();
int month = SysTimeUitl.getCurrentMonth();
int day = SysTimeUitl.getCurrentDay();
int quarter = getQuarter(month);
boolean monthFlag = false;
boolean quarterFlag = false;
boolean yearFlag = false;
if (day==1)
monthFlag = true;
if (month==1 && day==1)
yearFlag = true;
if (day==1 && (month==1 || month==4 || month==7 || month==10))
quarterFlag = true;
//如果判断标识为true时,强制清零,相当于重新导入了一份新数据
if (judge){
monthFlag = true;
yearFlag = true;
quarterFlag = true;
}
String monthServiceKey = CacheUtil.getMonthServiceKey(year,month);
String quarterServiceKey = CacheUtil.getQuarterRankServiceKey(year,quarter);
String yearServiceKey = CacheUtil.getYearServiceKey(year);
String monthInviteKey = CacheUtil.getMonthInviteKey(year,month);
String quarterInviteKey = CacheUtil.getQuarterInviteKey(year,quarter);
Map<String,Object> map = new HashMap<String, Object>();
map.put("authstatus",0);
//服务排行榜,只查出认证通过的
List<Lawyer> lawyerList = lawyerDao.getLawyerList(map);
for (Lawyer lawyer:lawyerList ) {
long score = baseNum - lawyer.getCtime();
if (monthFlag){
//初始化月服务排行榜
CacheUtil.addZset(monthServiceKey,lawyer.getUid(),score);
}
if (quarterFlag){
//初始化季度服务排行榜
CacheUtil.addZset(quarterServiceKey,lawyer.getUid(),score);
}
if (yearFlag){
//初始化年服务排行榜
CacheUtil.addZset(yearServiceKey,lawyer.getUid(),score);
}
}
//邀请排行榜,查出所有
map.put("authstatus",null);
List<Lawyer> inviteLawyerList = lawyerDao.getLawyerList(map);
for (Lawyer lawyer:inviteLawyerList ) {
long score = baseNum - lawyer.getCtime();
if (monthFlag){
//初始化月邀请排行榜
CacheUtil.addZset(monthInviteKey,lawyer.getUid(),score);
}
if (quarterFlag){
//初始化季度邀请排行榜
CacheUtil.addZset(quarterInviteKey,lawyer.getUid(),score);
}
}
}
/**
* 服务月排行榜查询
* @param uid
* @return
*/
public RankingResult queryRankMonthServiceList(int uid) {
RankingResult rankingResult = new RankingResult();
int year = SysTimeUitl.getCurrentYear();
int month = SysTimeUitl.getCurrentMonth();
String monthRankServiceKey = CacheUtil.getMonthServiceKey(year,month);
//前100名的律师
List<LawyerVO> lawyerVOList = CacheUtil.getZset(monthRankServiceKey,baseNum,lawyerService);
//自已的排名
LawyerVO lawyerVO = CacheUtil.getSelfRank(monthRankServiceKey,uid,baseNum,lawyerService);
rankingResult.setRankingList(lawyerVOList);
rankingResult.setSelfRanking(lawyerVO);
return rankingResult;
}
//java处理redis的数据,底层实现
/**
* 向zset中添加数据
* @param key
* @param o
* @param score
*/
public static void addZset(String key,Object o,long score){
ZSetOperations<Serializable, Object> operations = redisTemplate.opsForZSet();
operations.add(key,o,score);
}
/**
* 获取前100排名
* multiplier=10000000000
* @return
*/
public static List<LawyerVO> getZset(String key, long baseNum, LawyerService lawyerService){
ZSetOperations<Serializable, Object> operations = redisTemplate.opsForZSet();
Set<ZSetOperations.TypedTuple<Object>> set = operations.reverseRangeWithScores(key,0,99);
List<LawyerVO> lawyerList = new ArrayList<LawyerVO>();
int i=1;
for (ZSetOperations.TypedTuple<Object> o:set){
int uid = (int) o.getValue();
LawyerCache lawyerCache = lawyerService.getLawyerCache(uid);
LawyerVO lawyerVO = lawyerCache.getLawyerVO();
long score = (o.getScore().longValue() - baseNum + lawyerVO.getCtime())/CommonUtil.multiplier;
lawyerVO.setScore(score);
lawyerVO.setRank(i);
lawyerList.add( lawyerVO);
i++;
}
return lawyerList;
}
/**
* 查出自己的排名
* @return
*/
public static LawyerVO getSelfRank(String key, int uid,long baseNum,LawyerService lawyerService) {
ZSetOperations<Serializable, Object> operations = redisTemplate.opsForZSet();
Long rank = operations.reverseRank(key,uid);
Double score = operations.score(key,uid);
LawyerCache lawyerCache = lawyerService.getLawyerCache(uid);
LawyerVO lawyerVO = lawyerCache.getLawyerVO();
lawyerVO.setRank(rank+1);
long scoreLong = (score.longValue() - baseNum + lawyerVO.getCtime())/CommonUtil.multiplier;
lawyerVO.setScore(scoreLong);
return lawyerVO;
}
/**
* 自己在zset中实现存的分数
* @return
*/
public static long getSelfScore(String key, int uid) {
ZSetOperations<Serializable, Object> operations = redisTemplate.opsForZSet();
Double score = operations.score(key,uid);
return score.longValue();
}
做到这里,本来就算完事了,奈何产品后续需求出了,当前实现方式无法满足需求
后续需求:下个月初的排行,要是上个月的排行顺序,分数为0,前端显示未服务
下面贴出我的实现方式二
- 实现方式二:新的需求,下个月,刚开始的榜单要呈现上个月榜单的排名,随后排名再慢慢发生变化
- 设计思路:实际分数① (初始排名因子②).(排名一样时的控制因子③),对应的double数据 aaaabb.cc(aaaa①bb②cc③),最后程序返回实际分数,在redis中存的是带时间戳的分数,以达到正常排名
- 如下设计方式:
- 带时间戳的分数 = 实际分数*100 + (9999999999 - ctime)/10000000000
- 实际分数 = ( 带时间戳的分数 - (9999999999 - ctime)/10000000000 )/100
- 下个月的初始导入数据:
- 带时间戳的分数 = (99…1) + (9999999999 - ctime)/10000000000(这里需要大家细想一下,应该很好理解)
- 如果刚开始的数据,还没有上个月,那就查库里的数据
- 这种实现方式,把影响相同分数排名的因素放到了小数点后面,把上个月排名挪到当月分数还不变的因子,设为100,保持排名不变,最后再除以100得到真实分数
- 季度和年,同理
代码和上面代码大同小异,只是思路上有所变化,所以就不重复贴代码了