当前位置:   article > 正文

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

redis 统计某时间段订单数

一、需求

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 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110

四、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;
	}
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114

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小时
    };
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67

五、zset的扩展

  1. zset做延迟队列

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

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

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/201028
推荐阅读
相关标签
  

闽ICP备14008679号