基于 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,无密码。
步骤 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,支持序列化。
package com.example.bookstore.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 使用 JSON 序列化(支持复杂对象)
RedisSerializer<?> jsonSerializer = new GenericJackson2JsonRedisSerializer();
template.setDefaultSerializer(jsonSerializer);
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
return template;
}
}
- 解释:默认 StringRedisTemplate 只支持字符串;这里用 RedisTemplate 支持对象存储。JSON 序列化便于存储 Book 对象。
步骤 3: 图书实体与数据存储到 Redis
- Book 实体(Book.java):
package com.example.bookstore.entity;
import lombok.Data;
@Data
public class Book {
private Long id;
private String title;
private String author;
private Double price;
}
- BookService:存储图书到 Redis 的 Sorted Set(键:”books”,score 为 id 或 price 以支持排序)。
package com.example.bookstore.service;
import com.example.bookstore.entity.Book;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Service
public class BookService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String BOOKS_KEY = "books"; // Redis Sorted Set 键
// 添加图书(score 为 id,假设 id 自增)
public void addBook(Book book) {
ZSetOperations<String, Object> zSetOps = redisTemplate.opsForZSet();
zSetOps.add(BOOKS_KEY, book, book.getId()); // score = id
}
// 分页查询(使用 ZRANGE 模拟分页)
public List<Book> getBooksByPage(int page, int size) {
ZSetOperations<String, Object> zSetOps = redisTemplate.opsForZSet();
// ZRANGE start = (page-1)*size, end = page*size -1
Set<Object> booksSet = zSetOps.range(BOOKS_KEY, (page - 1) * size, page * size - 1);
return booksSet.stream()
.map(obj -> (Book) obj)
.collect(Collectors.toList());
}
// 获取总记录数
public long getTotalBooks() {
return redisTemplate.opsForZSet().size(BOOKS_KEY);
}
}
- 分页实现:使用 Sorted Set 的 ZRANGE,按 score 分页。page 从 1 开始。
- 为什么 Sorted Set:支持排序和范围查询,效率高于 List 的 LRANGE(List 为 O(N) 最坏情况)。
- 数据初始化:可在应用启动时(或测试方法)添加样例数据:
// 在服务中添加测试方法或 CommandLineRunner
public void initData() {
addBook(new Book(1L, "Java编程思想", "Bruce Eckel", 99.0));
addBook(new Book(2L, "Spring Boot实战", "Craig Walls", 89.0));
// ... 添加更多
}
步骤 4: 后端控制器(REST API)
BookController.java:提供分页 API,支持 ?page=1&size=10 参数。
package com.example.bookstore.controller;
import com.example.bookstore.entity.Book;
import com.example.bookstore.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
public class BookController {
@Autowired
private BookService bookService;
@GetMapping("/books")
public ResponseEntity<Map<String, Object>> getBooks(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
List<Book> books = bookService.getBooksByPage(page, size);
long total = bookService.getTotalBooks();
Map<String, Object> response = new HashMap<>();
response.put("books", books);
response.put("total", total);
response.put("currentPage", page);
response.put("totalPages", (total + size - 1) / size);
return ResponseEntity.ok(response);
}
}
- 解释:返回 JSON,包括图书列表、总记录、页码。使用 Spring 的 @RequestParam 处理分页参数。
步骤 5: 前端交互(简单 HTML + JS)
假设前端页面为 index.html(放在 src/main/resources/static/ 下,或单独前端项目)。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="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>
<table id="bookTable">
<thead>
<tr><th>ID</th><th>标题</th><th>作者</th><th>价格</th></tr>
</thead>
<tbody></tbody>
</table>
<div class="pagination">
<button id="prevBtn" disabled>上一页</button>
<span id="pageInfo"></span>
<button id="nextBtn">下一页</button>
</div>
<script>
let currentPage = 1;
const pageSize = 10;
async function fetchBooks(page) {
const response = await fetch(`/books?page=${page}&size=${pageSize}`);
const data = await response.json();
const tbody = document.querySelector('#bookTable tbody');
tbody.innerHTML = ''; // 清空表格
data.books.forEach(book => {
const tr = 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),随时告诉我!🚀