[size=large]zset是一个排序集合,我主要用来给用户进行排名,以及对一个指定区间的数据进行统计,可以用来替代mysql中between and语句 ,列举几个场景如何利用zset解决需求
业务场景:用户每天都有刷牙数据产生,刷牙数据包括刷牙时长,刷牙时间,刷牙分数

需求1 : 根据每天每个用户的最高分数进行排名
需求2:运营活动根据每天每个用户的最高分数进行排名,如果分数一样则根据坚持天数进行排名
需求3:统计近7天单个用户是刷牙数据,并且显示给用户刷牙平均分,刷牙时长平均分,有多少天没有坚持每天刷牙两次。

需求1:根据每天每个用户的最高分数进行排名
每天新建一个zset,当有用户刷牙数据过来,则根据用户的id在zset中通过zscore命令查找当前用户是否存在记录,如果不存在 当前用户记录,则直接用zadd命令添加新记录,value为用户id,score为刷牙分数 ,如果存在记录,则比较当前记录的分数与zset中的记录哪个更大,把大的score插入到当前用户id所对应的zset中即可。
当用户查看刷牙记录排行时,直接通过zRevRangeWithScores命令即可根据分数从大到小的显示用户的排行情况。如果希望知道某个用户的排名,则可根据用户id,查找出用户分数,通过zset.count(zsetkey, userScore + 0.01, topScore) + 1来算出用户的排名,这个逻辑是给用户的分数加一个0.01,然后计算出最大值与当前值+0.01之间有多少个用户,得到的人数+1即为用户名次。 此条语句为java中的写法,redis中应该使用zcount

需求2:运营活动根据每天每个用户的最高分数进行排名,如果分数一样则根据坚持天数进行排名
兼顾分数与天数
对于计算坚持天数,我们需要利用redis的set来完成,当用户添加一条刷牙记录时,使用一个set来记录用户的坚持日期,比如20161027 这样,把这个记录加入到以用户id为组合key的set中,然后通过set的scard命令(springdata中是size)来计算用户的坚持天数。这样做的好处:1.set可以过滤重复的日期 ,每条记录过来只要无脑插入即可 2:如果某运营活动需要统计某个时间段内的用户坚持天数,可新建一个set存储运营活动的所有日期,然后使用set的交集运算intersect来算出交集,并统计元素个数,得出的数据即为运营活动总用户的坚持天数。
得到坚持天数insistday后,对应需求1中用户的刷牙分数,我们做一个扩大化,比如*1000,前提是用户的坚持天数不会超过999,每次插入用户数据到zset的时候,把用户的分数score*1000+insistday。这样通过zRevRangeWithScores命令既可得到根据用户分数排名,再根据坚持天数排名的排行榜

需求3:统计近7天单个用户是刷牙数据,并且显示给用户刷牙平均分,刷牙时长平均分,有多少天没有坚持每天刷牙两次。
思路同需求2,在zset的score上做文章。把score做成一个同时记录日期,分数,时长的double型数据,分成16位 :前10位把时间转化成yyMMddHHmm的字符型,中间三位记录刷牙分数 000,注意务必占满3位,不够的话用0替代,最后三位记录坚持时长,三位数字转化成字符串表示,最后将这10+3+3个字符串拼装起来,转化为double型,记录到每个用户的zset中即可。
这样当统计7天的数据时,只有算出第一天和第七天的日期,将其转化为begindate:yyMMdd0000000000 endDate:yyMMdd2359999999,利用zset的rangeByScoreWithScores命令
zset.zRangeByScoreWithScores(zsetkey,begindate, mendDateax)即可得到中间的所有数据[/size]
 

private Set<TypedTuple<Object>> getUserRecordBetweenDate(Integer userId,
			Date beginDate, Date endDate) {
		String beginStr = DateUtil.dateToStr(beginDate, "yyMMdd");
		String endStr = DateUtil.dateToStr(endDate, "yyMMdd");
		StringBuffer sbbegin = new StringBuffer();
		sbbegin.append(beginStr);
		sbbegin.append("0000");// 时分
		sbbegin.append("000");// 分数
		sbbegin.append("000");// 刷牙时间
		Double min = Double.valueOf(sbbegin.toString());
		StringBuffer sbend = new StringBuffer();
		sbend.append(endStr);
		sbend.append("2359");
		sbend.append("100");
		sbend.append("999");
		Double max = Double.valueOf(sbend.toString());
		String zsetkey = Constant.RedisZsetEnum.USERALLRECORD.getKey() + userId;
		ZSetOperations<String, Object> zset = redisService
				.getZsetRedisTemplate();
		Set<TypedTuple<Object>> userZset = zset.rangeByScoreWithScores(zsetkey,
				min, max);
return userZset;


[size=large]然后对score转化成string型,利用substring(10,13),substring(13,16)得到刷牙分数和刷牙时长,对其求和,再除以条数即位平均数。
对应没有坚持两次的天数统计:循环遍历开始日期到结束日期组成的列表,利用rangeByScoreWithScores命令把begindate与endDate中的yyMMdd换成那一天的日期即可,如果得到的结果个数<2则将统计的天数+1

需要注意的问题:
1.在zset中声明的类型与调用的类型一定要统一。比如声明的zset中value的值为String,那么如果调用的时候哪怕你知道用户id为Integer型12345,也要将其转化为String才能调用,否则是得不到数据的

2.在需求三中实际上有个漏洞,如果数据是当天23:59之后的时间,其实最后的秒数是被忽略了。一个是业务的考虑,不需要太顾及,另外一个就是在zset的score中,对于double型的数据,如果太长,超过了17位,则其采用科学计数法记录的数据会有误差,后面的数据就记不住了,对于我们的需求中,需要一个数据同时完成时间,得分,时长的记录,所以就对score的形式进行了缩减,把年的前两位减掉,把日期的秒数减掉,以保证16位的长度。另外在获取score的时候,得到的是科学计数法的数据,我通过这段代码将其转化为string[/size]

public static String getStringFromDouble(Double d){
		DecimalFormat format = new DecimalFormat("#");
		String a = format.format(d);
		return a;
	}[size=large][/size]
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐