按时段实时统计订单数(基于redis的zset)

一、需求

C端App上,用户是否能在某个时间段内选择配送,需要在后台实时统计每个时段(每个小时或每半个小时)的订单数,并考虑当前仓内拣货打包和仓外配送的压力请求,决定用户可以选择的配送时段。

image-20211029170020052

二、方案

使用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的扩展

  1. zset做延迟队列

    zset 会按 score 进行排序,如果 score 代表想要执行时间的时间戳。在某个时间将它插入 zset 集合中,它变会按照时间戳大小进行排序,也就是对执行时间前后进行排序。

    起一个死循环线程不断地进行取第一个 key 值,如果当前时间戳大于等于该 key 值的 score 就将它取出来进行消费删除,可以达到延时执行的目的

  2. 排行榜
    对 “1小时最热门” 这类榜单,如果记录在数据库中,不太容易对实时统计数据做区分。我们以当前小时的时间戳作为 zset 的 key,把贴子ID 作为 member ,点击数评论数等作为 score,当 score 发生变化时更新 score。利用 ZREVRANGE 或者 ZRANGE 查到对应数量的记录。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值