Redis实战(9)-SortedSet实战之再谈游戏充值排行榜(如何处理历史与异常的充值记录)

作者: 修罗debug
版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。



摘要:每当我们谈起缓存中间件Redis的应用场景时,我们一般都会根据其数据结构联想到对应的应用场景,有序集合SortedSet也不例外,“排行榜”一直都是与其紧密挂钩、不得不谈的其中一种实战场景!本文我们将继续再谈“游戏充值排行榜”,介绍如何去处理历史已经存在的充值记录 或者 在将充值记录塞入缓存Cache失败时如何开启后续的补偿处理措施!

内容:在上篇文章中,我们已经给各位小伙伴介绍了如何基于Spring Boot2.0 + 缓存Redis的SortedSet以实际的代码实战一种典型的业务场景“游戏充值排行榜”,在文中我们介绍了这一业务场景两大典型的核心功能模块,即“用户充值”、“获取充值排行榜”,各位小伙伴可以自行前往回顾!

然而,这世间本就没有十全十美之物,“游戏充值排行榜”这一业务场景也不例外,虽然我们基本上已经实现了该业务场景几乎所有的功能模块,但是我们却忽略了其他两种情况:

A.如果“充值排行榜”这一功能模块是增量式的需求,那么上线时如何去处理历史的用户充值记录呢?你总不能说我们的“充值排行榜”对于以往充值的用户记录不生效吧?(那样岂不令人笑掉大牙!)

B.虽然我们的代码看似完美,但是要知道Bug是无处不在的,这些Bug有的是能一眼被洞穿的,也有的是后知后觉的,“用户充值的过程”便是如此,如果用户充值后插入数据库DB成功、但是插入缓存Cache失败(DB事务不回滚的前提),那毫无疑问,最终得出来的“充值排行榜”一定是不准确的(因为我们是直接从缓存Redis中获取的)!

带着这两大问题,我们给大家提供了一种并非十全十美的,但是却能保证“最终一致性”的充值排行榜的解决方案,那就是万能的定时任务调度

既然是定时任务调度,那么这个定时任务是做啥的呢?没错,它要完成的任务就是开启一个定时时钟,基于数据库DB中的“用户充值记录表”,借助数据库提供的Order By、Group By等查询得出目前为止所有有效用户的“充值排行榜”,下面我们以实际的代码进行实战。

(1)直接建立一个定时任务调度类PhoneFareScheduler,并开发相应的方法实现具体的定时任务逻辑,其完整源代码如下所示:

/**补偿机制:手机号码充值排行榜
* @Author:debug (SteadyJack)
* @Link: weixin-> debug0868 qq-> 1948831260**/
@Component
public class PhoneFareScheduler {
private static final Logger log= LoggerFactory.getLogger(PhoneFareScheduler.class);

@Autowired
private PhoneFareMapper phoneFareMapper;

@Autowired
private RedisTemplate redisTemplate;
//时间频度设定为30min,当然啦,具体的设定要根据实际情况而定
@Scheduled(cron = "0 0/30 * * * ?")
public void sortFareScheduler(){
log.info("--补偿性手机号码充值排行榜-定时任务");

this.cacheSortResult();
}

@Async("threadPoolTaskExecutor")
private void cacheSortResult(){
try {
ZSetOperations<String,FareDto> zSetOperations=redisTemplate.opsForZSet();

List<PhoneFare> list=phoneFareMapper.getAllSortFares();
if (list!=null && !list.isEmpty()){
redisTemplate.delete(Constant.RedisSortedSetKey2);

list.forEach(fare -> {
FareDto dto=new FareDto(fare.getPhone());
zSetOperations.add(Constant.RedisSortedSetKey2,dto,fare.getFare().doubleValue());
});
}
}catch (Exception e){
log.error("--补偿性手机号码充值排行榜-定时任务-发生异常:",e.fillInStackTrace());
}
}
}

值得一提的是,在该定时任务调度中我们设定的时间频率为 每30min进行执行一次任务,实现“充值排行榜”的大洗盘!也就是说,如果前端“排行榜”页面数据出现差错,那么其恢复正确的等待时间是30min(因为我们的定时任务就是前往数据库DB,查询获取得到排行榜,当然啦,其前提是保证DB中的数据是正确无误的!)

(2)其中,phoneFareMapper.getAllSortFares() 的作用就是前往数据库Mysql,通过Group By、Order By和SUM等查询得到排行榜,其完整的动态SQL如下所示:

  <!--基于数据库的补偿排名机制-->
<select id="getAllSortFares" resultType="com.boot.debug.redis.model.entity.PhoneFare">
SELECT
phone,
SUM(fare) AS fare
FROM
phone_fare
GROUP BY
phone
ORDER BY
fare DESC
</select>

除此之外,@Async("threadPoolTaskExecutor") 的作用便是采用“线程池-多线程的方式异步执行定时任务”,故而我们需要作一个全局的Config,用于配置线程池-多线程的相关信息:  

/**线程池-多线程配置
* @Author:debug (SteadyJack)
* @Link: weixin-> debug0868 qq-> 1948831260**/
public class ThreadConfig {
@Bean("threadPoolTaskExecutor")
public Executor threadPoolTaskExecutor(){
ThreadPoolTaskExecutor executor=new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setKeepAliveSeconds(10);
executor.setQueueCapacity(8);

executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}

至此,我们已经撸完了“游戏充值排行榜”这一完整业务的“补偿机制”功能代码了,在测试之前,我们先“偷偷”在数据库表phone_fare中新增几条充值记录,代表“以前存在的历史充值记录”或者“插入DB成功,但插入缓存失败的充值记录”,如下图所示:


最后我们基于Postman测试一波吧,下面一张图足以说明一切了:


好了,本篇文章我们就介绍到这里了,建议各位小伙伴一定要照着文章提供的样例代码撸一撸,只有撸过才能知道这玩意是咋用的,否则就成了“空谈者”!

对Redis相关技术栈以及实际应用场景实战感兴趣的小伙伴可以前往Debug搭建的技术社区的课程中心进行学习观看:https://www.fightjava.com/web/index/course/detail/12

其他相关的技术,感兴趣的小伙伴可以关注底部Debug的技术公众号,或者加Debug的微信,拉你进“微信版”的真正技术交流群!一起学习、共同成长!

补充:

1、本文涉及到的相关的源代码可以到此地址,check出来进行查看学习:

https://gitee.com/steadyjack/SpringBootRedis

2、目前Debug已将本文所涉及的内容整理录制成视频教程,感兴趣的小伙伴可以前往观看学习:https://www.fightjava.com/web/index/course/detail/12

3、关注一下Debug的技术微信公众号,最新的技术文章、课程以及技术专栏将会第一时间在公众号发布哦!