排行榜实战演练--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做排行榜真是太轻松了,简直就是为排行榜而生
还有一个问题,就是分数一样的时候,该怎么排序
理想的思路是: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得到真实分数
    • 季度和年,同理

代码和上面代码大同小异,只是思路上有所变化,所以就不重复贴代码了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值