基于springboot开发,Redis的Zset类型实现排行榜

Redis的Zset类型实现排行榜

实现截图:
在这里插入图片描述

项目结构:
在这里插入图片描述
yml文件配置:连接Redis

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password: 123456

配置Redis序列化规则:

package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String,String> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        StringRedisTemplate redis = new StringRedisTemplate();
        redis.setConnectionFactory(redisConnectionFactory);

        StringRedisSerializer serializer = new StringRedisSerializer();
        redis.setKeySerializer(serializer);
        redis.setValueSerializer(serializer);
        redis.setHashKeySerializer(serializer);
        redis.setHashValueSerializer(serializer);

        return redis;
    }
}

创建实体类:

package com.example.demo.entity;

import java.io.Serializable;

public class RankDO implements Serializable {
    private static final long serialVersionUID = 4804922606006935590L;

    /**
     * 排名
     */
    private Long rank;

    /**
     * 积分
     */
    private Float score;

    /**
     * 用户id
     */
    private Long userId;


    public RankDO(Long rank, Float score, Long userId) {
        this.rank = rank;
        this.score = score;
        this.userId = userId;
    }

    public static long getSerialVersionUID() {
        return serialVersionUID;
    }

    public Long getRank() {
        return rank;
    }

    public void setRank(Long rank) {
        this.rank = rank;
    }

    public Float getScore() {
        return score;
    }

    public void setScore(Float score) {
        this.score = score;
    }

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }
}

排行榜ScoreService接口:

package com.example.demo.service;

import com.example.demo.entity.RankDO;

import java.util.List;

public interface ScoreService {

    /**
     * 获取前n名的排行榜数据
     *
     * @param num
     * @return
     */
    List<RankDO> findTopByNum(int num);

    /**
     * 插入用户数据
     * @param userId
     * @param score
     * @return
     */
    RankDO updateRank(long userId,float score);

    /**
     * 获取用户的排行榜位置
     *
     * @param userId
     * @return
     */
    RankDO getRankDo(long userId);

    /**
     * 获取用户所在排行榜的位置,以及排行榜中其前后n个用户的排行信息
     *
     * @param userId
     * @param n
     * @return
     */
    List<RankDO> getRankAroundUser(Long userId, int n);
}

实现接口ScoreServiceImpl:

package com.example.demo.service;

import com.example.demo.entity.RankDO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;

@Service
public class ScoreServiceImpl implements ScoreService {

    @Autowired
    StringRedisTemplate redisTemplate;

    @Override
    public List<RankDO> findTopByNum(int num) {
        //查询集合中指定顺序的值和score,0, -1 表示获取全部的集合内容
        Set<ZSetOperations.TypedTuple<String>> tupleSet = redisTemplate.opsForZSet().rangeWithScores("global_rank", 0, num-1);
        List<RankDO> rankList = new ArrayList<>(tupleSet.size());
        long rank = 1;
        for (ZSetOperations.TypedTuple<String> sub : tupleSet) {
            rankList.add(new RankDO(rank++, Math.abs(sub.getScore().floatValue()), Long.parseLong(sub.getValue())));
        }
        return rankList;
    }

    @Override
    public RankDO updateRank(long userId, float score) {
        //添加一个元素, zset与set最大的区别就是每个元素都有一个score,因此有个排序的辅助功能;  zadd
        redisTemplate.opsForZSet().add("global_rank", String.valueOf(userId), -score);
        Long rank = redisTemplate.opsForZSet().rank("global_rank", String.valueOf(userId));
        return new RankDO(rank + 1,score,userId);
    }

    @Override
    public RankDO getRankDo(long userId) {
        // 获取排行, 因为默认是0为开头,因此实际的排名需要+1
        Long rank = redisTemplate.opsForZSet().rank("global_rank", String.valueOf(userId));
        if (rank == null) {
            // 没有排行时,直接返回一个默认的
            return new RankDO(-1L, 0F, userId);
        }

        // 获取积分
        Double score = redisTemplate.opsForZSet().score("global_rank", String.valueOf(userId));
        return new RankDO(rank + 1, Math.abs(score.floatValue()), userId);
    }

    @Override
    public List<RankDO> getRankAroundUser(Long userId, int n) {
        // 首先是获取用户对应的排名
        RankDO rankDo = getRankDo(userId);
        if (rankDo.getRank() <= 0) {
            // fixme 用户没有上榜时,不返回
            return Collections.emptyList();
        }

        // 因为实际的排名是从0开始的,所以查询周边排名时,需要将n-1
        // 查询集合中指定顺序的值和score,0, -1 表示获取全部的集合内容
        Set<ZSetOperations.TypedTuple<String>> result =
                redisTemplate.opsForZSet().rangeWithScores("global_rank", Math.max(0, rankDo.getRank() - n - 1), rankDo.getRank() + n - 1);
        List<RankDO> rankList = new ArrayList<>(result.size());
        long rank = rankDo.getRank() - n;
        for (ZSetOperations.TypedTuple<String> sub : result) {
            rankList.add(new RankDO(rank++, Math.abs(sub.getScore().floatValue()), Long.parseLong(sub.getValue())));
        }
        return rankList;
    }
}

控制层RankController :

package com.example.demo.controller;

import com.example.demo.entity.RankDO;
import com.example.demo.service.ScoreService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

import java.util.List;

@RestController
public class RankController {

    @Autowired
    ScoreService scoreService;

    @RequestMapping(value = "/index",method = RequestMethod.GET)
    public ModelAndView index(){
        return new ModelAndView("/index");
    }


    /**
     * 获取前n名的排行榜数据
     * @param n
     * @return
     */
    @GetMapping(path = "/topn")
    public List<RankDO> showTopN(int n) {
        return scoreService.findTopByNum(n);
    }

    /**
     * 插入新的数据排名
     * @param userId
     * @param score
     * @return
     */
    @GetMapping(path = "/update")
    public RankDO updateScore(long userId, float score) {
        return scoreService.updateRank(userId,score);
    }

    /**
     * 获取用户的排行榜位置
     * @param userId
     * @return
     */
    @GetMapping(path = "/rank")
    public RankDO queryRank(long userId) {
        return scoreService.getRankDo(userId);
    }

    /**
     * 获取用户所在排行榜的位置,以及排行榜中其前后n个用户的排行信息
     * @param userId
     * @param n
     * @return
     */
    @GetMapping(path = "/around")
    public List<RankDO> around(long userId, int n) {
        return scoreService.getRankAroundUser(userId,n);
        //return rankListComponent.getRankAroundUser(userId, n);
    }

}

前端代码实现index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id="redis">
    Redis排行榜(前五名)
    <table border="1">
        <tr>
            <td>user</td>
            <td>score</td>
        </tr>
        <tr v-for="user in ranks">
            <td>{{user.userId}}</td>
            <td>{{user.score}}</td>
        </tr>
    </table>
    <input placeholder="用户id" v-model="user.userId" style="width: 50px"/>
    <input placeholder="分数" v-model="user.score" style="width: 50px"/>
    <button @click="doSave()">提交</button>
    <div>
        <input placeholder="用户id" v-model="userId" style="width: 50px"/>
        <input placeholder="排名" v-model="no" style="width: 50px"/>
        <button @click="findNo">查询</button>
    </div>
</div>

<script src="/jquery-2.2.3.min.js"></script>
<script src="/vue-2.6.11.js"></script>
<script src="/axios-0.19.2.min.js"></script>
<script>
    let v = new Vue({
        el:'#redis',
        data:{
            userId:'',
            user:{},
            ranks:{},
            no:''
        },
        methods:{
            showTopN:function (n) {
                axios({
                    url:'/topn',
                    method:'get',
                    params:{
                        'n':n
                    }
                }).then(response=>{
                    this.ranks = response.data;
                    this.user={};
                })
            },
            doSave:function () {
                axios({
                    url:'/update',
                    params: {
                        "userId":this.user.userId,
                        "score":this.user.score
                    }
                }).then(response=>{
                    this.showTopN(5);
                })
            },
            findNo:function () {
                axios({
                    url:'/rank',
                    params:{
                        "userId":this.userId
                    }
                }).then(response=>{
                    this.no = response.data.rank;
                })
            }
        },
        created:function () {
            this.showTopN(5);
        }
    })
</script>
</body>
</html>
  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
【资源说明】 课程设计基于Java实现的校园快递代拿系统源码+sql数据库+项目详细说明.zip 1、该资源内项目代码都是经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载使用,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果有一点儿基础,亦可在此代码基础上进行修改,以实现其他功能。 项目核心功能简述: 在此之前最好先看下notes、plan目录下的txt文件 本项目是一个自己臆想出需求(当然,大部分以课程设计文档需求为主)的大学快递代拿服务系统, 后端使用了springboot,mybatis-plus,redis(含lua),rocketmq等进行功能开发实现。 除此之外也整合了nacos,sentinel实现了简单的微服务 至于前端,本人前端水平有限,只能使用老一套的jq,bootstrap实现 分配配送员完整思路 各组件使用情况: 1. springboot作为项目使用的基础建设框架 2. mybatis-plus作为crud核心(为什么不用tk?因为mybatis-plus的使用更为简单,尤其是乐观锁、逻辑删除等) 3. spring security+oauth2使用rsa加密实现认证中心(我这里没有使用io.jsonwebtoken,实际上应该使用,nacos的源码也有使用jjwt,我没使用只是为了更好的熟悉security的部分源码) 4. redis除了用作基本缓存以外,还是程序实现自动分配配送员的核心(详细看plan目录下的编写计划),简单概括下就是zset+lua实现区域按权重分配配送员 5. rocketmq则是用作订单超时自动取消、用户支付后发送分配配送员的事务消息,用户评价后发送分数校准的事务消息 6. nacos作为注册中心服务发现 7. sentinel作为服务熔断/限流降级 8. 实现了配送员签到/加班功能,能统计连签天数 9. 也实现了不同会员下单时打折的功能 10. 也有整合swagger(layui),访问地址是 ip:port + context-path + /docs.html,例如localhost:40200/order/docs.html或http://localhost:40300/docs.html,如果提示Access token expired请清除cookie 额外说明: 1. 启动express-order之前要启动express-ucenter, 2. 因为初始化配送员权重的代码写到order去了,order需要远程调用ucenter。 3. 代码还可能出现大改 4. 2019/11/22日之前rocketmq的两阶段提交还没学透,所以在这之前写的rocketmq事务处理方式是错的 5. 账号到数据库看,密码不是 mimajiushi 就是 123 6. express-auth 暂时没实现注册,登录页面 http://localhost:40400/page/index 7. 项目启动需要nacos是启动的,nacos的ip改成自己的, sentinel暂时可有可无 8. 项目中的反馈功能还未实现,因为只是简单的crud(包括某些查询)没什么新知识,所以暂时放着不管 9. 本项目偏向于个人的未知领域探索实现(绝对不是因为偷懒,笑),比如分布式事务如何保证最终一致性等,所以对个别功能定了计划但并没有实现, 比如:百度地图的区域可视化,收获地址管理等(以前的作品都实现过的功能我就不再实现了) 缺点: 1. 部份表可能显得比较肿,主要原因还是时间有限,不想重复过多无意义的dao代码 2. 我前端比较垃圾 3. 授权粒度只细化到角色级别,白话就是角色即权限,这么做一方面本项目没有前后分离也没有实现前后分离的动态路由,所以没必要细化到各角色权限也能实现该有的功能 4. 一些开源组件实现可能还存在比较细节的错误 一些容易出错的坑和项目主要关注的点: 1. 一个大坑就是rocketmq,我是现学现用的,曾因为理论不完善而导致代码多次出现大改, 1.1. 主要坑有两个,一个两阶段提交如何确保事务最终一致性(其实就是这学期nosql的理论实践)
压力测试 服务器是学生服务器,就是腾讯那个10元的服务器,在本地利用jmeter压测工具设置5000个线程、10000个请求时,首页的QPS为1024,而秒杀接口则为1678,可能服务器的问题,我感觉QPS不应该这么低。 <img src="https://github.com/suyeq/steamMall/blob/master/TestResult.png" width=350px height=200px> # 项目的运行 在数据库中创建steam库,然后导入sql文件,修改相应的的配置,关于主从配置请自行百度啦,运行起项目,然后需要把`GameService`类以及`CommentService`类里面的`afterPropertiesSet`方法里面的代码注释掉,这是第一次启动加载缓存的代码。 # 架构以及详细实现 ## 1.登录注册 * 两次md5,对密码加密 * 分布式session,将用户的登录信息缓存在redis中 * 权限检查,每次操作在权限允许下才能进行 * 注册,需要邮箱验证码,邮箱验证码会在缓存中存在90s的时间 ## 2.页面数据 * 首页及其他页面因为读多写少,利用Mysql主从复制实现读写分离,写入在主Mysql下进行,读取在从Mysql进行 * 关于数据的不一致性,可以在写入的时候先写入缓存,读取的时候也先在缓存中读取,这样就可以避免数据的不一致性 * 缓存利用Redis,内存满的情况下,键的删除策略采用volatile-lru * 热卖榜以及排序功能依据Redis的zset实现,缓存时间为30s * 为了减少网络时延的影响,引入了redis的管道技术,实行批处理 * 为了更好地加快性能,在redis缓存层之上加了加了一层本地的缓存 ## 3.秒杀功能 原理:尽量减少Mysql的访问 * 将先将需要秒杀的数据缓存在Redis中,在秒杀接口里做预减少库存 * 判断秒杀订单里有无对应的信息,有则秒杀重复 * 将user信息与秒杀资源信息加入消息队列中 * 消息接受者减少库存,新增秒杀订单 * 客户端不断轮询缓存,查询到对应的秒杀订单,则秒杀成功<br> 秒杀的一些优化(借鉴了网上的思路): * 增加了秒杀的随机路径,防刷 * 在秒杀开始前需要验证码输入,延缓压力 * 利用redis的缓存,限制每个用户的每分钟的秒杀次数 ## 4.页面的渲染 * 页面的渲染采取了jquery加ajax技术的字符串拼接以及thymleaf模板的渲染 * 大部分是用的字符串拼接,只有少部分采用的模板渲染 * 前端主要是由字符串拼接,后台主要是模板渲染 ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值