SpringBoot集成Redis实现图书分页缓存:ZSet实战指南
2026/9/24 18:45:57 网站建设 项目流程

做图书购买系统的时候,"图书列表分页"和"Redis缓存"几乎是绕不开的两个词。我这次在SpringBoot项目里把图书数据塞进Redis,并且让前端以分页的形式展示出来,踩了不少坑——从Redis安装配置、key序列化乱码,到MyBatis-Plus分页插件突然失效、Redis非分页缓冲池内存暴涨,一路排查下来,对"配置到前后端交互"这条完整链路有了很深的体会。

这篇文章把整条实现路径完整梳理一遍,包括数据结构的选型逻辑、缓存预热方案、ZSet分页的核心命令与代码实现、接口封装,以及Vue前端对接分页组件的全过程。适合正在做SpringBoot购物类项目、想用Redis做列表缓存分页、或者被Redis乱码、分页失效这类问题折磨过的同学参考。

1. 为什么要用Redis做图书分页:业务场景与选型思考

1.1 图书购买系统里,列表查询到底有多痛

图书购买系统虽然比不上一线电商平台那种动不动百万并发的规模,但"图书列表页"依然是用户访问最密集的入口。用户打开首页要看热门书籍,点进分类要看某个分类下的书籍列表,搜索关键词要看搜索结果页——几乎每一次操作都会触达图书列表的查询。

最开始我直接用MySQL分页,LIMIT offset, size语法简单、开发效率也高。但数据量上来之后问题就出现了:图书表几十万条记录只是起步,加上多条件筛选(分类、出版社、价格区间、评分排序),SQL会变得非常复杂。每次用户翻页都要完整走一遍索引扫描和排序,数据库的连接数被频繁打满,高峰期接口响应从几十毫秒飙升到两三秒。

更重要的是,图书列表有一个特点:热门榜单、新品推荐、分类列表这些数据的实时变化并不频繁。一本书上架后,它的书名、作者、价格、封面可能一个月都不会变,只有销量和评分会波动。这种读多写少、短暂延迟可以接受的数据,非常适合放到Redis里做缓存。

1.2 为什么"缓存+分页"必须一起设计

很多人的第一反应是:我只要给MySQL加一层缓存,把查询结果缓存住不就好了?比如用商品ID作为key缓存单个图书信息,分页还是走MySQL——这种方案能缓解一部分压力,但治标不治本。

因为分页查询的SQL组合太多:第1页、第2页、第5页,按销量排、按价格排、按上架时间排……如果以"整个分页请求URL"作为缓存key,那同一份图书数据会被缓存成几十种不同的key,缓存命中率很低,数据一致性维护也很痛苦。而且当图书的销量变化时,所有涉及这个图书的分页缓存全部要失效,缓存重建的瞬间DB压力又上来了。

所以我在这次项目里换了一个思路:把经过排序的图书ID集合直接放进Redis,分页读取在Redis中完成,MySQL只负责全量数据的存取和更新。这样列表页的每次翻页都是一次内存操作,速度极快,而且所有分页共用同一份缓存数据,不会有"同一份数据在多个缓存key里不一致"的问题。

1.3 Redis里适合做分页的数据结构选型对比

Redis提供了丰富的数据结构,但并不是每一种都适合做分页。我在做技术选型的时候,把几种常用方案放在一起对比了一轮。

数据结构分页实现方式优点缺点是否适合图书列表分页
String拼接JSON字符串,整体存取简单粗暴无法部分读取,更新成本高
ListLRANGE key start stop分页效率高,支持倒序只能按插入顺序,无法按业务字段排序部分适合
HashHSCAN配合手动计算适合存储实体字段分页需要借助游标,逻辑复杂
SetSSCAN去重无序,分页无意义
ZSetZRANGE / ZREVRANGE key start stop按score排序,天然支持分页需要合理设计score非常适合

最终我选择了ZSet。原因是图书列表几乎都存在"按某个指标排序"的需求:热门图书按销量排、最新上架按时间排、价格区间按价格排,这些都可以映射到ZSet的score上。ZSet底层使用跳跃表实现,ZRANGE按排名区间取数据的时间复杂度是O(log N + M),即使集合里有几十万条记录,取一页数据也很快。

2. 环境与项目搭建:Redis安装、SpringBoot基础配置、序列化策略

2.1 本机Redis安装与可视化客户端选择

我在Windows开发机上做本地调试,所以先用的是Windows版本Redis。Redis官方其实不直接提供Windows安装包,我用的开源移植版本,版本号选的是稳定分支。解压后直接运行redis-server.exe即可启动,默认端口6379。

生产环境用的是Linux服务器上的Redis 6.x,安装方式是直接下载官方源码编译,配置好requirepass设置访问密码,同时把daemonize yes打开让Redis后台运行。这里提醒一句:本地开发建议Redis不设密码或设一个简单的,但生产环境必须设置强密码,并且建议绑定内网IP而不是0.0.0.0

我日常工作用的可视化客户端是Another Redis Desktop Manager,方便查看key列表、查看某个zset的全部成员和score、执行Redis命令。排查序列化乱码问题、验证分页数据是否正确,它帮了大忙。纯命令行也可以,但可视化工具在调试阶段效率高得多。

2.2 引入SpringBoot依赖与核心配置

项目基于SpringBoot 2.7.14,Java版本用的8。在pom.xml中加入Redis相关依赖:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency>

第一个依赖提供了Spring Data Redis的封装,包含Lettuce连接工厂和RedisTemplate等核心类。第二个依赖是连接池,生产环境高并发下强烈建议加上,否则每个操作都新建连接,性能会很难看。

然后配置application.yml

spring: redis: host: 127.0.0.1 port: 6379 password: 123456 database: 0 timeout: 5000ms lettuce: pool: max-active: 16 max-idle: 8 min-idle: 2 max-wait: 3000ms

这里有个容易被忽略的点:SpringBoot 2.x默认使用Lettuce作为Redis客户端,而不是Jedis。Lettuce基于Netty,连接复用能力更强,多线程环境下表现更好,所以默认选它是有道理的。如果你之前用过Jedis,不用刻意换回来,Lettuce在大多数场景下更优。

database: 0表示使用默认的0号库。如果你一个Redis实例同时服务多个业务,建议为不同业务分配不同的database,避免key互相干扰。图书分页相关的缓存我单独放在了2号库,防止和会话缓存、验证码缓存混在一起。

2.3 自定义RedisTemplate序列化策略,这步不能省

Spring Data Redis在自动配置时,默认使用JdkSerializationRedisSerializer对value进行序列化。这在纯Java环境里没问题,但一旦配合可视化工具查看,或者让其他语言的服务读取数据,就会遇到一串转义字符,可读性极差,而且序列化后的数据体积很大,浪费内存。

我在项目里自定义了一个RedisTemplate,key使用StringRedisSerializer,value使用GenericJackson2JsonRedisSerializer:

@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); StringRedisSerializer keySerializer = new StringRedisSerializer(); GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(keySerializer); template.setHashKeySerializer(keySerializer); template.setValueSerializer(valueSerializer); template.setHashValueSerializer(valueSerializer); template.afterPropertiesSet(); return template; } }

这步处理直接影响后续分页数据的可读性。否则你在Redis里看到的key是\xac\xed\x00\x05t\x00...这样的二进制乱码,看到zset里的member也是乱码,排查数据对错会非常崩溃。

GenericJackson2JsonRedisSerializer会把对象序列化成JSON字符串,同时携带@class类型信息,反序列化时能还原成原来的Java对象。这里有一个注意点:被序列化的对象必须有无参构造函数,否则反序列化会报错。

3. 图书数据入缓存:缓存预热方案与数据结构设计

3.1 图书数据模型与Redis键设计

图书实体类如下:

public class Book { private Long id; private String title; private String author; private Double price; private Integer sales; private Long publishTime; private String category; private String coverUrl; // 省略getter/setter }

在Redis里,我把每本图书的详情存为Hash结构,但是参与排序和分页的,是ZSet。举一个最常用的场景——热门图书榜:

键:book:rank:sales score:图书销量 member:图书ID

用Redis命令查看就是这个效果:

ZREVRANGE book:rank:sales 0 9 WITHSCORES

类似地,如果要做"按上架时间最新"的分页,就可以设计另一个键:

键:book:rank:latest score:图书上架时间戳 member:图书ID

设计键名时用冒号分层,业务模块:功能:排序维度,这样既清晰又方便统一清理。可视化工具里看的时候也能按前缀搜索。

3.2 缓存预热:别让第一个用户成为"受害者"

如果缓存是空的,第一个用户访问热门图书列表时,程序需要去MySQL查数据再写入Redis,这个过程耗时几百毫秒,用户看到的就是一个"白屏转圈"。这就是缓存击穿的一种体现。

所以我在项目启动并完成数据库初始化后,主动做了一次缓存预热。利用ApplicationRunner接口,在SpringBoot启动完成后自动执行:

@Component public class BookCacheWarmer implements ApplicationRunner { @Autowired private BookService bookService; @Override public void run(ApplicationArguments args) { log.info("开始预热图书排行榜缓存..."); bookService.rebuildRankCache("sales"); bookService.rebuildRankCache("latest"); log.info("图书排行榜缓存预热完成"); } }

预热的核心逻辑是:从MySQL查出全量参与排序的图书,按排序维度写入对应的ZSet。每次预热之前先删除旧的key,再重新构建,保证幂等,防止重复数据累积:

public void rebuildRankCache(String dimension) { String key = getRankKey(dimension); stringRedisTemplate.delete(key); List<Book> books = bookMapper.selectList(null); for (Book book : books) { double score = getScoreByDimension(book, dimension); stringRedisTemplate.opsForZSet().add(key, String.valueOf(book.getId()), score); } }

如果图书量特别大(比如上百万),单线程预热会很慢,可以考虑用SCAN分批读取MySQL,再用pipeline批量写入Redis。本次项目图书量在10万级别,单线程全量写入大约两三秒,完全能接受。

3.3 为什么坚持用ZSet而不是List

很多人会问:List的LRANGE也能分页,为什么我用ZSet?我来实际对比一下。

List按插入顺序存储,如果图书上架时我们用LPUSH把新书插到头部,那"最新上架"这个排序确实可以用List实现。但"按销量排序"就麻烦了:一本书销量增长后,它在List中的位置不会自动变化,必须把元素删掉重新插入,这个操作在List里是O(N)级别的。

ZSet则天然支持排序。score就是排序依据,销量变了只需要更新score,Redis底层跳跃表会自动维护顺序。这样无论是新增图书、还是已有图书的销量/价格变化,我们只需要做一次ZADD,它就会落到正确的位置上,分页查询永远拿到最新顺序。这一点的工程价值非常大。

另外,ZSet还支持范围查询:ZCOUNT可以统计某个score区间有多少本书,ZRANGEBYSCORE可以取某个score区间内的数据。做"价格100到200元的图书分页"这类筛选,ZSet几乎就是为这种场景准备的。

4. Redis分页查询核心实现:从命令到Spring Data Redis代码

4.1 读懂ZSet分页命令的语义

先用Redis命令把ZSet分页的核心语义搞清楚。以"按销量排行"为例:

ZREVRANGE book:rank:sales 0 9 WITHSCORES

这条命令的意思是:从book:rank:sales这个有序集合中,按score从大到小排序,取排名从0到9的数据,即第1页的10条。

注意ZSet的排名是从0开始计算的,这和Java数组下标一样。ZREVRANGE是倒序取(score从大到小),ZRANGE是正序取(score从小到大)。

它和MySQL分页的对应关系是:

MySQLRedis ZSet
ORDER BY sales DESC LIMIT offset, sizeZREVRANGE key offset offset+size-1
ORDER BY price ASC LIMIT offset, sizeZRANGE key offset offset+size-1
COUNT(*)ZCARD key

要判断"是否有下一页",Redis没有直接提供这样的方法,一般是用ZCARD获取总数量,然后计算offset + size >= total是否成立。

4.2 基于ZSetOperations的Service层实现

在SpringBoot中,我封装了一个分页查询方法。先放分页结果的统一返回类:

public class PageResult<T> { private List<T> list; private Long total; private Integer pageNum; private Integer pageSize; private Boolean hasNext; // 构造方法和getter/setter省略 }

核心Service实现如下:

@Service public class BookRankService { @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private BookMapper bookMapper; private static final String RANK_KEY = "book:rank:sales"; public PageResult<BookVO> getHotBooks(int pageNum, int pageSize) { // 页码从1开始,转换为Redis的start下标 long start = (long) (pageNum - 1) * pageSize; long end = start + pageSize - 1; // 分页取member(图书ID列表) Set<String> bookIds = stringRedisTemplate.opsForZSet() .reverseRange(RANK_KEY, start, end); // 获取总数判断是否有下一页 Long total = stringRedisTemplate.opsForZSet().zCard(RANK_KEY); boolean hasNext = end < total - 1; List<BookVO> books = new ArrayList<>(); if (bookIds != null && !bookIds.isEmpty()) { for (String bookId : bookIds) { Book book = bookMapper.selectById(bookId); if (book != null) { books.add(convertToVO(book)); } } } PageResult<BookVO> result = new PageResult<>(); result.setList(books); result.setTotal(total); result.setPageNum(pageNum); result.setPageSize(pageSize); result.setHasNext(hasNext); return result; } }

这里有个细节:从Redis取回的member是图书ID字符串,拿到ID之后还是要回MySQL查详情。小题大做?不是的,这是典型的"索引与数据分离"思路:ZSet中只存ID,不存完整图书信息,控制内存占用;ID从数据库批量查出详情后,还能走MySQL的主键索引,一次查询极快。

如果一本书删除后没有及时清理Redis中的member,这里bookMapper.selectById(bookId)可能返回null,所以要做空值判断,否则前端分页列表里会出现空位。

4.3 大offset优化:从跳页到游标

上面这种实现有一个隐性瓶颈:当用户不断往后翻页,比如翻到第100页(offset=990)时,ZSet内部的跳跃表需要从链表头开始遍历跳过前990个元素才能取到目标区间。数据量小无所谓,但数据量达到百万级时,深分页的延迟会明显上升。

这跟MySQL深分页LIMIT 1000000, 20慢是同一个道理。

图书购买系统的榜单查询通常不会有用户真的翻到几百页,但如果你的业务后续扩展到"全部图书分页浏览"这种场景,可以考虑把分页模式从"跳页"改成"游标":

上次请求返回的最后一本书的score值,下一页就用ZREVRANGEBYSCORE按score区间继续往后取,代码思路示意:

// 上一页最后一条的score作为游标 Double lastScore = getLastScoreFromPrevPage(); Set<String> nextPage = stringRedisTemplate.opsForZSet() .reverseRangeByScore(RANK_KEY, 0, lastScore - 1, 0, pageSize);

游标方式只能一页一页往下走,不能随意跳页,但它的性能不随页数增加而退化。图书榜单这种场景,我给前端同时提供pageNum/pageSize跳页模式和cursor滚动模式,根据业务需要二选一。

5. 接口层与前端交互:返回结构设计、Vue分页组件对接

5.1 统一返回结构与RESTful接口设计

后端接口不是直接把PageResult丢给前端就行,通常外面还要包一层统一的返回结构,让前端可以统一处理成功、失败、未登录等状态码。我设计的统一返回体:

public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }

Controller层接口:

@RestController @RequestMapping("/api/books") public class BookController { @Autowired private BookRankService bookRankService; @GetMapping("/hot") public Result<PageResult<BookVO>> getHotBooks( @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize) { if (pageNum < 1) { pageNum = 1; } if (pageSize < 1 || pageSize > 50) { pageSize = 10; } return Result.success(bookRankService.getHotBooks(pageNum, pageSize)); } }

接口设计为GET /api/books/hot?pageNum=1&pageSize=10。注意参数校验:页码最小是1,每页条数限制在50以内。如果不做限制,有人传pageSize=100000,虽然Redis不会崩,但返回的数据传输会给网络带宽造成压力,前端渲染也会卡死。

5.2 前端Vue + Element UI分页组件对接

前端我用的Vue 2 + Element UI,分页组件是el-pagination。模板部分:

<template> <div class="book-list"> <el-table :data="bookList" v-loading="loading"> <el-table-column prop="title" label="书名"></el-table-column> <el-table-column prop="author" label="作者"></el-table-column> <el-table-column prop="price" label="价格"></el-table-column> <el-table-column prop="sales" label="销量"></el-table-column> </el-table> <el-pagination background layout="total, prev, pager, next" :total="total" :page-size="pageSize" :current-page.sync="pageNum" @current-change="handlePageChange"> </el-pagination> </div> </template>

对应的逻辑部分,重点在于请求封装和竞态处理:

export default { data() { return { bookList: [], total: 0, pageNum: 1, pageSize: 10, loading: false, requestSeq: 0 }; }, mounted() { this.fetchBooks(); }, methods: { async fetchBooks() { // 用一个自增序列号防止响应乱序覆盖 const currentSeq = ++this.requestSeq; this.loading = true; try { const res = await axios.get('/api/books/hot', { params: { pageNum: this.pageNum, pageSize: this.pageSize } }); if (currentSeq !== this.requestSeq) { return; } if (res.data.code === 200) { this.bookList = res.data.data.list; this.total = res.data.data.total; } } finally { this.loading = false; } }, handlePageChange() { this.fetchBooks(); } } };

这里有一个实战中很容易踩的坑:快速翻页时的响应竞态。用户在第1页还没加载完的时候,迅速翻到第2页,如果第1页的响应比第2页晚到,前端会把旧数据展示在当前页面。我用了一个requestSeq自增序号,回调时判断当前请求是否还是最新一次,不是就直接丢弃,这样能保证展示的一定是最后一次请求的结果。

5.3 联调阶段的几个小经验

联调时最容易出问题的不是接口本身,而是参数名约定。后端我用了pageNum/pageSize,前端必须完全一致,否则会收到默认值,用户翻页"没有反应"。

另外建议后端接口无论成功失败都返回HTTP 200并在body中携带业务code,只在系统级异常(如网络断开、服务宕机)时才返回HTTP 500。这样前端的axios拦截器可以统一处理业务错误提示,而不是每个请求都去解析HTTP状态码。

分页组件的:current-page.sync修饰符要和@current-change事件配合好——current-page是当前页码,@current-change在用户点击页码时触发,两个必须绑定同一个数据,否则会出现"点了第5页,但地址栏参数还是第1页"的诡异现象。

6. 实战中踩过的坑:分页失效、序列化乱码、缓冲池占用

6.1 MyBatis-Plus分页插件失效的根因

做图书后台管理的时候,我用MyBatis-Plus作为ORM框架。写第一个分页查询时就遇到了"分页插件完全没生效"的问题——Page参数传了,但SQL打印出来没有LIMIT,接口把全表数据都返回了。

排查过程是这样的:

第一步,看依赖:确认引入了mybatis-plus-boot-starter。没问题。

第二步,看配置:分页插件需要自己声明一个MybatisPlusInterceptor的Bean,加上PaginationInnerInterceptor(注意是PaginationInnerInterceptor,不是PaginationInterceptor,后者在MyBatis-Plus 3.5.0以上版本已经废弃)。我一开始写的是废弃的那个类,自然没生效。

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

第三步,检查配置类是否被SpringBoot扫描到。我一开始把MybatisPlusConfig放在子包里面没有加@MapperScan,导致整个配置类没有加载。加上@Configuration并且保证它在主启动类的扫描范围内之后,分页才正常。

这个坑其实和Redis没有直接关系,但它提醒了我一件事:当你的项目同时用了MyBatis-Plus的分页和Redis的分页时,要明确每一层分页发生在哪。我的设计是:后台管理系统里,图书维护用MyBatis-Plus分页查数据库;前台商城列表展示,走Redis ZSet分页。两者职责分离,不要混在一个接口里,否则分页逻辑会互相干扰。

6.2 Redis键名出现乱码:序列化器没配对

用Spring Data Redis写入ZSet之后,我在可视化工具里看到的key是这样的:

\xac\xed\x00\x05t\x00\x0fbook:rank:sales

member显示也是乱码。这个问题的根源很明确:我使用了默认的RedisTemplate,value序列化器是JdkSerializationRedisSerializer,它在序列化字符串时会在前面加一段Java序列化的二进制头。

解决方法就是前面第2.3节写的那样,自定义RedisTemplate,统一使用StringRedisSerializer处理key。如果你已经写入了乱码数据,记得先删除旧key再重建,否则新旧数据会混在一起。

排查乱码问题时有一个技巧:可以用TYPE book:rank:sales命令查看key的类型,再用ZRANGE book:rank:sales 0 5直接看存储的原始内容。如果输出是二进制乱码而不是可读的图书ID,那基本可以断定是序列化器配置问题,不用再怀疑业务代码。

6.3 Redis非分页缓冲池占用很高的问题

项目运行一段时间后,我发现Redis进程的内存占用增长得很快,用redis-cli INFO memory查看,used_memory_dataset不算特别大,但used_memory_rss(进程占用的物理内存)很高,同时日志里有大量内存碎片告警。

再执行redis-cli --bigkeys扫描,发现存在几个超大key,其中一个ZSet挂了十几万条图书ID数据。正常情况下不行——如果所有图书都在一个key下,单key过大,写入和读取都会变慢,内存碎片率升高,最终导致maxmemory触发后出现淘汰抖动。

解决方案有两个方向:

一是给Redis设置内存淘汰策略。在redis.conf中配置:

maxmemory 512mb maxmemory-policy allkeys-lru

allkeys-lru表示所有key按LRU(最近最少使用)淘汰。图书榜单缓存即使被淘汰了,下一次请求时还能从MySQL重新预热,所以这种非强一致的数据用LRU策略很合适。

二是把单个大ZSet拆分成多个小ZSet。按图书分类拆分,热门榜拆成"文学类ZSet""科技类ZSet""少儿类ZSet",每个分类的ZSet只维护本分类的图书ID。这样每个key的数据量可控,内存碎片问题明显缓解,而且还能顺便支持"某个分类下按销量分页"的需求。

6.4 缓存与数据库的一致性提醒

Redis分页缓存最容易被忽视的就是数据一致性。图书下架了,但ZSet里还留着它的ID;图书改价格了,但按价格排行的ZSet里它的score还是旧价格——这些都会导致前端展示错误的数据。

我的做法是:所有图书的"写操作"(新增、修改、删除)在事务提交之后,同步更新Redis中受影响的ZSet:

  • 新增图书:ZADD加入对应榜单,score按初始值(如销量为0、上架时间为当前时间戳)
  • 修改销量/价格:ZADD更新对应榜单的score
  • 下架图书:ZREM从所有榜单中移除该图书ID

这套逻辑直接放在Service层,保证更新MySQL和更新Redis在一个方法内完成。对于极端并发场景,有人会用"延迟双删"策略:先删除缓存,再更新数据库,过几百毫秒后再删除一次缓存,防止缓存里写入脏数据。但在图书这种低并发写场景下,同步更新已经够用,不需要引入额外的消息队列来处理。

最后的实操总结:先把数据结构想清楚再动手

整个系统完整跑通之后,我最深的体会就是一个词:结构先行。图书购买系统里"列表分页"看似简单,其实牵扯到缓存选型、Redis数据结构、序列化配置、接口设计、前端联调、一致性维护整整一条链路。如果一开始就把这些问题考虑清楚,后面的开发就是一马平川;如果跟我一样边做边踩坑,后面要付出的debug成本绝对是翻倍的。

再分享一个小技巧:在动手写代码之前,先用redis-cli把核心命令手动敲一遍,确认数据写入、分页读取、排序变化都符合预期,再写Java代码。Redis的命令是理解Spring Data Redis封装逻辑的钥匙,命令行玩明白了,ZSetOperations那些方法一看就知道是什么意思。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询