Redis积分排行榜设计与实现第一篇

前言

越来越多的社交类APP为了提升应用的用户活跃度,刺激用户消费,采取签到功能,通过签到进行一系列的促销活动,比如签到赠送积分,通过积分可以兑换相应的礼品,购物抵扣券之类的

在某些APP上,有一种积分排行榜的功能,或者游戏APP应用上面也有类似的得分排行榜,这个排行榜一定程度上对玩家来说,如果能上榜,也是不错的成就感
在这里插入图片描述

以签到送积分为例简单分析下,当前账号绑定的信息,每天签到一次,就往一张签到表中插入一条数据,同时在积分表增加一条数据,最终根据业务需要,展示排行前10或者20的积分用户

关于签到 -> 送积分 ->积分排行榜的功能设计与实现,第一反应很容易想到使用数据库就可以实现

比如创建一张用户的积分表,在之前签到功能的基础上,每次签到就送一次积分,然后往用户积分榜添加一条数据,当然送积分的规则需要提前根据产品需求定,假设连续签到1天,送10积分,连续2天,送20,3天送30,连续签到次数超过3天之后每次送50积分

那么对于用户积分表来说,就存储了大量的不同用户的签到之后积分信息,做top20的排行榜排名时,只需考虑操作这张表的数据即可

在这里插入图片描述

分析来说,整个业务并不难,和之前我们分析签到功能一样,需要考虑的是,如果签到送积分功能背后的用户体量超大,像腾讯的微信或QQ那样,最后一定要考虑性能问题

mysql实现方案

数据表,新增一张用户积分表

CREATE TABLE `t_points` (
  `id` int(12) NOT NULL,
  `user_id` int(12) DEFAULT NULL,
  `points` int(12) DEFAULT NULL,
  `type` int(12) DEFAULT NULL COMMENT '积分类似, 0:签到送积分,1:评论送积分,2:点赞送积分',
  `is_valid` int(12) DEFAULT NULL COMMENT '是否有效,0:否,1:是',
  `create_date` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

添加积分的逻辑

	//送积分操作,实际业务中根据需求定,比如签到一次送多少积分,点赞一次送多少积分等
    //以签到为例,第一天签到送10积分,第二天送20,第三天送30,超过4天之后每天50积分
    public int addRealPoints(int countSequenceDay,int userId){
        int points = 10;
        if(countSequenceDay ==1){
            points=10;
        }else if(countSequenceDay==2){
            points=20;
        }else if(countSequenceDay==3){
            points=30;
        }else {
            points=50;
        }
        Points pointsSave = new Points();
        pointsSave.setId(atomicInteger.incrementAndGet());
        pointsSave.setUserId(userId);
        pointsSave.setPoints(points);
        pointsSave.setIsValid(1);
        pointsSave.setCreateDate(new Date());
        pointsMapper.save(pointsSave);
        log.info("积分添加成功");
        return points;
    }

还记得Redis签到功能设计与实现中连续签到的逻辑吗,在方法的最后,返回了截止到本次签到,连续签到的天数,那么,送积分的操作,就可以紧接在这里,根据当前用户连续签到的天数,来确定每次送的积分数量
在这里插入图片描述
因此,添加了送积分操作之后,签到的完整的逻辑就变成下面这样,

	public Object doSign(int userId,String dateStr) {
        //获取日期
        Date date = getDate(dateStr);
        //获取传入的日期对应的天数,即保存到redis的bitmaps的偏移量,索引位置
        int offset = DateUtil.dayOfMonth(date) - 1;
        String signKey = getSignKey(userId,date);
        //检查当前这个时间是否签到
        Boolean hasSign = redisTemplate.opsForValue().getBit(signKey, offset);
        if(hasSign){
            throw new RuntimeException("已经签到了,下次再来吧");
        }
        redisTemplate.opsForValue().setBit(signKey,offset,true);
        //连续签到的天数
        int sequenceSignCount = getSignCount(userId,date);
        //FIXME 根据连续签到的天数,确定赠送积分的数量
        int currentPoints = addRealPoints(sequenceSignCount, userId);
        return currentPoints;
    }

下面来简单测试下,依照之前的测试经验,结合本次的签到送积分规则,工程启动之后,我们依次调用下面的接口,观察返回值,

http://localhost:8089/doSign?userId=1112&dateStr=2021-03-01
http://localhost:8089/doSign?userId=1112&dateStr=2021-03-02
http://localhost:8089/doSign?userId=1112&dateStr=2021-03-03
http://localhost:8089/doSign?userId=1112&dateStr=2021-03-04
http://localhost:8089/doSign?userId=1112&dateStr=2021-03-05
http://localhost:8089/doSign?userId=1112&dateStr=2021-03-08
http://localhost:8089/doSign?userId=1112&dateStr=2021-03-09

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

再看数据库,插入了当前用户不同签到状况下的积分数据
在这里插入图片描述

那么,紧接着,在实际业务中,有了这张表之后,结合用户表,就可以开始做积分top20的功能了

简单来说,从mysql的实现方案来说,就是想办法通过sql语句来实现,为了方便后面排行数据更接近真实情况,我们通过测试程序向数据库中随机增加30个用户,以及这30个用户的积分数据

@RunWith(SpringRunner.class)
@SpringBootTest(classes = FollowApp.class)
public class AppTest {

    @Resource
    private UserService userService;

    @Resource
    private SignService signService;

    /**
     * 批量添加30个用户
     */
    @Test
    public void testSaveUser() {
        System.out.println("testSaveUser");
        for(int i=1;i<=30;i++){
            User user = new User();
            user.setId(i);
            user.setUsername("user" + i);
            user.setPassword("123456");
            user.setNickname("user" +i);
            user.setPhone("xxxx");
            user.setEmail("xxxx");
            user.setCreateDate(new Date());
            user.setUpdateDate(new Date());
            userService.saveUser(user);
        }
    }

    /**
     * 为30个用户随机增加积分
     */
    @Test
    public void testAddPoints(){
        for(int i=0;i<=30;i++){
            Random rd1 = new Random();
            int timeCount = rd1.nextInt(10);;
            for(int j=1;j<=timeCount;j++){
                Random rd = new Random();
                int points = rd.nextInt(100) * 10;
                signService.addPoints(i,1,points);
            }
        }
    }

}

运行上述的两个单元测试即可,最终的效果如下,
在这里插入图片描述
随机截取的部分积分表数据
在这里插入图片描述
完整的sql语句如下:

SELECT
	user_id,
	sum(points) total,
	rank () over (ORDER BY sum(points) DESC) ranks
FROM
	t_points
GROUP BY
	user_id
ORDER BY
	total DESC
LIMIT 10

遗憾的是rank()函数目前支持的版本为5.8以上的版本,因此如果使用这种方式来做的话,对你的mysql数据库版本有一定限制

下面我们切换至mysql8的版本试试,可以看到,通过这种方式,可以很方便的对排名前N的积分数据查出来
在这里插入图片描述
为了更完整的展示用户数据,关联下用户表进行查询,简单改造下上述的sql

SELECT
	tp.user_id,
	sum(points) total,
	rank () over (ORDER BY sum(points) DESC) ranks,
	tu.username
FROM
	t_points tp left join t_user tu
	on tp.user_id = tu.id
GROUP BY
	tp.user_id
ORDER BY
	total DESC
LIMIT 10

在这里插入图片描述
这样查询出来的就是一个完整的用户积分前topN的排行榜数据,如果要查看某个具体用户的排行信息,可以利用下面这条语句进行查询,比如查询用户ID为3的积分排行信息

select id,total,ranks,username from
(
SELECT
	tp.user_id id,
	sum(points) total,
	rank () over (ORDER BY sum(points) DESC) ranks,
	tu.username
FROM
	t_points tp left join t_user tu
	on tp.user_id = tu.id
GROUP BY
	tp.user_id
ORDER BY
	total DESC
) r

where id = 3

在这里插入图片描述

有了上面的数据之后,就可以编写相关的Java代码进行数据获取了,由于本篇主要想讨论的是使用Redis解决积分排行榜的问题,上面的代码实现就不再继续了,有兴趣的同学请自行完成

随着时间的增长,参与点赞等送积分活动的人肯定会越来越多,通过t_points和t_user这两张表进行关联查询展示最终肯定会成为性能的瓶颈,因此我们考虑使用Redis来解决这个问题,具体怎么操作呢?限于篇幅原因,本篇先到这里结束了,留着下篇继续分析

需要源码的同学可自行下载:https://download.csdn.net/download/zhangcongyi420/15499799

本篇到此结束,最后感谢观看!

猜你喜欢

转载自blog.csdn.net/zhangcongyi420/article/details/114216883
今日推荐