引言
在当今互联网应用开发中,缓存技术已成为提升系统性能和用户体验的关键组件。Redis作为一款高性能的键值存储数据库,以其丰富的数据结构、快速的读写能力和灵活的扩展性,被广泛应用于各类系统的缓存层设计。本文将围绕一个基于Redis的图书管理系统展开,详细介绍如何利用Redis实现图书信息的增删改查功能,并通过前后端分离的架构设计,构建一个完整的图书管理解决方案。
通过本案例,读者将深入理解Redis在实际项目中的应用场景,掌握RedisTemplate的配置与使用技巧,学习前后端交互的接口设计规范,并领略如何将Redis的高性能特性与业务逻辑完美结合。无论是初入职场的开发新人,还是希望拓展技术栈的资深工程师,都能从本文中获取实用的开发经验和技术 insights。
1. 前后端交互接口设计
在前后端分离的开发模式中,接口设计是连接前端展示与后端逻辑的桥梁。合理的接口设计不仅能提高开发效率,还能增强系统的可维护性和扩展性。本图书管理系统基于RESTful架构设计了一套简洁明了的接口规范,主要包含以下核心接口:
1.1 接口规范与设计原则
- 接口命名:采用名词复数形式表示资源集合,如
/special/bookInfos
- 请求方法:遵循HTTP动词语义
- GET:获取资源(如查询图书)
- POST:创建资源(如添加图书)
- PUT:更新资源(如修改图书)
- DELETE:删除资源(如逻辑删除图书)
- 参数传递:
- 简单查询参数通过URL query string传递
- 复杂对象通过请求体(RequestBody)传递
- 返回格式:统一使用JSON格式,包含状态码、状态信息和数据内容
1.2 核心接口列表
接口名称 | 请求路径 | 请求方法 | 功能描述 |
---|---|---|---|
添加图书 | /special/addBookInfo |
POST | 向Redis中添加新的图书信息 |
根据ID查询图书 | /special/getBookInfoById |
GET | 根据图书ID获取详细信息 |
更新图书信息 | /special/updateSpecialBook |
POST | 更新图书的全部信息 |
逻辑删除图书 | /special/updateBookInfo |
POST | 修改图书状态为"无效" |
批量删除图书 | /special/batchDeleteBookInfoById |
POST | 批量修改多本图书的状态 |
分页查询图书列表 | /special/getSpecialListByPage |
GET | 按分页获取特价图书列表 |
1.3 接口交互示例
以"添加图书"接口为例,展示前后端数据交互流程:
前端请求(AJAX):
$.ajax({
type: "post",
url: "/special/addBookInfo",
data: $("#addBook").serialize(), // 序列化表单数据
success: function(result) {
if (result == "") {
location.href = "special_admin_list.html";
alert("添加成功");
} else {
console.log(result);
alert("添加失败:" + result);
}
},
error: function(error) {
console.log("请求失败:" + error);
}
});
后端响应(Controller):
@RequestMapping("/addBookInfo")
public String addBookInfo(BookInfo bookInfo) {
// 参数校验
if (bookInfo == null || !StringUtils.hasLength(bookInfo.getBookName())
|| !StringUtils.hasLength(bookInfo.getAuthor())
|| !StringUtils.hasLength(bookInfo.getPublish())
|| bookInfo.getPrice() == null || bookInfo.getCount() == null
|| bookInfo.getStatus() == null) {
return "输入的参数不合法,请检查后重新输入";
}
boolean b = true;
try {
b = specialDealAdminService.addBookInfo(bookInfo);
} catch (Exception e) {
log.error("添加图书发生报错e{}:" + e);
return "添加图书失败,请联系管理员";
}
return b ? "" : "controller 添加图书失败";
}
这种接口设计模式实现了前后端的松耦合交互,前端只需关注UI展示和用户交互,后端专注于业务逻辑处理和数据操作,极大地提高了开发效率和系统的可维护性。
2. 整体代码逻辑架构
在深入分析各模块代码之前,我们需要先建立对整个系统架构的宏观认知。本图书管理系统采用了典型的三层架构设计,结合Redis的高性能特性,形成了一套完整的技术解决方案。
2.1 系统架构概述
系统整体架构可分为以下几个层次:
- 表示层(前端):负责用户界面展示和交互,采用HTML、CSS、JavaScript和jQuery技术栈
- 控制层(Controller):处理前端请求,协调服务层完成业务逻辑
- 服务层(Service):封装核心业务逻辑,操作Redis数据
- 数据访问层:通过RedisTemplate实现与Redis数据库的交互
- 模型层(Model):定义数据实体类,如BookInfo
2.2 核心业务流程
以"添加图书"功能为例,展示系统的整体逻辑流转:
- 前端交互:用户在添加页面填写图书信息,点击"确定"按钮
- 请求发送:前端通过AJAX将表单数据发送至
/special/addBookInfo
接口 - 参数校验:Controller层对请求参数进行合法性校验
- 业务处理:Service层执行添加图书的核心逻辑
- 从Redis中获取现有图书ID,确定新图书的ID
- 生成创建时间和更新时间
- 将图书对象序列化为JSON格式存入Redis
- 结果返回:Service层返回操作结果,Controller层封装响应数据
- 前端反馈:根据响应结果,前端提示操作成功并跳转至列表页
2.3 Redis数据模型设计
在Redis中,图书数据采用以下存储策略:
- 键命名规范:使用
bookInfoId:{id}
的格式作为图书对象的键,如bookInfoId:100001
- 数据类型:采用String类型存储序列化后的JSON对象
- ID生成策略:使用
IdStatic
工具类生成递增ID,确保唯一性 - 状态标识:通过
status
字段标识图书状态(1-可借阅,2-不可借阅,0-无效,3-特价商品)
这种数据模型设计充分利用了Redis的键值存储特性,实现了快速的读写操作,同时通过合理的键命名和状态标识,为复杂查询和业务逻辑提供了支持。
3. Redis配置详解
Redis的正确配置是系统高性能运行的基础。在本系统中,我们通过Spring Data Redis提供的RedisTemplate
实现了对Redis的操作,下面详细介绍配置过程和关键参数。
3.1 RedisTemplate配置类
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
// 创建redisTemplate对象
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置连接工厂
redisTemplate.setConnectionFactory(connectionFactory);
// 创建JSON序列化工具
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
// 设置Key的序列化(使用StringSerializer)
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
// 设置Value的序列化(使用JSON序列化)
redisTemplate.setValueSerializer(jsonRedisSerializer);
redisTemplate.setHashValueSerializer(jsonRedisSerializer);
return redisTemplate;
}
}
3.2 配置参数解析
连接工厂(RedisConnectionFactory):
- 由Spring自动注入,负责创建与Redis服务器的连接
- 支持单机、哨兵、集群等多种部署模式
序列化配置:
- Key序列化:使用
StringSerializer
,确保键的可读性和唯一性 - Value序列化:使用
GenericJackson2JsonRedisSerializer
,将Java对象序列化为JSON格式 - 为什么选择JSON序列化?
- 跨语言兼容性好
- 可读性强,便于调试
- 支持复杂对象结构
- Key序列化:使用
模板功能:
RedisTemplate
提供了丰富的操作方法,如opsForValue()
、opsForHash()
等- 支持事务、管道等高级功能,提升批量操作性能
3.3 序列化方案对比
在Redis的使用中,序列化方案的选择至关重要。本系统采用JSON序列化而非默认的JDK序列化,主要基于以下考虑:
对比项 | JDK序列化 | JSON序列化(Jackson) |
---|---|---|
数据格式 | 二进制 | 文本(JSON) |
可读性 | 差(二进制不可读) | 好(JSON文本易读) |
跨语言支持 | 仅Java | 多语言支持 |
空间占用 | 较大 | 较小(压缩后更优) |
性能 | 较快 | 稍慢(但在实际应用中可接受) |
依赖 | 无额外依赖 | 需要Jackson库 |
在实际项目中,JSON序列化因其良好的可读性和跨语言特性,成为分布式系统中更常用的选择。而通过Jackson的优化配置,其性能损耗在大多数场景下是可以接受的。
4. 后端代码讲解
后端代码是整个系统的核心,负责处理业务逻辑和数据操作。本节将详细解析Controller层和Service层的实现细节,揭示如何利用Redis实现图书的增删改查功能。
4.1 Controller层实现
Controller层作为前端请求的入口,主要负责请求接收、参数校验和结果返回,不包含复杂的业务逻辑。以下是核心Controller的实现分析:
@Slf4j
@RequestMapping("/special")
@RestController
public class SpecialDealAdminController {
@Autowired
private SpecialDealAdminService specialDealAdminService;
// 添加图书
@RequestMapping("/addBookInfo")
public String addBookInfo(BookInfo bookInfo) {
// 参数合法性校验
if (bookInfo == null
|| !StringUtils.hasLength(bookInfo.getBookName())
|| !StringUtils.hasLength(bookInfo.getAuthor())
|| !StringUtils.hasLength(bookInfo.getPublish())
|| bookInfo.getPrice() == null
|| bookInfo.getCount() == null
|| bookInfo.getStatus() == null) {
return "输入的参数不合法,请检查后重新输入";
}
boolean b = true;
try {
b = specialDealAdminService.addBookInfo(bookInfo);
} catch (Exception e) {
log.error("添加图书发生报错e{}:" + e);
return "添加图书失败,请联系管理员";
}
return b ? "" : "controller 添加图书失败";
}
// 根据ID查询图书
@RequestMapping("/getBookInfoById")
public BookInfo getBookInfoById(Integer bookId) {
log.info("-------bookId:" + bookId);
if (bookId == null) {
return null;
}
BookInfo bookInfo = specialDealAdminService.getBookInfoById(bookId);
if (bookInfo == null) {
return null;
}
log.info("--------controller---bookInfo:" + bookInfo);
return bookInfo;
}
// 更新图书信息
@RequestMapping("/updateSpecialBook")
public String updateSpecialBook(BookInfo bookInfo) {
if (bookInfo == null) {
log.info("特价秒杀--update --传参为空");
return " 错误";
}
boolean b = specialDealAdminService.updateSpecialBook(bookInfo);
return b ? "" : "错误";
}
// 逻辑删除图书
@RequestMapping("/updateBookInfo")
public String updateBookInfo(BookInfo bookInfo) {
if (bookInfo == null) {
log.info("controller-special-单个删除传参为空");
return "错误";
}
boolean b = specialDealAdminService.updateBookInfo(bookInfo);
return b ? "" : "错误";
}
// 批量删除图书
@RequestMapping("/batchDeleteBookInfoById")
public String batchDeleteBookInfoById(@RequestParam("idList") List<Integer> idList) {
if (idList == null) {
log.info("controller--batchDelete--传来的参数为空");
return "错误";
}
BookInfo bookInfo = new BookInfo();
try {
for (Integer id : idList) {
bookInfo.setId(id);
bookInfo.setStatus(0); // 设置状态为"无效"
boolean b = specialDealAdminService.updateBookInfo(bookInfo);
if (b == false) {
log.info("批量删除错误");
return "错误";
}
}
} catch (Exception e) {
log.info("批量删除抛异常");
return "错误";
}
return "";
}
}
控制器实现了图书管理的核心功能,通过简洁的接口设计和明确的职责划分,将前端请求与业务逻辑分离。
4.2 Service和Mapper层实现
4.2.1 代码展示
Service层是业务逻辑的核心,负责与Redis交互并执行业务规则。本系统中,SpecialDealAdminService
类实现了所有图书管理相关的业务逻辑:
@Slf4j
@Service
public class SpecialDealAdminService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 从Redis中获取所有图书信息
public List<BookInfo> getAllBookInfoFromRedis() {
List<BookInfo> resultList = new ArrayList<>();
// 获取所有以"bookInfoId:"开头的键
String keyPattern = "bookInfoId:*";
Set<String> keys = redisTemplate.keys(keyPattern);
// 遍历键,获取对应的值
for (String key : keys) {
BookInfo value = (BookInfo) redisTemplate.opsForValue().get(key);
log.info("----------value-----" + value);
resultList.add(value);
}
return resultList;
}
// 添加图书
public boolean addBookInfo(BookInfo bookInfo) {
// 查找当前最大的ID
Integer maxId = 0;
String keyPattern = "bookInfoId:*";
Set<String> keys = redisTemplate.keys(keyPattern);
// 遍历获取最大ID
for (String key : keys) {
BookInfo value = (BookInfo) redisTemplate.opsForValue().get(key);
if (value.getId() > maxId) {
maxId = value.getId();
}
}
// 设置ID生成器的起始值
IdStatic.setId(maxId + 1);
try {
// 生成新ID
Integer id = IdStatic.getId();
bookInfo.setId(id);
bookInfo.setCreateTime();
bookInfo.setUpdateTime();
// 构造Redis键并存储
String idString = "bookInfoId:" + id;
redisTemplate.opsForValue().set(idString, bookInfo);
return true;
} catch (Exception e) {
log.error("redis-添加图书失败");
return false;
}
}
// 根据ID查询图书
public BookInfo getBookInfoById(Integer id) {
BookInfo bookInfo = null;
String keyPattern = "bookInfoId:*";
Set<String> keys = redisTemplate.keys(keyPattern);
// 遍历查找匹配ID的图书
for (String key : keys) {
BookInfo value = (BookInfo) redisTemplate.opsForValue().get(key);
if (id.equals(value.getId())) {
bookInfo = value;
return bookInfo;
}
}
return bookInfo;
}
// 更新图书信息
public boolean updateSpecialBook(BookInfo bookInfo) {
String keyPattern = "bookInfoId:*";
Set<String> keys = redisTemplate.keys(keyPattern);
// 查找并更新对应ID的图书
for (String key : keys) {
BookInfo value = (BookInfo) redisTemplate.opsForValue().get(key);
if (bookInfo.getId().equals(value.getId())) {
bookInfo.setUpdateTime();
// 替换bookInfo
redisTemplate.opsForValue().set(key, bookInfo);
log.info("--------service---bookInfo:" + bookInfo);
return true;
}
}
return false;
}
public boolean updateBookInfo(BookInfo bookInfo) {
String keyPattern = "bookInfoId:*";
Set<String> keys = redisTemplate.keys(keyPattern);
for (String key : keys) {
BookInfo value = (BookInfo) redisTemplate.opsForValue().get(key);
if (bookInfo.getId().equals(value.getId())) {
value.setUpdateTime();
value.setStatus(bookInfo.getStatus());
redisTemplate.opsForValue().set(key, value);
log.info("--------service---bookInfo:" + bookInfo);
return true;
}
}
return false;
}
}
4.2.2 Service层核心逻辑解析
- 数据获取策略:在多个方法中,通过
redisTemplate.keys("bookInfoId:*")
获取所有符合条件的键,再遍历键获取对应图书对象。这种方式在数据量较小时可行,但当Redis中存储大量图书数据时,可能会影响性能,后续可考虑使用Redis的scan命令进行优化,避免阻塞Redis服务器。 - ID生成机制:
addBookInfo
方法中,通过遍历现有图书ID找出最大值,再利用IdStatic
工具类生成新的唯一ID。这种方式在分布式环境下可能会出现ID重复问题,可引入Redis的INCR
命令或分布式ID生成算法(如雪花算法)来确保ID的唯一性和高效性。 - 数据更新逻辑:
updateSpecialBook
和updateBookInfo
方法看似功能相似,实则有细微差别。前者直接用传入的bookInfo
对象替换Redis中原有数据,适合图书信息全面更新的场景;后者仅更新图书的状态和更新时间,适用于仅修改图书状态的业务需求,体现了对不同业务场景的针对性处理。
4.2.3 与Redis交互的细节
RedisTemplate
提供的 opsForValue()
操作,是处理字符串类型数据的核心工具。在本系统中,将 BookInfo
对象直接作为值存储,得益于之前配置的 GenericJackson2JsonRedisSerializer
序列化器,它会自动将Java对象转换为JSON字符串存入Redis,读取时再反序列化为 BookInfo
对象,极大简化了数据处理流程。同时,在操作过程中,合理使用日志记录关键步骤,方便排查问题和监控系统运行状态。
5. 前端代码讲解
前端页面通过简洁直观的UI设计,结合JavaScript和AJAX技术,实现与后端接口的交互,完成图书的增删改操作。下面分别对各操作对应的前端代码进行详细解读。
5.1 删除操作
删除操作分为单个删除和批量删除,页面通过HTML表格展示图书列表,并提供复选框用于批量选择。
<table>
<thead>
<tr>
<td>选择</td>
<td class="width100">图书ID</td>
<td>书名</td>
<td>作者</td>
<td>数量</td>
<td>定价</td>
<td>出版社</td>
<td>状态</td>
<td class="width200">操作</td>
</tr>
</thead>
<tbody>
</tbody>
</table>
- 单个删除:
function deleteBook(id) {
var isDelete = confirm("确认删除?");
if (isDelete) {
// 删除图书
$.ajax({
type: "post",
url: "/special/updateBookInfo", // 逻辑删除,改图书的状态
data: {
id: id,
status: 0
},
success: function (result) {
if (result == "") {
alert("删除成功!");
location.href = "book_list.html";
} else {
alert("删除失败:" + result);
}
},
error: function (error) {
alert("请求失败,请联系管理员");
}
});
}
}
当用户点击删除按钮时,弹出确认框,确认后通过AJAX向后端 /special/updateBookInfo
接口发送请求,传递图书ID和要设置的状态(0表示无效),根据后端返回结果提示操作是否成功,并刷新页面。
- 批量删除:
function batchDelete() {
var isDelete = confirm("确认批量删除?");
if (isDelete) {
// 获取复选框的id
var ids = [];
$("input:checkbox[name='selectBook']:checked").each(function () {
ids.push($(this).val());
});
// 发送请求,批量删除
$.ajax({
type: "post",
url: "/special/batchDeleteBookInfoById?idList=" + ids, // 逻辑删除,改图书的状态
success: function (result) {
if (result == "") {
alert("批量删除成功!")
location.href = "book_list.html";
} else {
alert("删除失败");
}
},
error: function (error) {
console.log("请求失败");
}
});
}
}
批量删除时,先获取所有选中复选框的图书ID,拼接成参数发送到后端 /special/batchDeleteBookInfoById
接口,后端接收到ID列表后循环更新每本图书的状态,前端根据后端返回结果进行相应提示和页面刷新。
5.2 添加操作
添加页面通过表单收集图书信息,点击确定按钮触发添加逻辑。
<form id="addBook">
<div class="form-group">
<label for="bookName">图书名称:</label>
<input type="text" class="form-control" placeholder="请输入图书名称" id="bookName" name="bookName">
</div>
<div class="form-group">
<label for="bookAuthor">图书作者</label>
<input type="text" class="form-control" placeholder="请输入图书作者" id="bookAuthor" name="author">
</div>
<div class="form-group">
<label for="bookStock">图书库存</label>
<input type="text" class="form-control" placeholder="请输入图书库存" id="bookStock" name="count">
</div>
<div class="form-group">
<label for="bookPrice">图书定价:</label>
<input type="number" class="form-control" placeholder="请输入价格" id="bookPrice" name="price">
</div>
<div class="form-group">
<label for="bookPublisher">出版社</label>
<input type="text" id="bookPublisher" class="form-control" placeholder="请输入图书出版社" name="publish">
</div>
<div class="form-group">
<label for="bookStatus">图书状态</label>
<select class="custom-select" id="bookStatus" name="status">
<option value="3">特价秒杀</option>
</select>
</div>
<div class="form-group" style="text-align: right">
<button type="button" class="btn btn-info btn-lg" onclick="add()">确定</button>
<button type="button" class="btn btn-secondary btn-lg" onclick="javascript:history.back()">返回</button>
</div>
</form>
function add() {
$.ajax({
type: "post",
url: "/special/addBookInfo",
data: $("#addBook").serialize(),
success: function (result) {
if (result == "") {
location.href = "special_admin_list.html";
alert("添加成功");
} else {
console.log(result);
alert("添加失败:" + result);
}
},
error: function (error) {
console.log("请求失败:" + error);
}
});
}
点击确定按钮调用 add
函数,通过 $.ajax
发送POST请求到 /special/addBookInfo
接口,使用 $("#addBook").serialize()
将表单数据序列化为键值对形式发送,后端处理成功后,前端提示添加成功并跳转至图书列表页。
5.3 修改操作
修改页面同样以表单形式展示图书信息,并且在页面加载时自动填充原有数据。
<form id="updateBook">
<input type="hidden" class="form-control" id="bookId" name="id">
<div class="form-group">
<label for="bookName">图书名称:</label>
<input type="text" class="form-control" id="bookName" name="bookName">
</div>
<div class="form-group">
<label for="bookAuthor">图书作者</label>
<input type="text" class="form-control" id="bookAuthor" name="author">
</div>
<div class="form-group">
<label for="bookStock">图书库存</label>
<input type="text" class="form-control" id="bookStock" name="count">
</div>
<div class="form-group">
<label for="bookPrice">图书定价:</label>
<input type="number" class="form-control" id="bookPrice" name="price">
</div>
<div class="form-group">
<label for="bookPublisher">出版社</label>
<input type="text" id="bookPublisher" class="form-control" name="publish">
</div>
<div class="form-group">
<label for="bookStatus">图书状态</label>
<select class="custom-select" id="bookStatus" name="status">
<option value="3" selected>特价秒杀</option>
</select>
</div>
<div class="form-group" style="text-align: right">
<button type="button" class="btn btn-info btn-lg" onclick="update()">确定</button>
<button type="button" class="btn btn-secondary btn-lg" onclick="javascript:history.back()">返回</button>
</div>
</form>
$.ajax({
type: "get",
url: "/special/getBookInfoById" + location.search,
success: function (bookInfo) {
if (bookInfo != null) {
$("#bookId").val(bookInfo.id);
$("#bookName").val(bookInfo.bookName);
$("#bookAuthor").val(bookInfo.author);
$("#bookStock").val(bookInfo.count);
$("#bookPrice").val(bookInfo.price);
$("#bookPublisher").val(bookInfo.publish);
$("#bookStatus").val(bookInfo.status);
} else {
alert("用户访问失败,请联系管理员");
}
}
});
function update() {
$.ajax({
type: "post",
url: "/special/updateSpecialBook",
data: $("#updateBook").serialize(),
success: function (result) {
if (result == "") {
location.href = "/special_admin_list.html";
alert("修改成功!");
} else {
console.log(result);
alert("修改失败:" + result);
}
},
error: function (error) {
console.log("请求失败:" + error);
}
});
}
页面加载时,通过AJAX请求 /special/getBookInfoById
接口获取当前要修改图书的信息,并填充到表单中。用户修改信息后点击确定按钮,调用 update
函数,将表单数据发送到 /special/updateSpecialBook
接口,后端更新成功后,前端提示修改成功并跳转回图书列表页。
6. 总结
通过对基于Redis的图书管理系统的全栈开发实践,我们深入了解了Redis在实际项目中的应用方式,以及前后端交互的完整流程。从Redis的配置优化,到后端业务逻辑的实现,再到前端页面的交互设计,每个环节都体现了技术的巧妙运用和对业务需求的精准实现。
在Redis的使用上,合理的配置和数据模型设计是系统高效运行的关键,同时要注意数据操作的性能问题和分布式场景下的兼容性。后端代码通过分层架构,将业务逻辑与数据访问分离,提高了代码的可维护性和扩展性。前端页面则以用户体验为核心,通过简洁直观的交互设计和AJAX技术,实现了与后端的无缝对接。
然而,本系统仍存在一些可优化的空间。例如,Redis数据查询的性能优化、分布式环境下的ID生成方案改进、前端页面的响应式设计等。未来,可以进一步引入Redis的高级特性(如缓存淘汰策略、事务处理),结合更先进的前端框架(如Vue.js、React),打造功能更强大、性能更优越的图书管理系统。希望本文能为读者在Redis应用开发和全栈项目实践中提供有益的参考和启发。如果你在实际开发中遇到问题,或者有更多优化想法,欢迎在评论区交流讨论!