news 2026/4/23 17:13:36

天机学堂-排行榜功能-day08(六)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
天机学堂-排行榜功能-day08(六)

接口

一 实时排行榜

1.查询赛季列表功能

参数说明
请求方式GET
请求路径/boards/seasons/list
请求参数
返回值[ { "id": "110", // 赛季id "name": "第一赛季", // 赛季名称 "beginTime": "2023-05-01", // 赛季开始时间 "endTime": "2023-05-31", // 赛季结束时间 } ]
PointsBoardSeasonController.java
/** * 查询赛季列表 * @return */@ApiOperation("查询赛季列表")@GetMapping("/list")publicList<PointsBoardSeason>list(){returnpointsBoardSeasonService.list();}

2.实时排行榜功能(基于Zset改造之前的代码)

RedisConstants.java
/** * 积分排行榜的前缀 boards:202501 */StringPOINTS_BOARDS_KEY_PREFIX="boards:";
PointsRecordServiceImpl.java
@OverridepublicvoidaddPointsRecord(LonguserId,intpoint,PointsRecordTypetype){//判断该积分类型是否有上限 type.maxPoints是否大于0if(point<=0){return;}intmaxPoints=type.getMaxPoints();LocalDateTimenow=LocalDateTime.now();if(maxPoints>0){LocalDateTimedayStartTime=DateUtils.getDayStartTime(now);LocalDateTimedayEndTime=DateUtils.getDayEndTime(now);//如果有上限 查询该用户 该积分类型 今日已得积分 points_record 条件userId typeQueryWrapper<PointsRecord>wrapper=newQueryWrapper<>();wrapper.select("sum(points) as totalPoints");wrapper.eq("user_id",userId);wrapper.eq("type",type);wrapper.between("create_time",dayStartTime,dayEndTime);Map<String,Object>map=this.getMap(wrapper);//当前用户该积分类型 已得积分intcurrentPoints=0;if(map!=null&&map.containsKey("totalPoints")){BigDecimaltotalPoints=(BigDecimal)map.get("totalPoints");currentPoints=totalPoints.intValue();}//判断已得积分是否超过上限if(currentPoints>=maxPoints){//说明已得积分 达到上限return;}// 此时的point标识能得得积分if(currentPoints+point>maxPoints){point=maxPoints-currentPoints;}}//保存积分PointsRecordrecord=newPointsRecord();record.setUserId(userId);record.setType(type);record.setPoints(point);this.save(record);// 累计积分添加到Redis当中)(改造部分)Stringkey=RedisConstants.POINTS_BOARDS_KEY_PREFIX+now.format(DateUtils.POINTS_BOARD_SUFFIX_FORMATTER);stringRedisTemplate.opsForZSet().incrementScore(key,userId.toString(),point);}

3.查询学霸积分排行榜

接口说明查询指定赛季的积分排行榜以及当前用户的积分和排名信息
请求方式GET
请求路径/boards
请求参数分页参数,例如PageNo、PageSize赛季id,为空或0时,代表查询当前赛季。否则就是查询历史赛季
返回值{ "rank": 8, // 当前用户的排名 "points": 21, // 当前用户的积分值 [ { "rank": 1, // 排名 "points": 81, // 积分值 "name": "Jack" // 姓名 }, { "rank": 2, // 排名 "points": 74, // 积分值 "name": "Rose" // 姓名 } ] }
PointsBoardController.java
@ApiOperation("查询学霸积分榜-当前赛季和历史赛季都可用")@GetMappingpublicPointsBoardVOqueryPointsBoardList(PointsBoardQueryquery){returnpointsBoardService.queryPointsBoardList(query);}
IPointsBoardService.java
PointsBoardVOqueryPointsBoardList(PointsBoardQueryquery);
PointsBoardServiceImpl.java
@OverridepublicPointsBoardVOqueryPointsBoardList(PointsBoardQueryquery){// 获取当前登录用户idLonguserId=UserContext.getUser();// 判断是查当前赛季还是历史赛季 query.seasonbooleanisCurrent=query.getSeason()==null||query.getSeason()==0;LocalDatenow=LocalDate.now();Stringformat=now.format(DateTimeFormatter.ofPattern("yyyyMM"));Stringkey=RedisConstants.POINTS_BOARD_KEY_PREFIX+format;Longseason=query.getSeason();// 查询我的排名和积分PointsBoardboard=isCurrent?queryMyCurrentBoard(key):queryMyHistoryBoard(season);// 分页查询赛季列表List<PointsBoard>list=isCurrent?queryCurrentBoard(key,query.getPageNo(),query.getPageSize()):queryHistoryBoard(query);// 封装vo返回PointsBoardVOvo=newPointsBoardVO();vo.setRank(board.getRank());//我的排名vo.setPoints(board.getPoints());//我的积分//封装用户id集合 调用用户服务 获取用户信息 转mapSet<Long>uids=list.stream().map(PointsBoard::getUserId).collect(Collectors.toSet());List<UserDTO>userDTOS=userClient.queryUserByIds(uids);if(userDTOS.isEmpty()){thrownewBizIllegalException("用户不存在");}//转map key:用户id value 用户名称Map<Long,String>userDtoMap=userDTOS.stream().collect(Collectors.toMap(UserDTO::getId,c->c.getName()));List<PointsBoardItemVO>voList=newArrayList<>();for(PointsBoardpointsBoard:list){PointsBoardItemVOitemVO=newPointsBoardItemVO();itemVO.setName(userDtoMap.get(pointsBoard.getUserId()));itemVO.setPoints(pointsBoard.getPoints());itemVO.setRank(pointsBoard.getRank());voList.add(itemVO);}vo.setBoardList(voList);returnvo;}/** * 查询历史赛季排行榜列表 * * @param query * @return */privateList<PointsBoard>queryHistoryBoard(PointsBoardQueryquery){if(query.getPageNo()<=0||query.getPageSize()<=0){thrownewBadRequestException("非法参数");}intoffset=query.getPageNo()-1;List<PointsBoard>list=this.lambdaQuery().eq(PointsBoard::getSeason,query.getSeason()).orderByAsc(PointsBoard::getPoints).last("LIMIT "+query.getPageSize()+" OFFSET "+offset).list();returnlist;}/** * 查询当前赛季排行榜列表 * * @param key * @param pageNo 页码 * @param pageSize 条数 * @return */privateList<PointsBoard>queryCurrentBoard(Stringkey,IntegerpageNo,IntegerpageSize){// 计算start和stop 下标都是从零开始intstart=(pageNo-1)*pageSize;intend=start+pageSize-1;// 利用zrevrange 按分数倒序 分页查询Set<ZSetOperations.TypedTuple<String>>typedTuples=redisTemplate.opsForZSet().reverseRangeByScoreWithScores(key,start,end);if(CollUtils.isEmpty(typedTuples)){returnCollUtils.emptyList();}intrank=start+1;List<PointsBoard>list=newArrayList<>();//封装结果返回for(ZSetOperations.TypedTuple<String>typedTuple:typedTuples){Stringvalue=typedTuple.getValue();//用户idDoublescore=typedTuple.getScore();//总积分值if(StringUtils.isBlank(value)||score==null){continue;}PointsBoardboard=newPointsBoard();board.setUserId(Long.valueOf(value));board.setPoints(score.intValue());board.setRank(rank++);list.add(board);}returnlist;}/** * 查询历史赛季我的积分和排名 * * @param season * @return */privatePointsBoardqueryMyHistoryBoard(Longseason){LonguserId=UserContext.getUser();if(season==null){thrownewBadRequestException("非法参数");}PointsBoardone=this.lambdaQuery().eq(PointsBoard::getSeason,season).eq(PointsBoard::getUserId,userId).one();returnone;}/** * 查询当前赛季我的积分和排名 * * @param key * @return */privatePointsBoardqueryMyCurrentBoard(Stringkey){// 获取当前登录用户idLonguserId=UserContext.getUser();// 从Redis中获取分值Doublescore=redisTemplate.opsForZSet().score(key,userId.toString());// 获取排名 从0开始 需要加一Longrank=redisTemplate.opsForZSet().reverseRank(key,userId.toString());PointsBoardboard=newPointsBoard();board.setRank(rank==null?0:rank.intValue()+1);board.setPoints(score==null?0:score.intValue());returnboard;}

二 历史排行榜

1.定时任务生成榜单表


PointsBoardPersistentHandler.java
@XxlJob("createTableJob")publicvoidcreatePointBoardTableOfLastSeason(){//1.获取上个月的时间LocalDateTimetime=LocalDateTime.now().minusMonths(1);//2.查询赛季idIntegerseasonId=pointsBoardSeasonService.querySeasonIdByTime(time);//3.创建积分榜单表if(seasonId==null){thrownewBadRequestException("当前赛季不存在");}pointsBoardService.createPointsBoardTable(seasonId);}
IPointsBoardSeasonService.java+IPointsBoardService.java
IntegerquerySeasonIdByTime(LocalDateTimetime);voidcreatePointsBoardTable(IntegerseasonId);
PointsBoardSeasonServiceImpl.java+PointsBoardServiceImpl.java
@OverridepublicIntegerquerySeasonIdByTime(LocalDateTimetime){Optional<PointsBoardSeason>pointsBoardSeason=lambdaQuery().le(PointsBoardSeason::getBeginTime,time).ge(PointsBoardSeason::getEndTime,time).oneOpt();returnpointsBoardSeason.map(PointsBoardSeason::getId).orElse(null);}
@Override public void createPointsBoardTable(Integer seasonId) { String tableName = "points_board_" + seasonId; mapper.createPointsBoardTable(tableName); }
PointsBoardMapper
void createPointsBoardTable(@Param("tableName") String seasonId);
PointsBoardMapper.xml
<insertid="createPointsBoardTable">CREATE TABLE if not exists `${tableName}` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '榜单id', `user_id` BIGINT NOT NULL COMMENT '学生id', `points` INT NOT NULL COMMENT '积分值', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_user_id` (`user_id`) USING BTREE ) COMMENT ='学霸天梯榜' COLLATE = 'utf8mb4_0900_ai_ci' ENGINE = InnoDB ROW_FORMAT = DYNAMIC</insert>

2.定时任务榜单持久化


TableInfoContext

创建一个ThreadLocal工具类在工作线程当中去存储表名

package com.tianji.learning.utils; /** * 获取当前线程的TableInfo对象 * * @author ax */ public class TableInfoContext { private static final ThreadLocal<String> TABLE_INFO = new ThreadLocal<>(); public static void setTableInfo(String tableInfo) { TABLE_INFO.set(tableInfo); } public static String getTableInfo() { return TABLE_INFO.get(); } public static void remove() { TABLE_INFO.remove(); } }
MybatisConfig.java

声明对应的配置类去实现对表名的修改

通过拦截器机制线程上下文传递,优雅地实现了逻辑表名到物理表名的动态映射。

MybatisConfig搭建了处理流水线,MybatisConfiguration为特定表配置了换名规则,而具体的表名信息则由业务代码通过TableInfoContext在关键时刻传递。整个过程对业务代码入侵极小,是分库分表场景下的经典解决方案。

MybatisConfiguration.java

package com.tianji.learning.config; import com.baomidou.mybatisplus.extension.plugins.handler.TableNameHandler; import com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor; import com.tianji.learning.utils.TableInfoContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.HashMap; import java.util.Map; /** * mybatis配置类 * * @author ax */ @Configuration public class MybatisConfiguration { @Bean public DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor() { Map<String, TableNameHandler> map = new HashMap<>(1); map.put("points_board", (sql, tableName) -> TableInfoContext.getTableInfo()); return new DynamicTableNameInnerInterceptor(map); } }

Mybatis-plus的配置类

packagecom.tianji.common.autoconfigure.mybatis;importcom.baomidou.mybatisplus.annotation.DbType;importcom.baomidou.mybatisplus.core.mapper.BaseMapper;importcom.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;importcom.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor;importcom.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.autoconfigure.condition.ConditionalOnClass;importorg.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;@Configuration@ConditionalOnClass({MybatisPlusInterceptor.class,BaseMapper.class})publicclassMybatisConfig{/** * @see MyBatisAutoFillInterceptor 通过自定义拦截器来实现自动注入creater和updater * @deprecated 存在任务更新数据导致updater写入0或null的问题,暂时废弃 */// @Bean// @ConditionalOnMissingBeanpublicBaseMetaObjectHandlerbaseMetaObjectHandler(){returnnewBaseMetaObjectHandler();}@Bean@ConditionalOnMissingBeanpublicMybatisPlusInterceptormybatisPlusInterceptor(@Autowired(required=false)DynamicTableNameInnerInterceptordynamicTableNameInnerInterceptor){MybatisPlusInterceptorinterceptor=newMybatisPlusInterceptor();PaginationInnerInterceptorpaginationInnerInterceptor=newPaginationInnerInterceptor(DbType.MYSQL);paginationInnerInterceptor.setMaxLimit(200L);interceptor.addInnerInterceptor(paginationInnerInterceptor);interceptor.addInnerInterceptor(newMyBatisAutoFillInterceptor());if(dynamicTableNameInnerInterceptor!=null){interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);}returninterceptor;}}
PointsBoardPersistentHandler.java

这里还借助了xxljob的分片广播,主要应用于多实例部署时,可以实现多实例分片,类似于取模运算的思路,每个实例负责不同的部分。

@XxlJob("savePointsBoard2DB")publicvoidsavePointsBoard2DB(){//1.获取上个月的时间LocalDateTimetime=LocalDateTime.now().minusMonths(1);//2.查询赛季id// 2.1拼接Redis当中的keyStringkey=RedisConstants.POINTS_BOARDS_KEY_PREFIX+time.format(DateUtils.POINTS_BOARD_SUFFIX_FORMATTER);// 2.2查询数据PointsBoardQuerypointsBoardQuery=newPointsBoardQuery();intindex=XxlJobHelper.getShardIndex();inttotal=XxlJobHelper.getShardTotal();pointsBoardQuery.setPageNo(index+1);pointsBoardQuery.setPageSize(100);// 2.3计算动态表名,存入ThreadLocal中IntegerseasonId=pointsBoardSeasonService.querySeasonIdByTime(time);TableInfoContext.setTableInfo("points_board_"+seasonId);while(true){List<PointsBoard>pointsBoards=pointsBoardService.queryCurrentBoardList(key,pointsBoardQuery);if(CollUtils.isEmpty(pointsBoards)){break;}// 持久化到数据库mapper.saveDb(pointsBoards,TableInfoContext.getTableInfo());//翻页pointsBoardQuery.setPageNo(pointsBoardQuery.getPageNo()+total);}TableInfoContext.remove();}
queryCurrentBoardList方法
@Override public List<PointsBoard> queryCurrentBoardList(String key, PointsBoardQuery query) { Integer pageNo = query.getPageNo(); Integer pageSize = query.getPageSize(); int from = (pageNo - 1) * pageSize; Set<ZSetOperations.TypedTuple<String>> typedTuples = redisTemplate.opsForZSet().reverseRangeWithScores(key, from, from + pageSize - 1); int rank = from + 1; if (CollUtils.isEmpty(typedTuples)) { return CollUtils.emptyList(); } List<PointsBoard> list = new ArrayList<>(typedTuples.size()); for (ZSetOperations.TypedTuple<String> tuple : typedTuples) { String value = tuple.getValue(); Double score = tuple.getScore(); if (score == null || value == null) { continue; } PointsBoard board = new PointsBoard(); board.setUserId(Long.valueOf(value)); board.setPoints(score.intValue()); board.setRank(rank++); list.add(board); } return list; }
PointsBoardMapper.java
void saveDb(@Param("list") List<PointsBoard> pointsBoards, @Param("tableName") String tableName);
PointsBoardMapper.xml

这里是将五个字段修改为三个字段的映射,rank排名映射为id,user_id与points则是普通的映射。

<insert id="saveDb"> insert into ${tableName} (id, user_id, points) values <foreach collection="list" item="item" separator=","> (#{item.rank}, #{item.userId}, #{item.points}) </foreach> </insert>

Redis中del和unlink区别

在Redis中,DELUNLINK命令都用于删除键,但它们在实现机制和对系统性能的影响上有显著区别:

1.同步 vs 异步

  • DEL命令同步删除。直接删除键及其关联的数据,操作会立即释放内存。如果删除的键对应大型数据结构(如包含数百万元素的哈希或列表),DEL可能会阻塞主线程,导致其他请求延迟。
  • UNLINK命令异步删除。首先将键从键空间(keyspace)中移除(逻辑删除),后续的内存回收由后台线程处理。命令立即返回,不会阻塞主线程,适合删除大对象。

2.性能影响

  • DEL:删除大键时可能引发明显延迟,影响Redis的响应时间。
  • UNLINK:几乎无阻塞,适合高吞吐场景,尤其适用于需要频繁删除大键的情况。

3.使用场景

  • DEL:适合删除小键或对内存释放时效性要求高的场景(如避免内存不足)。
  • UNLINK:推荐在大多数情况下使用,尤其是删除大键或需要低延迟的场景。

4.返回值

  • 两者均返回被删除键的数量,但UNLINK返回时数据可能尚未完全释放。

5.版本要求

  • UNLINKRedis 4.0引入,需确保版本支持;DEL在所有版本中可用。

示例对比

# 同步删除,可能阻塞主线程DEL large_key# 异步删除,立即返回,后台清理UNLINK large_key

总结

特性DELUNLINK
删除方式同步异步
阻塞主线程是(大键时)
适用场景小键或需立即释放内存大键或高并发场景
版本支持所有版本Redis 4.0+

建议:优先

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/23 13:35:40

61、深入了解Crash工具:系统崩溃分析利器

深入了解Crash工具:系统崩溃分析利器 1. 引言 在系统运行过程中,可能会遇到各种问题,其中内核崩溃是较为严重的情况。当出现内核崩溃时,我们需要借助一些工具来分析问题,Crash工具就是这样一个强大的工具。 2. Crash工具概述 Crash是一个依赖于机器的程序,不同的制造…

作者头像 李华
网站建设 2026/4/23 12:14:54

卷王回归预测模型实战手册:用MATLAB玩转CNN-LSTM多输出预测

CNN-LSTM多输出回归 基于卷积神经网络结合长短期记忆神经网络(CNN-LSTM)的多输出回归预测(多输入多输出) 程序已经调试好&#xff0c;仅需要根据输出个数修改outdim(输出个数)值。 替换数据集即可运行数据格式为excel 1、运行环境要求MATLAB版本为2020b及其以上 2、评价指标包括…

作者头像 李华
网站建设 2026/4/23 13:36:05

66、深入探索文件与内存相关命令及数据结构

深入探索文件与内存相关命令及数据结构 在系统管理和调试过程中,了解文件系统和内存相关的数据结构以及如何查看它们的信息至关重要。下面将详细介绍一些常用的命令及其输出信息的含义。 1. 文件相关命令 1.1 file命令 file address 命令用于打印指定地址处文件结构的内容…

作者头像 李华
网站建设 2026/4/23 12:18:08

Windows系统文件Windows.Devices.Enumeration.dll缺少或损坏 下载修复

在使用电脑系统时经常会出现丢失找不到某些文件的情况&#xff0c;由于很多常用软件都是采用 Microsoft Visual Studio 编写的&#xff0c;所以这类软件的运行需要依赖微软Visual C运行库&#xff0c;比如像 QQ、迅雷、Adobe 软件等等&#xff0c;如果没有安装VC运行库或者安装…

作者头像 李华
网站建设 2026/4/23 11:39:39

爬虫全链路加密传输:HTTPS + 数据AES加密实战

在网络爬虫的开发与应用中&#xff0c;数据传输的安全性是不可忽视的核心问题。尤其是在抓取敏感数据、对接企业级接口时&#xff0c;未加密的传输链路可能面临数据篡改、窃取等风险。HTTPS 协议能解决传输层的加密问题&#xff0c;而 AES 对称加密可实现应用层的数据加密&…

作者头像 李华
网站建设 2026/4/23 11:40:14

8088单板机 NASM汇编实验方法与步骤

以所给例程ex014为例1.例程功能&#xff1a;Intel8259中断测试&#xff0c;手动触发8259的中断&#xff0c;中断程序响应后&#xff0c;控制板子上的LED灯显示状态变化。2.硬件电路如下端口地址和硬件原理图&#xff0c;参见3.程序代码如下可以适用自己熟悉的编辑器进行修改编写…

作者头像 李华