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

微笑、不失礼 提交于 2020-02-10 11:14:15

概述:本系列博文所涉及的相关内容来源于debug亲自录制的实战课程:缓存中间件Redis技术入门与应用场景实战(SpringBoot2.x + 抢红包系统设计与实战),感兴趣的小伙伴可以点击自行前往学习(毕竟以视频的形式来掌握技术 会更快!) 文章所属技术专栏:缓存中间件Redis技术入门与实战

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

视频介绍:

3-16数据类型之有序集合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搭建的技术社区的课程中心进行学习观看:程序员实战基地 !其他相关的技术,感兴趣的小伙伴可以关注底部debug的技术公众号,一起学习、共同成长!

补充:

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

2、目前debug已将本文所涉及的内容整理录制成视频教程,感兴趣的小伙伴可以前往观看学习:https://edu.csdn.net/course/detail/26619

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!