一、需求
C端App上,用户是否能在某个时间段内选择配送,需要在后台实时统计每个时段(每个小时或每半个小时)的订单数,并考虑当前仓内拣货打包和仓外配送的压力请求,决定用户可以选择的配送时段。
二、方案
使用redis的zset (sorted set )数据结构进行实时统计分析。
redis 有序集合zset和集合set一样也是string类型元素的集合,且不允许重复的成员。不同的是 zset 的每个元素都会关联一个分数(分数可以重复),redis 通过分数来为集合中 的成员进行从小到大的排序。
每天每个仓使用一个zset的key存储,orderId作为zset的member,订单对应的预计完成时间戳作为zset的score;
当用户下一个订单时,就记录这个orderId,及订单对应的预计完成时间戳 (zadd)。
当订单取消、或配送完成是,就从这个zset中删除这个订单(zrem)。
查询时间段内的订单数,只需要把时间段的起止时间戳作为zset的score,使用zcount即可统计订单数
三、zset测试
127.0.0.1:6379>
# 1.添加,如果值存在添加,将会重新排序。zadd
127.0.0.1:6379> zadd user 1 wuxiaolong
(integer) 1
127.0.0.1:6379> zadd user 10 sukailiang 8 yufeng 5 shilei 6 huangchao
(integer) 4
127.0.0.1:6379>
# 2.查看zset集合的成员个数。zcard
127.0.0.1:6379> zcard user
(integer) 5
127.0.0.1:6379>
# 3.查看Zset指定范围的成员,withscores为输出结果带分数。zrange
127.0.0.1:6379> zrange user 3 8
1) "yufeng"
2) "sukailiang"
127.0.0.1:6379> zrange user 3 8 withscores
1) "yufeng"
2) "8"
3) "sukailiang"
4) "10"
127.0.0.1:6379> zrange user 0 -1 withscores
1) "wuxiaolong"
2) "1"
3) "shilei"
4) "5"
5) "huangchao"
6) "6"
7) "yufeng"
8) "8"
9) "sukailiang"
10) "10"
# 4.获取zset成员的下标位置,如果值不存在返回null。zrank
127.0.0.1:6379> zrank user yufeng
(integer) 3
127.0.0.1:6379> zrank user wuxiaolong
(integer) 0
127.0.0.1:6379> zrank user nobody
(nil)
127.0.0.1:6379>
# 5.获取zset集合指定分数之间存在的成员个数。zcount
127.0.0.1:6379> zcount user 3 8
(integer) 3
127.0.0.1:6379> zcount user 5 8
(integer) 3
# 6.删除指定的一个成员或多个成员。zrem
127.0.0.1:6379> zrem user yufeng
(integer) 1
127.0.0.1:6379> zrem user nobody
(integer) 0
127.0.0.1:6379>
# 7.获取指定值的分数。zscore
127.0.0.1:6379> zscore user wuxiaolong
"1"
127.0.0.1:6379> zscore user nobody
(nil)
127.0.0.1:6379> zscore user shilei
"5"
127.0.0.1:6379>
# 8.给指定元素的分数进行增减操作,负值为减,正值为加。zincrby
127.0.0.1:6379>
127.0.0.1:6379> zincrby user 3 shilei
"8"
127.0.0.1:6379> zscore user shilei
"8"
127.0.0.1:6379>
127.0.0.1:6379> zrange user 0 -1 withscores
1) "wuxiaolong"
2) "1"
3) "huangchao"
4) "6"
5) "shilei"
6) "8"
7) "sukailiang"
8) "10"
127.0.0.1:6379>
127.0.0.1:6379>
# 9.根据指定分数的范围获取值。zrangebysocre
127.0.0.1:6379> zrangebyscore user 0 8
1) "wuxiaolong"
2) "huangchao"
3) "shilei"
127.0.0.1:6379>
# 10.倒序,从高到底排序输出指定范围的数据。zrevrange,zrevrangebyscore
127.0.0.1:6379> zrevrange user 0 -1
1) "sukailiang"
2) "shilei"
3) "huangchao"
4) "wuxiaolong"
127.0.0.1:6379> zrevrange user 0 -1 withscores
1) "sukailiang"
2) "10"
3) "shilei"
4) "8"
5) "huangchao"
6) "6"
7) "wuxiaolong"
8) "1"
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379>
# 11.根据坐标,分数范围删除数据。zremrangebyscore,zremrangebyrank
127.0.0.1:6379> zrevrangebyscore user 8 1
1) "shilei"
2) "huangchao"
3) "wuxiaolong"
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379>
# 12.zset还有求两个集合的交集和并集的操作。zunionzstore,zinterstore
四、JAVA测试
1.controller
package message.queue.engine.controller;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Calendar;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping(value = "/v1")
public class HelloController {
@Autowired
private ZSetOperations<String, Object> zSetOperations;
@RequestMapping("/add")
public Boolean add(@RequestParam("orderId") String orderId){
// 履约时间
Date deliveryDate = new Date();
// 履约日期
String ymd = Utils.getYMD(deliveryDate);
// 每天一个key
String key = Const.STAT_PREFIX+ ymd;
// 放入订单号 重复orderId会覆盖
Boolean result = zSetOperations.add(key, orderId, deliveryDate.getTime());
// 存储一周
if(result){
Boolean expire = zSetOperations.getOperations().expire(key, Const.RDS_EXPIRE_DAY, TimeUnit.DAYS);
}
return result;
}
@RequestMapping("/del")
public Long del(@RequestParam("orderId") String orderId){
// 履约时间
Date deliveryDate = new Date();
// 履约日期
String ymd = Utils.getYMD(deliveryDate);
// 每天一个key
String key = Const.STAT_PREFIX+ ymd;
// 删除
Long remove = zSetOperations.remove(key, orderId);
return remove;
}
@RequestMapping("/stat")
public Long stat(){
// 当前时间
Date now = new Date();
// 时间段长度 以30分钟划分一天的时段
int segmentType = 30;
// 履约日期
String ymd = Utils.getYMD(now);
// 每天一个key
String key = Const.STAT_PREFIX+ ymd;
// 今天开始
Calendar calendar = Calendar.getInstance();
calendar.setTime(now);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
Date todayStart = calendar.getTime();
// 相对时间段
Long relativeTime = now.getTime() - todayStart.getTime();
long[] segmentArr;
if(segmentType ==30){
segmentArr = Const.TIME_SEGMENT_30;
}else {
segmentArr = Const.TIME_SEGMENT_60;
}
Long start = null;
Long end = null;
for (int i=0; i< segmentArr.length-1; i++){
if(relativeTime >= segmentArr[i] && relativeTime <= segmentArr[i+1]){
start = segmentArr[i] + todayStart.getTime();
end = segmentArr[i+1] + todayStart.getTime();
}
}
// todo start=null
// 根据时间段统计订单数量
Long count = zSetOperations.count(key, start, end);
System.out.println(start +" " + end +" " + count);
return count;
}
}
2.Constant
package or.message.queue.engine.constant;
public class Const {
public static final String STAT_PREFIX = "eta:order:stat:zset:";
public static final String ORDER_PREFIX = "eta:order:";
public static final Integer RDS_EXPIRE_DAY = 7;
public static final long[] TIME_SEGMENT_30 = {
0,
1800000, 3600000,
5400000, 7200000,
9000000, 10800000,
12600000, 14400000,
16200000, 18000000, // 6小时
19800000, 21600000,
23400000, 25200000,
27000000, 28800000,
30600000, 32400000,
34200000, 36000000, // 10小时
37800000, 39600000,
41400000, 43200000, // 12
45000000, 46800000,
48600000, 50400000,
52200000, 54000000,
55800000, 57600000,
59400000, 61200000,
63000000, 64800000, // 18
66600000, 68400000,
70200000, 72000000, // 20小时
73800000, 75600000,
77400000, 79200000,
81000000, 82800000,
84600000, 86400000, // 24小时
};
public static final long[] TIME_SEGMENT_60 = {
0,
3600000,
7200000,
10800000,
14400000,
18000000, // 6小时
21600000,
25200000,
28800000,
32400000,
36000000, // 10小时
39600000,
43200000, // 12
46800000,
50400000,
54000000,
57600000,
61200000,
64800000, // 18
68400000,
72000000, // 20小时
75600000,
79200000,
82800000,
86400000, // 24小时
};
}
五、zset的扩展
-
zset做延迟队列
zset 会按 score 进行排序,如果 score 代表想要执行时间的时间戳。在某个时间将它插入 zset 集合中,它变会按照时间戳大小进行排序,也就是对执行时间前后进行排序。
起一个死循环线程不断地进行取第一个 key 值,如果当前时间戳大于等于该 key 值的 score 就将它取出来进行消费删除,可以达到延时执行的目的
-
排行榜
对 “1小时最热门” 这类榜单,如果记录在数据库中,不太容易对实时统计数据做区分。我们以当前小时的时间戳作为 zset 的 key,把贴子ID 作为 member ,点击数评论数等作为 score,当 score 发生变化时更新 score。利用 ZREVRANGE 或者 ZRANGE 查到对应数量的记录。