自然时间和滚动时间
需求:直播间 基于 最近一周 打赏的 金额 来排序
自然时间和滚动时间最明显的区别就是,当下一周开始时自然时间排序会因为数据需要重新统计导致没有可以排序的数据。而滚动时间则会基于当前时间往前推一周,始终基于一周的数据来排序。
滚动时间实现
实现方式涉及两个桶的概念:
- 过程桶 Hash:用于存储某个时间片产生的数据,过程桶的数量根据排序结果可容忍的时间误差来调整。
- 结果桶 ZSet:始终保持时间窗口内的数据,查询时直接查结果桶。
滚动时间有两种常规实现方式,通常我们为了实时都会采用提前汇总的方式。
提前汇总结果,定期抛弃旧数据。优点是数据实时,查询时只需要去结果桶取就行了。缺点是需要定时去滚桶删减旧数据来保证结果桶的数据处于时间范围内。
当产生消费时,我们将数据同时存储到 过程桶和结果桶,结果桶始终保持着全量的数据。所以我们需要定期去滚桶,丢弃掉过时的旧数据,大致实现如下。
String KEY_PROCESS_BUCKET = "SCROLL_RANK:PROCESS:%s:%s"; // 过程桶
String KEY_RESULT_BUCKET = "SCROLL_RANK:RESULT:%s"; // 结果桶
String RANK_ROOM_CONSUMER = "ROOM_CONSUMER"; // 房间消费排行榜
/**
* 消费监听
*
* 最近 7 天的排序,每6个小时一个过程桶,总共 7 * 24 / 6 个过程桶。
* 容忍 6 个小时的数据误差: 定时器每六个小时删除上周同时间片产生的数据。
*/
public void consumerListener(String roomId, Number score) {
// 过程桶
redissonClient.getMap(
String.format(KEY_PROCESS_BUCKET, RANK_ROOM_CONSUMER, hourOfWeek()),
StringCodec.INSTANCE)
.addAndGet(roomId, score);
// 结果桶
redissonClient.getScoredSortedSet(
String.format(KEY_RESULT_BUCKET, RANK_ROOM_CONSUMER),
StringCodec.INSTANCE)
.addScore(roomId, score)
}
/**
* 现在是这周的第几小时?
*/
private int hourOfWeek() {
LocalDateTime now = LocalDateTime.now();
// 每周从周日开始
// return (now.get(WeekFields.SUNDAY_START.dayOfWeek()) - 1) * 24 + now.getHour();
return (now.getDayOfWeek().getValue() - 1) * 24 + now.getHour();
}
/**
* 定时任务六小时执行一次, 丢弃过时数据
*/
public void discardOutdatedData() {
// 上周当前时间片的数据
RMap<String, String> outdated = redissonClient.getMap(
String.format(KEY_PROCESS_BUCKET, RANK_ROOM_CONSUMER, hourOfWeek()),
StringCodec.INSTANCE);
RScoredSortedSet<String> scoredSortedSet = redissonClient.getScoredSortedSet(
String.format(KEY_RESULT_BUCKET, rank.name()),
StringCodec.INSTANCE);
outdated.forEach((k, v) -> {
// 减去过时数据
Double old = Double.parseDouble(v) * -1;
scoredSortedSet.addScore(k, old);
// 过程桶减去旧数据 尽量保证丢弃过程中进来的新数据不受影响
outdated.addAndGet(k, old);
});
}
定时汇总
大致实现思路同上,区别在于监听到消费时只往过程桶加,而后定时器的工作则是定时汇总一次数据到结果桶,牺牲不必要的实时性,减轻服务器资源消耗。
这种方式适用于时间范围较大的场景,例如最近一个月的排序,可以容忍较大的数据误差,数据量大,用户对排行榜实时性感知不大,可以每天定时汇总一次。
多权重场景
二进制拆分法按bit分割权重
多权重场景
算好值再加进去即可