基于 Spring Boot 的图书购买系统:Redis 中的数据以分页形式展示完整实现
在图书购买系统中,我们常常需要将图书数据缓存到 Redis 中(如热门图书列表),并支持分页展示。这可以提高查询效率,避免频繁访问数据库。本教程基于 Spring Boot 3.x(最新 LTS 版本),使用 Redis 作为缓存层,实现从配置到前后端交互的完整流程。
前提假设:
- 项目使用 Spring Boot + Redis + Spring Data Redis。
- 图书实体(Book):包含 id、title、author、price 等字段。
- 数据存储在 Redis 的 List 或 Sorted Set 中(推荐 Sorted Set 以支持排序和高效分页)。
- 分页策略:Redis 无原生分页,使用 ZRANGE(Sorted Set)或 LRANGE(List)模拟。基于工具搜索结果,Sorted Set 是高效选择(O(log N + M) 复杂度,M 为页面大小)。
- 前端:简单使用 HTML + JavaScript(Fetch API)调用 REST 接口展示分页数据。生产环境可换 Vue/React。
项目结构概览:
src/main/java ├── com.example.bookstore │ ├── BookstoreApplication.java │ ├── config/RedisConfig.java │ ├── entity/Book.java │ ├── service/BookService.java │ ├── controller/BookController.java resources/application.properties frontend/index.html // 前端页面(可选,放在 static 目录或单独前端项目)步骤 1: 项目初始化与依赖配置
创建 Spring Boot 项目:
- 使用 Spring Initializr(https://start.spring.io/)创建项目。
- 选择:Spring Boot 3.2.x、Java 17+、依赖:Spring Web、Spring Data Redis、Lombok(可选)。
添加依赖(pom.xml):
<dependencies><!-- Spring Boot Starter --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Redis --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- Lombok 简化代码 --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency></dependencies>- 安装 Redis:
- Windows/macOS:下载 Redis 官方版或用 Docker:
docker run -d -p 6379:6379 redis。 - 默认端口 6379,无密码。
- Windows/macOS:下载 Redis 官方版或用 Docker:
步骤 2: Redis 配置
- application.properties配置 Redis 连接:
# Redis 配置 spring.data.redis.host=localhost spring.data.redis.port=6379 # 可选:密码、数据库 # spring.data.redis.password=yourpassword # spring.data.redis.database=0 # 缓存配置(可选,启用缓存) spring.cache.type=redis spring.cache.redis.time-to-live=600000 # 缓存 TTL 10 分钟- Redis 配置类(RedisConfig.java):自定义 RedisTemplate,支持序列化。
packagecom.example.bookstore.config;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.data.redis.connection.RedisConnectionFactory;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;importorg.springframework.data.redis.serializer.RedisSerializer;@ConfigurationpublicclassRedisConfig{@BeanpublicRedisTemplate<String,Object>redisTemplate(RedisConnectionFactoryconnectionFactory){RedisTemplate<String,Object>template=newRedisTemplate<>();template.setConnectionFactory(connectionFactory);// 使用 JSON 序列化(支持复杂对象)RedisSerializer<?>jsonSerializer=newGenericJackson2JsonRedisSerializer();template.setDefaultSerializer(jsonSerializer);template.setKeySerializer(RedisSerializer.string());template.setHashKeySerializer(RedisSerializer.string());template.setValueSerializer(jsonSerializer);template.setHashValueSerializer(jsonSerializer);returntemplate;}}- 解释:默认 StringRedisTemplate 只支持字符串;这里用 RedisTemplate 支持对象存储。JSON 序列化便于存储 Book 对象。
步骤 3: 图书实体与数据存储到 Redis
- Book 实体(Book.java):
packagecom.example.bookstore.entity;importlombok.Data;@DatapublicclassBook{privateLongid;privateStringtitle;privateStringauthor;privateDoubleprice;}- BookService:存储图书到 Redis 的 Sorted Set(键:“books”,score 为 id 或 price 以支持排序)。
packagecom.example.bookstore.service;importcom.example.bookstore.entity.Book;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.data.redis.core.ZSetOperations;importorg.springframework.stereotype.Service;importjava.util.List;importjava.util.Set;importjava.util.stream.Collectors;@ServicepublicclassBookService{@AutowiredprivateRedisTemplate<String,Object>redisTemplate;privatestaticfinalStringBOOKS_KEY="books";// Redis Sorted Set 键// 添加图书(score 为 id,假设 id 自增)publicvoidaddBook(Bookbook){ZSetOperations<String,Object>zSetOps=redisTemplate.opsForZSet();zSetOps.add(BOOKS_KEY,book,book.getId());// score = id}// 分页查询(使用 ZRANGE 模拟分页)publicList<Book>getBooksByPage(intpage,intsize){ZSetOperations<String,Object>zSetOps=redisTemplate.opsForZSet();// ZRANGE start = (page-1)*size, end = page*size -1Set<Object>booksSet=zSetOps.range(BOOKS_KEY,(page-1)*size,page*size-1);returnbooksSet.stream().map(obj->(Book)obj).collect(Collectors.toList());}// 获取总记录数publiclonggetTotalBooks(){returnredisTemplate.opsForZSet().size(BOOKS_KEY);}}- 分页实现:使用 Sorted Set 的 ZRANGE,按 score 分页。page 从 1 开始。
- 为什么 Sorted Set:支持排序和范围查询,效率高于 List 的 LRANGE(List 为 O(N) 最坏情况)。
- 数据初始化:可在应用启动时(或测试方法)添加样例数据:
// 在服务中添加测试方法或 CommandLineRunnerpublicvoidinitData(){addBook(newBook(1L,"Java编程思想","Bruce Eckel",99.0));addBook(newBook(2L,"Spring Boot实战","Craig Walls",89.0));// ... 添加更多}步骤 4: 后端控制器(REST API)
BookController.java:提供分页 API,支持 ?page=1&size=10 参数。
packagecom.example.bookstore.controller;importcom.example.bookstore.entity.Book;importcom.example.bookstore.service.BookService;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.http.ResponseEntity;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RequestParam;importorg.springframework.web.bind.annotation.RestController;importjava.util.HashMap;importjava.util.List;importjava.util.Map;@RestControllerpublicclassBookController{@AutowiredprivateBookServicebookService;@GetMapping("/books")publicResponseEntity<Map<String,Object>>getBooks(@RequestParam(defaultValue="1")intpage,@RequestParam(defaultValue="10")intsize){List<Book>books=bookService.getBooksByPage(page,size);longtotal=bookService.getTotalBooks();Map<String,Object>response=newHashMap<>();response.put("books",books);response.put("total",total);response.put("currentPage",page);response.put("totalPages",(total+size-1)/size);returnResponseEntity.ok(response);}}- 解释:返回 JSON,包括图书列表、总记录、页码。使用 Spring 的 @RequestParam 处理分页参数。
步骤 5: 前端交互(简单 HTML + JS)
假设前端页面为 index.html(放在 src/main/resources/static/ 下,或单独前端项目)。
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title>图书购买系统 - 分页展示</title><style>table{border-collapse:collapse;width:100%;}th, td{border:1px solid #ddd;padding:8px;}.pagination{margin-top:10px;}button{margin:0 5px;}</style></head><body><h1>Redis 中的图书列表(分页)</h1><tableid="bookTable"><thead><tr><th>ID</th><th>标题</th><th>作者</th><th>价格</th></tr></thead><tbody></tbody></table><divclass="pagination"><buttonid="prevBtn"disabled>上一页</button><spanid="pageInfo"></span><buttonid="nextBtn">下一页</button></div><script>letcurrentPage=1;constpageSize=10;asyncfunctionfetchBooks(page){constresponse=awaitfetch(`/books?page=${page}&size=${pageSize}`);constdata=awaitresponse.json();consttbody=document.querySelector('#bookTable tbody');tbody.innerHTML='';// 清空表格data.books.forEach(book=>{consttr=document.createElement('tr');tr.innerHTML=`<td>${book.id}</td><td>${book.title}</td><td>${book.author}</td><td>${book.price}</td>`;tbody.appendChild(tr);});document.getElementById('pageInfo').textContent=`第${data.currentPage}页 / 共${data.totalPages}页 (总${data.total}本书)`;document.getElementById('prevBtn').disabled=data.currentPage===1;document.getElementById('nextBtn').disabled=data.currentPage>=data.totalPages;}// 按钮事件document.getElementById('prevBtn').addEventListener('click',()=>{if(currentPage>1){currentPage--;fetchBooks(currentPage);}});document.getElementById('nextBtn').addEventListener('click',()=>{currentPage++;fetchBooks(currentPage);});// 初始加载fetchBooks(currentPage);</script></body></html>- 解释:使用 Fetch API 调用后端 /books 接口,动态渲染表格和分页按钮。点击“上一页/下一页”触发请求。
步骤 6: 测试与运行
- 启动应用:运行 BookstoreApplication.java,确保 Redis 运行。
- 初始化数据:在服务中调用 initData() 或手动通过 Redis CLI 添加。
- 访问前端:浏览器打开 http://localhost:8080/index.html,查看分页效果。
- 验证 Redis:用 Redis CLI:
ZRANGE books 0 -1 WITHSCORES查看数据。
注意事项与优化
- 性能:分页大小(size)不宜太大(<100),避免 O(N) 开销。
- 错误处理:添加 try-catch 处理 Redis 连接异常。
- 生产优化:用 Lettuce 连接池(默认配置)、添加缓存注解 @Cacheable(如果结合数据库)。
- 扩展:结合数据库(JPA),Redis 只缓存热门图书;前端用 Element UI 或 Ant Design 提升 UI。
- 最佳实践(基于搜索结果):Sorted Set 适合带排序的分页;若无排序需求,可用 List + LRANGE。
这个实现覆盖了从配置到交互的全链路。如果需要完整源码或特定调整(如用 Vue),随时告诉我!🚀