news 2026/4/3 20:02: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/2 12:24:32

如何用 SpringAI 调用 StabilityAI 图像模型?轻松生成惊艳图像!

大家好,我是小米!今天带大家一起走进一个非常有趣的技术世界。我们要聊的,不是另一个普通的技术课题,而是如何通过 SpringAI 来调用 StabilityAI 图像模型,并把这个过程比作一场神奇的图像生成之旅! 想象一下,你走进了一家魔法工厂,这里有无数的机器,可以把你脑海中的…

作者头像 李华
网站建设 2026/3/24 4:48:16

基于vue的医院门诊处方管理系统_m964lx0c_springboot php python nodejs

目录具体实现截图项目介绍论文大纲核心代码部分展示项目运行指导结论源码获取详细视频演示 &#xff1a;文章底部获取博主联系方式&#xff01;同行可合作具体实现截图 本系统&#xff08;程序源码数据库调试部署讲解&#xff09;同时还支持java、ThinkPHP、Node.js、Spring B…

作者头像 李华
网站建设 2026/4/3 7:32:50

基于QueryInst的钢轨悬挂部件缺陷检测与识别系统实现_1

本数据集为钢轨探伤车采集的图像数据集&#xff0c;共包含93张经过预处理的图像&#xff0c;用于钢轨悬挂部件和机械零件的缺陷检测与识别任务。数据集采用YOLOv8格式标注&#xff0c;包含四个类别&#xff1a;悬挂部件(HangingParts)、轴盖(axlecover)、端块(empad)和弹簧(spr…

作者头像 李华
网站建设 2026/3/29 3:37:17

52、深入解析STREAMS的操作原理与机制

深入解析STREAMS的操作原理与机制 1. 首次打开流的操作流程 当 vp->v_stream == 0 时,意味着这是该流的首次打开操作。首先会进行内存检查,以确保STREAMS没有使用过多的内存。接着,会分配一个队列对和一个流头,初始化流头( stdata_t )并设置 STWOPEN 标志,同…

作者头像 李华
网站建设 2026/3/27 16:57:55

IDE透明视频播放插件:提升编程体验的多媒体解决方案

IDE透明视频播放插件&#xff1a;提升编程体验的多媒体解决方案 【免费下载链接】intellij-media-player 【&#x1f41f;摸鱼专用】上班偷偷看视频&#x1f4fa;而不会被老板打&#x1f528;的IDE插件&#xff0c;适配JetBrains全家桶 项目地址: https://gitcode.com/gh_mir…

作者头像 李华
网站建设 2026/4/3 14:58:31

快速文件传输神器:5分钟掌握transfer的完整使用指南

快速文件传输神器&#xff1a;5分钟掌握transfer的完整使用指南 【免费下载链接】transfer &#x1f36d; 集合多个API的大文件传输工具. 项目地址: https://gitcode.com/gh_mirrors/tr/transfer 还在为文件传输烦恼吗&#xff1f;无论你是需要临时分享文档给同事&#…

作者头像 李华