首页 > 数据库 >Redis 实现简单排行榜功能 | 实战案例

Redis 实现简单排行榜功能 | 实战案例

时间:2024-08-12 15:53:40浏览次数:20  
标签:实战 String userId Redis 用户 entity 排行榜 key nickname

一、业务场景

口算小程序,用户完成口算并获得满分,根据耗时长短进行rank排名,耗时越短,排名越高。主要有以下功能:

1. 用户数据Mysql与Redis同步:使用一个redis hash用来保存用户基本信息,field为userId,value为用户基础数据(本案例为昵称);用户修改昵称时,同步更新hash中对应userId的nickname;

2. 保存记录:口算完成后,根据当前口算结果(得分、耗时)更新对应难度的排行榜数据。用户获得满分时,并且耗时大于三秒(可以添加更专业的防脚本措施),则将userId作为field,耗时作为score,添加进排行榜zset中,并且剔除多余排名(本场景zset最大支持50名),这几步使用lua脚本保证原子性;

3. 查看排行榜:按照耗时升序排序,范围查找前20名用户,查询上述用户hash,将userId转化为nickname返回给前端。

二、数据库、数据结构

对重点涉及的Mysql表以及Redis数据结果进行说明

Mysql:

用户基础数据表

drop table if exists arithmetic_user;
create table arithmetic_user (
id bigint primary key auto_increment,
username varchar(255) comment '用户名称',
password varchar(255) comment '密码',
nickname varchar(40) default '匿名用户' comment '昵称',
integral float default 0 comment '积分',
total_integral float default 0 comment '总积分',

create_time datetime default now() comment '创建时间',
deleted tinyint default 0
);

Redis:

排行榜有序集合zset,field存放用户Id,score存放答题总耗时,根据耗时升序排序;

public static final String ARITHMETIC_RANK = "arithmetic:rank";

// 使用difficuty参数,可以对不同难度模式分别进行排行数据统计
public static String getArithmeticRank(int difficulty) {
    return ARITHMETIC_RANK + ":" + difficulty;
}

用户基础数据hash,field存放用户Id,value存放nickname昵称。

三、代码示例

为方便理解,非核心代码将进行黑盒处理

1、Mysql与Redis数据同步

首次同步:项目启动时,将数据库中的用户数据同步到Redis hash中,供后续查看排行榜使用(ps:用户量大时,可以分批次进行同步操作):

@PostConstruct
public void initUserInfoCache() {
    List<ToolArithmeticUserEntity> list = this.list();
    String key = RedisKeys.ARITHMETIC_USER_INFO;
    if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {
        redisTemplate.delete(key);
    }
    // 批量添加hmset
    redisTemplate.opsForHash().putAll(
            key,
            list.stream().collect(
                    Collectors.toMap(
                            item -> String.valueOf(item.getId()),
                            ToolArithmeticUserEntity::getNickname
                    )
            )
    );
}

新增用户:用户首次进入时,将数据保存至数据库中,并同步保存至Redis

@Transactional
@Override
public ArithmeticUserEntity getUserInfo() {
    // 查询用户
    ArithmeticUserEntity one = getUserInfo();
    // 如果没查询到,则基于ip新增当前用户至数据库中
    if (one == null) {
        ToolArithmeticUserEntity entity = new ToolArithmeticUserEntity();
        // 设置默认用户信息
        entity.setInfo();
        boolean b = this.save(entity);
        // 用户数据同步添加至Redis中
        if (b) {
            String userId = String.valueOf(entity.getId());
            redisTemplate.opsForHash().put(RedisKeys.ARITHMETIC_USER_INFO, userId, entity.getNickname());
        }
        
        return entity;
    }
    return this.getById(one.getUserId());
}

编辑昵称:当用户修改昵称时,同样需要同步修改Redis hash中的nickname(是否会存在数据不一致问题?)

@Override
public void editNickName(String nickname) {
    LambdaQueryWrapper<ArithmeticUserEntity> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(ArithmeticUserEntity::getNickname, nickname);
    if (this.count(queryWrapper) > 0) {
        throw new GlobalException(10000, "该昵称已被使用");
    }
    ToolArithmeticUserEntity byId = this.getUserInfo();
    byId.setNickname(nickname);
    boolean b = this.updateById(byId);
    if (b) {
        String userId = String.valueOf(byId.getId());
        redisTemplate.opsForHash().put(RedisKeys.ARITHMETIC_USER_INFO, userId, nickname);
    }
}

2、保存记录,更新排行榜

@Override
public void saveRecord(ArithmeticRecordDTO entity) {
    // 获取用户信息
    // 保存当前答题记录至记录表
    // 更新积分等
    // 只有满分才参与排行,并且耗时大于3秒(或更加详细的防刷机制)
    if (Objects.equals(entity.getRightCount(), entity.getQuestionCount()) && entity.getTime() > 3) {
        int difficulty = entity.getDifficulty();
        String key = RedisKeys.getArithmeticRank(difficulty);
        float time = entity.getTime();
        // 当耗时比原有的score小时,才更新。使用lua脚本封装成原子操作
        String script = getLuaScript();
        int total = 50;
        redisTemplate.execute((RedisCallback<Object>) connection ->
                connection.eval(script.getBytes(), ReturnType.INTEGER,
                        1, key.getBytes(), userId.toString().getBytes(),
                        String.valueOf(time).getBytes(), String.valueOf(total).getBytes()));
    }
}

// lua语句
private String getLuaScript() {
    return "local key = KEYS[1]\n" +
            "local userId = ARGV[1]\n" +
            "local time = tonumber(ARGV[2])\n" +
            "local total = tonumber(ARGV[3])\n" +
            "local score = redis.call('ZSCORE', key, userId)\n" +
            "if not score then\n" +
            "    redis.call('ZADD', key, time, userId)\n" +
            "elseif tonumber(score) > time then\n" +
            "    redis.call('ZADD', key, time, userId)\n" +
            "end\n" +
            "local size = redis.call('ZCARD', key)\n" +
            "if size ~= false and size > total then\n" +
            "    redis.call('ZREMRANGEBYRANK', key, total, size)\n" +
            "end\n" +
            "return 0";
}

3、查询排行榜

@Override
public List<ArithmeticRankVO> getRankings(int difficulty) {
    return getRankings(difficulty, 0, 20);
}
/**
 * @param difficulty 题目难度
 * @param start 最高名次(根据耗时升序排名)
 * @param end 最低名次
 * @return field、score
 */
private List<ArithmeticRankVO> getRankings(int difficulty, int start, int end) {
    String key = RedisKeys.getArithmeticRank(difficulty);
    if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {
        Set<ZSetOperations.TypedTuple<Object>> typedTuples = redisTemplate.opsForZSet().rangeWithScores(key, start, end);
        if (typedTuples != null) {
            // 先从zset中查询出userId
            List<ArithmeticRankVO> list = typedTuples.stream().map(tuple -> {
                ArithmeticRankVO vo = new ArithmeticRankVO();
                vo.setUserId(Objects.requireNonNull(tuple.getValue()).toString());
                vo.setTime(Objects.requireNonNull(tuple.getScore()).floatValue());
                return vo;
            }).collect(Collectors.toList());
            // 获取nickname
            String infoKey = RedisKeys.ARITHMETIC_USER_INFO;
            // 再使用userId去hash中查出nickname
            List<Object> nicknameList = redisTemplate.opsForHash().multiGet(infoKey, list.stream().map(ArithmeticRankVO::getUserId).
            for (int i = 0; i < list.size() && i < nicknameList.size(); i++) {
                list.get(i).setNickname(nicknameList.get(i) == null ? "账号昵称异常" : nicknameList.get(i).toString());
            }
            return list;
        }
    }
    return new ArrayList<>();
}

四、效果

演示地址(菜鸡小站,大佬手下留情):

口算排行榜示例地址

标签:实战,String,userId,Redis,用户,entity,排行榜,key,nickname
From: https://blog.csdn.net/m0_62467665/article/details/141132023

相关文章

  • 宏基因组实战之:样本组装
    紧接上文,质控去除宿主(土壤样本不需要去宿主)后下一步对样本序列进行组装。1、组装工具宏基因组学中常用序列组装工具不少,如SOAPdenovo2、megagit,spades、metaSPAdes、MOCAT2、IDBA-UD等各有优劣,下面两个软件是分析过程中比较常用的。spades:https://github.com/ablab/spadesm......
  • Python Redis Stream【生产者=》消费者模式】
    1importredis2importtime3fromtypingimportDict,List,Tuple,Any,Optional45fromconfig.modelimportsettings6frompydanticimportBaseModel789classStreamMessage(BaseModel):10message_id:str11message_da......
  • SpringSecurity+前端项目+redis完成认证授权的代码
    1.前端准备工作--都在全局main.js页面中设置的1.1.创建Vue工程后,并导入elementui和axios,添加连接后端项目的路径,把axios挂载到Vue1.2.前置路由守卫(所有路由跳转前核实一下身份)//前置路由守卫--所有的路由跳转都先经过这里//to:即将要访问的路径from:从哪里来......
  • Python编码系列—Python性能分析神器:cProfile的深度应用与实战案例
    ......
  • Centos7下安装redis
    一、安装redis第一步:下载redis安装包wgethttp://download.redis.io/releases/redis-4.0.6.tar.gz[root@iZwz991stxdwj560bfmadtZlocal]#wgethttp://download.redis.io/releases/redis-4.0.6.tar.gz--2017-12-1312:35:12--http://download.redis.io/releases/redis-4......
  • 【Redis】掌握Java中的Redis魔法:Jedis与Spring Data Redis(实战指南)
    文章目录掌握Java中的Redis魔法:Jedis与SpringDataRedis实战文章简介为什么使用Redis为什么选择Jedis和SpringDataRedis一、引言1.1Redis简介1.1.1Redis的特点和优势1.1.2Redis的应用场景1.2Java与Redis的结合1.2.1为什么选择Java1.2.2Java开发中Redis的重要......
  • 小程序营销实战:利用小程序实现精准营销与增长
    小程序营销实战:利用小程序实现精准营销与增长在数字化营销日益重要的今天,小程序以其轻量、便捷的特点,成为了企业实现精准营销与增长的重要工具。本文将从策略规划、用户洞察、内容创新、技术应用以及数据分析等多个维度,特别是技术层面,深入探讨如何利用小程序实现精准营销与增......
  • Redis三种高可用模式:主从、哨兵、集群
    一、主从主从模式的定义Redis的主从模式是一种数据备份和读写分离的模式。在这种模式下,有一个主节点(Master)和一个或多个从节点(Slave)。所有的写操作都在主节点上进行,而读操作可以在主节点和从节点上进行。从节点会复制主节点的数据,实现数据的备份。主从模式的工作原理在主从模......
  • Golang中使用redis,mysql
    一、redis简介redis(REmoteDIctionaryServer)是一个由SalvatoreSanfilippo写key-value存储系统,它由C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value类型的数据库,并提供多种语言的API。和Memcached类似,它支持存储的value类型相对更多,包括string(......
  • (七)Redis 持久化 AOF、RDB
    Redis一旦服务器宕机,内存中的数据将全部丢失,从后端数据库恢复这些数据,对数据库压力很大,且性能肯定比不上从Redis中读取,会拖慢应用程序。所以,对Redis来说,实现数据的持久化,避免从后端数据库中进行恢复,是至关重要的。1、AOF日志AOF日志是先执行命令,把数据写入内存,然后才记......