目录
前言
难点
解决方案
前言
通常一个主播的活动榜单大概会分为几个流程来进行,例如可以分为海选赛,晋级赛,突围赛,年度10大主播,年度总决赛。
1.海选赛:从平台所有的主播中进行选拔,在海选赛比赛期间,前60可以晋级到下一轮比赛中,60名以外的主播被淘汰。
2.晋级赛:从海选赛晋级的60名主播中,继续持续几天比赛,最终在60名主播中的前40名主播晋级到下一轮比赛中。
3.突围赛:40晋级20。
4.年度10大主播:20名主播在比赛阶段中的前10晋级到总决赛成为年度10大主播。
5.总决赛:决出最终的冠亚季军。
其实每个赛段中还会有一些其他的逻辑,比如:复活和保送逻辑。从上个轮次中淘汰的主播可以通过复活赛进行复活,或者通过其他活动可以获得晋级赛的保送名额,由于本篇文章主要讲解榜单的晋级逻辑,所以这里就暂且先不详细说明复活和保送的逻辑。
难点
通常比赛都是以每天0点进行当天榜单的结算,如果一个赛段持续2天,那就是第二天的2点会进行榜单的结算,以N进60的海选赛为例:6号和7号两天的前60名晋级到下一轮,晋级榜单会在8号的0点进行结算。假如在7号要到结算时间的时候用户来冲榜,导致很多礼物堆积在队列中,会导致在8号0点那一刻不能马上结算出前60名的榜单(因为队列中的礼物可能会存在没消费完的情况),所以这里需要有个结算时间,这就是为什么很多直播平台每天榜单结算的时候会有结算倒计时的原因!这里假设我们结算是从8号的0点开处理结算逻辑,结算时间为10s,那也就是8号的0点到8号的0点0分10秒榜单会处在结算中的状态,但是这个时间段60进40的赛程已经开始,可是晋级的60名榜单要在10s之后才能结算出来,那么在结算中0s-10s中60晋级40的榜单要怎么生成?
解决方案
大概的榜单实现逻辑已经在上一篇Redis实现日榜|直播间榜单|排行榜|Redis实现日榜01中提到了,所以本篇主要介绍结算的逻辑和榜单合并的逻辑。
/**
* 榜单结算定时任务
*/
@Scheduled(cron = "8 0 0 * * ?")
@EeSingleEntry(key = "guild:match:rank_lock", expire = 2)
public void anchorRank() {
ActivityDTO activityDTO = activityTimeCache.getActivityDTO();
LocalDateTime now = LocalDateTime.now();
if (EeObjectUtil.isNotEmpty(activityDTO) && now.isAfter(activityDTO.getStartTime())) {
//处理日榜
EeAsyncUtil.runAsync(() -> {
//获取当前进行的轮次
RoundTimeEntity beforeRound = roundTimeService.getGuildBeforRound(now);
if (EeObjectUtil.isNotEmpty(beforeRound)) {
if (now.isAfter(beforeRound.getEndTime()) && now.isBefore(beforeRound.getEndTime().plusSeconds(ActivityBase.PROCESS_TS))) {
log.info("定时任务开始执行---now={}", EeDateUtil.format(now));
dealGuildMatchRankList(beforeRound);
}
}
}, executor);
}
}
/**
* 处理对应轮次的榜单结算
*/
public void dealGuildMatchRankList(RoundTimeEntity beforeRound) {
if (EeObjectUtil.isNotEmpty(beforeRound)) {
if (beforeRound.getRoundId() == ONE) {
//n进10
dealOneRankList(beforeRound);
} else if (beforeRound.getRoundId() == TWO) {
//n进5
dealTwoRankList(beforeRound);
} else if (beforeRound.getRoundId() == THREE) {
//8大公会
dealThreeList(beforeRound);
} else if (beforeRound.getRoundId() == FOUR) {
//总决赛
dealFinalList(beforeRound);
}
}
}
进行N进60的结算的时候,判断榜单是否锁定,如果没锁定就进行结算,榜单锁定后,礼物队列再次受到前一日的礼物消息的时候,会进行丢弃处理,然后处理紧急榜单和初始化下一轮的榜单,最后结算完成后进行榜单的合并处理。
/**
* N进60
*/
private void dealOneRankList(RoundTimeEntity beforeRound) {
//获取榜单的缓存key
String matchRoundListName = guildMatchValueBusiness.getGuildMatchRoundListCacheName(beforeRound.getRoundId());
//判断榜单是否已经锁定
if (!guildMatchValueBusiness.checkGuildBlockRankList(matchRoundListName)) {
//先锁定榜单
guildMatchValueBusiness.setGuildBlockRankList(matchRoundListName);
//获取榜单中前60排名
List<AssociationListDTO> rankList = guildMatchValueBusiness.getAssociationList(matchRoundListName, 0, 59);
if (CollectionUtils.isNotEmpty(rankList)) {
for (AssociationListDTO association : rankList) {
//插入下一个轮次的晋级榜单,这里主要是用于存储晋级的公会,没实际作用
guildMatchValueBusiness.setPromotionList(TWO, association.getAssociationId());
//初始化下一个轮次的晋级榜单,这个才是真正的榜单
guildMatchValueBusiness.initializationNexDayList(String.valueOf(association.getAssociationId()), 3, BigDecimal.ZERO);
//发送晋级弹窗
sendPromotionPopUps(association.getAssociationId(), association.getRank(), beforeRound.getRoundId());
}
//发送未晋级弹窗
sendNoPromotionPopUps(beforeRound.getRoundId(), rankList.stream().map(AssociationListDTO::getAssociationId).collect(Collectors.toList()));
}
//处理完晋级名单后锁定临时榜单,并且把临时榜单数据copy到正式榜单中
guildMatchValueBusiness.setTemporaryBlockRankList(beforeRound.getRoundId() + 1);
}
}
这里处理榜单积分的时候,和简单日榜有点区别,判断如果当前是结算时间的时候,要把所有用户的分支都加到临时榜单中
/**
* 处理榜单积分
*/
public boolean doTwoProcessor(BigDecimal value, Integer associationId, RoundTimeEntity nowRound, String anchorId, LocalDateTime eventTime) {
//在结算时间之内 没有锁定要增加榜单数据到临时榜单中,为了兼容0-10s结算中,晋级名单还没出来的情况
if (isOrNotAddTemporaryRoundList(nowRound)) {
incrTemporaryRoundListValue(associationId.toString(), value.doubleValue(), nowRound.getRoundId(), anchorId);
} else {
//如果公会没晋级不需要处理
if (!checkPromotion(nowRound.getRoundId(), associationId)) {
log.warn("第三轮当前公会已经淘汰roundId={},associationId={}", nowRound.getRoundId(), associationId);
return false;
}
//增加公会赛榜单积分
incrRoundListValue(associationId.toString(), value.doubleValue(), nowRound.getRoundId(), anchorId, eventTime);
}
return true;
}
结算完成后,合并临时榜单到正式榜单的逻辑
/**
* 锁定临时榜单,数据写入正常日榜中
*/
public void setTemporaryBlockRankList(int roundId) {
//锁定榜单后把临时榜单中的内容复制到正式榜单中
log.warn("插入晋级名单成功now={},roundId={},region={},groupId={}", EeDateUtil.format(LocalDateTime.now()), roundId);
//临时榜单key
String temporaryName = getTemporaryRoundListCacheName(roundId);
redisTemplate.opsForHash().put(GUILD_TEMPORARY_LIST_BLOCK, temporaryName, EeDateUtil.format(LocalDateTime.now()));
//今日榜单的key
String todayName = getGuildMatchRoundListCacheName(roundId);
//获取到已经初始化好的晋级榜单
Set<ZSetOperations.TypedTuple<String>> rankByRangeWithScores = getRankByRangeWithScores(todayName, 0, -1);
if (CollectionUtils.isNotEmpty(rankByRangeWithScores)) {
for (ZSetOperations.TypedTuple data : rankByRangeWithScores) {
//获取到临时榜单中的值,incr到正式榜单中
Double score = redisTemplate.opsForZSet().score(temporaryName, data.getValue().toString());
if (EeObjectUtil.isNotEmpty(score)) {
//合并今日榜单
redisTemplate.opsForZSet().incrementScore(todayName, data.getValue().toString(), score);
}
}
}
}
晋级榜单最重要的就是在每天日榜结算的过程中会出现第二天榜单还没结算出来的情况,所以在结算期间还是要计算所有人的一个临时榜单,最后结算完成后,把临时榜单中已经晋级的主播分数合并到正常榜单中就可以了。