Redis实战(8)-SortedSet典型应用场景实战之游戏充值排行榜

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



摘要:缓存中间件Redis的数据结构~有序集合SortedSet在实际项目开发中还是比较常见的,特别是在一些诸如“排行榜”的业务场景更是经常可以见到其身影!本文我们将以项目中实际的业务场景“游戏充值排行榜”为案例,一起来践行有序集合SortedSet的“有序 + 唯一”的特性,感受感受其在实际项目中是如何得到应用的!

内容:“排行榜”,通俗地讲,就是一份榜单,我们小时候每次考试之后学校贴出来的成绩榜其实就是“排行榜”的一种。顾名思义就是将某些对象/实体,比如“某个人”、“某个手机号”按照某个值“从大排到小”、“从高排到低”或者“从小到排到大”、“从低排到高”而出来的一种结果。

站在程序的角度上看,“排行榜”亦可以说是某种“排序算法”运行出来的结果,典型、常见的业务场景包括:手机充值排行榜、商城积分排行榜、游戏充值排行榜等等…其最终的效果如下图所示:


由于“排行榜”涉及到“排名”,故而在“放榜”的那一刻,会有很多小伙伴一拥而上前往观看,这就类似于在某一瞬间,许许多多、并发产生的线程 请求 查看“排行榜”,而排行榜的数据一般是存储在DB数据库中的,如果每个请求过来时都走一遍数据库查询、排序,那无疑是需要付出很大的代价的,比如最为明显的就是某一瞬间DB负载会变高、压力变大,更夸张的可能会压垮DB。

因此,我们将想办法将那些跟排行榜相关的业务数据转移到缓存Cache中,并在缓存中实现业务数据的排行,最终将得到的排行榜返回给到每个发起请求的用户!

在这里我们使用的缓存Cache便是Redis,并使用其中的数据结构:有序集合SortedSet加以实现!SortedSet这种数据结构延伸了集合Set的“元素唯一/不重复”的特性,却额外增添了不同于集合Set的另外一个特性:“有序性”,正是这个“有序性”,才使得我们的“排行榜”业务可以得到很好的实现!

值得一提的是,有序集合SortedSet “有序性”的实现是通过 “在添加成员时附带一个double类型的参数:分数”实现的,在接下来的代码实战中,各位小伙伴将会看到这个“分数”参数的无穷魅力!

接下来我们以“游戏充值排行榜”为案例,一起来践行有序集合SortedSet在实际业务场景的应用。对于“游戏充值排行榜”这一业务而言,无非包含两个核心模块,一个用户充值模块,一个是用户获取排行榜模块!下面我们将重点来介绍并实战这两大核心功能模块

一、用户游戏充值模块

对于用户充值模块,玩过游戏的小伙伴估计都晓得其大概的业务流程,其实无非就是输入手机号/游戏账号以及金额,然后点击支付即完成充值的整个过程,如下图所示为该模块的核心业务流程图:


下面,我们进入代码实战环节!

(1)同样的道理,工欲善其事,必先利其器,我们先建立一张用于记录 用户历史充值记录的“用户充值表”,其DDL如下所示:

CREATE TABLE `phone_fare` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`phone` varchar(50) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '手机号码',
`fare` decimal(10,2) DEFAULT NULL COMMENT '充值金额',
`is_active` tinyint(4) DEFAULT '1' COMMENT '是否有效(1=是;0=否)',
PRIMARY KEY (`id`),
KEY `idx_phone` (`phone`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='手机充值记录';

采用Mybatis逆向工程或者代码生成器生成该数据库表的实体类Entity、Mapper操作接口以及对应的用于写动态SQL的Mapper.xml,在这里就不贴出来了,各位小伙伴可以前往文末提供的源码地址进行下载观看!

(2)紧接着我们需要开发一个SortedSetController,用于前端用户发起“充值”的请求,其完整的源代码如下所示:

/**@Author:debug (SteadyJack)  weixin-> debug0868 qq-> 1948831260
**/
@RestController
@RequestMapping("sorted/set")
public class SortedSetController extends AbstractController {

@Autowired
private SortedSetService sortedSetService;

@RequestMapping(value = "put/v2",method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
public BaseResponse putv2(@RequestBody @Validated PhoneFare fare, BindingResult result){
String checkRes= ValidatorUtil.checkResult(result);
if (StrUtil.isNotBlank(checkRes)){
return new BaseResponse(StatusCode.Fail.getCode(),checkRes);
}
BaseResponse response=new BaseResponse(StatusCode.Success);
try {
response.setData(sortedSetService.addRecordV2(fare));
}catch (Exception e){
response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
}
return response;
}
}

其中,实体类PhoneFare的代码如下所示:  

@Data
@EqualsAndHashCode
public class PhoneFare implements Serializable {
private Integer id;

@NotBlank(message = "手机号码不能为空!")
private String phone;

@NotNull(message = "充值金额不能为空!")
private BigDecimal fare;

private Byte isActive = 1;
}

(3)而sortedSetService.addRecordV2(fare) 要做的事情就是“如何将前端用户提交过来的手机号和对应的金额塞到数据库DB和缓存Redis中去”,其完整的源代码如下所示:  

    //TODO:新增/手机话费充值 记录 v2
@Transactional(rollbackFor = Exception.class)
public Integer addRecordV2(PhoneFare fare) throws Exception{
log.info("----sorted set话费充值记录新增V2:{} ",fare);

int res=fareMapper.insertSelective(fare);
if (res>0){
FareDto dto=new FareDto(fare.getPhone());

ZSetOperations<String,FareDto> zSetOperations=redisTemplate.opsForZSet();
Double oldFare=zSetOperations.score(Constant.RedisSortedSetKey2,dto);
if (oldFare!=null){
//TODO:表示之前该手机号对应的用户充过值了,需要进行叠加
zSetOperations.incrementScore(Constant.RedisSortedSetKey2,dto,fare.getFare().doubleValue());
}else{
//TODO:表示只充过一次话费
zSetOperations.add(Constant.RedisSortedSetKey2,dto,fare.getFare().doubleValue());
}
}
return fare.getId();
}

在这里,我们塞入到缓存SortedSet中的对象实体为FareDto类,该类包含一个字段信息,即“手机号”,如下所示:  

/**手机号唯一性
* @Author:debug (SteadyJack) weixin-> debug0868 qq-> 1948831260 **/
@Data
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
public class FareDto implements Serializable{
private String phone;
}

(4)至此,我们已经完成了“用户充值”业务模块的功能,下面我们用Postman测试一波,贴几张测试结果的图吧:



二、用户获取充值排行榜模块

既然我们的充值都成功插入到了数据库DB和缓存Cache中,那么接下来自然而然是需要将其从缓存中获取出来,并将其处理成“排行榜”的形式展示给用户观看,其核心业务流程图如下所示:


(1)同样的道理, 我们仍然在SortedSetController中开发“获取充值排行榜”的请求方法,其完整的源代码如下所示:

    @RequestMapping(value = "get/v2",method = RequestMethod.GET)
public BaseResponse getV2(){
BaseResponse response=new BaseResponse(StatusCode.Success);
try {
response.setData(sortedSetService.getSortFaresV2());
}catch (Exception e) {
response = new BaseResponse(StatusCode.Fail.getCode(), e.getMessage());
}
return response;
}

(2)而 sortedSetService.getSortFaresV2() 做的事情便是实现如何从缓存Redis的有序集合“SortedSet中获取到充值排行榜”,其完整源码如下所示:  

    //TODO:获取充值排行榜V2
public List<PhoneFare> getSortFaresV2(){
List<PhoneFare> list= Lists.newLinkedList();

final String key=Constant.RedisSortedSetKey2;
ZSetOperations<String,FareDto> zSetOperations=redisTemplate.opsForZSet();
final Long size=zSetOperations.size(key);

Set<ZSetOperations.TypedTuple<FareDto>> set=zSetOperations.reverseRangeWithScores(key,0L,size);
if (set!=null && !set.isEmpty()){
set.forEach(tuple -> {
PhoneFare fare=new PhoneFare();
fare.setFare(BigDecimal.valueOf(tuple.getScore()));
fare.setPhone(tuple.getValue().getPhone());

list.add(fare);
});
}
return list;
}

(3)至此,我们已经将“获取用户充值排行榜”的功能模块实战完毕,下面我们也同样基于Postman测试一波吧,贴几张图:


最终可以看到,展现在我们面前的确实一张排行榜(从大排到小)!而且这张排行榜是直接从缓存Redis的SortedSet中拿到的,而并非前往数据库DB进行复杂的查询、排序和计算(无疑减少了许多数据库层面的查询压力)!

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

对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的技术微信公众号,最新的技术文章、课程以及技术专栏将会第一时间在公众号发布哦!