简介:适合高校软件专业学生与 Java Web 初学者的图书管理系统毕业设计文档,面向互联网应用开发场景,重点解决学校图书管理中读者信息、图书入库与借还流程繁琐等问题。包内为 1 份 docx 设计文稿,共 2.34MB,内容完整呈现从需求分析、可行性评估到总体设计、数据库设计与系统实现的整个过程。系统采用 JSP、Struts 框架与 MVC 模式,后端以 SQL Server 存储并通过 JDBC 连接,涵盖系统设置、读者管理、图书管理、图书借还、系统查询、更改口令六大功能模块,同时对图书信息表、借阅信息表等核心表结构作出详细设计。读者可借助该文档梳理论文写作脉络,了解 Java Web 管理类项目的分层架构与编码思路,也可作为课程设计或毕业设计的参考模板。目前已有 384 人学习下载,适合需要快速搭建系统或对照撰写设计说明的读者。
1. 这个“基于Java Web的图书管理系统”,到底解决谁的什么问题
前两天一个学弟发来消息,说毕设题目是“基于Java Web的图书管理系统的设计与实现.docx”,问我网上模板一抓一大把,为什么自己照着敲还是跑不起来。我没直接回他,先问了一句:你们的图书管理系统,是给谁用的?管理员在后台录书、读者在前台搜书借书还书、超期了要能查出来——这三件事跑通了,这个题目就立住了。Java Web这个限定词意味着你不玩Python的Flask,也不碰PHP那套,老老实实用Servlet、JSP、Tomcat、MySQL把一条完整链路走通。这篇文章就干一件事:把这个链路里从建库到部署的所有关键环节拆开,每一个步骤给到能抄的参数和代码,顺便把那些让你一夜回到解放前的坑提前说清楚。写给三类人:做毕设的、想交课程设计的、还有真要在内网搭个图书管理系统但不想上重型框架的开发者。
2. 技术选型与骨架搭建:为什么这个题目绕不开Java Web,目录怎么分
2.1 选型拆解:纯Servlet+JSP还是上SSM框架,先想清楚你的交付物是什么
先把这个题目里最容易被忽视的约束摆出来:标题写的是“基于Java Web”,不是“基于Spring Boot”。这两个东西差别很大。Spring Boot本质上也是Java Web,但你在答辩时如果讲不清楚“为什么不用Servlet非要上框架”,反而容易挨问。“基于Java Web”最正统的解读是Java EE那一套体系,即Servlet作为控制器、JSP作为视图层、JDBC或者MyBatis作为数据访问层。实际情况是,很多学校的毕设题目是多年前定的,当时Spring Boot还没像今天这么普及,题目里的“Java Web”默认就指Servlet+JSP+JDBC。
我的建议很简单:除非老师明确允许用Spring Boot,否则就用Servlet+JSP+JDBC。原因不光是合规,更在于这套组合足够轻,代码量可控,逻辑都在明面上,每一个请求怎么进、怎么出、数据库连接怎么拿、怎么关,你都能讲清楚。这恰恰是答辩时老师最爱问的部分。SSM或者Spring Boot存在大量“约定优于配置”的封装,遇到问题时排查起来反而更吃力。
那题目里的“设计与实现”怎么体现?你在论文里把分层讲清楚——实体层、DAO层、Service层、Servlet层、JSP页面层,每一层职责单一、相邻层通过接口交互,这就是设计。实现部分就是让借书还书这个核心流程能跑通。后半篇文章我会按这个分层逐一展开。
2.2 项目目录结构:一个能扛住答辩的Java Web工程长什么样
很多人建工程是从网上扒一个压缩包下来,解压之后目录乱成一团,连哪个class对应哪个页面都要猜半天。这里不按Maven的标准骨架说,因为很多毕设环境默认用Eclipse或IDEA里的Dynamic Web Project模板,那我就按这个模板给你一个可行的目录结构,你直接照着建。
BookSystem/ ├── src/ │ ├── com/book/entity/ // 实体类:Book、Reader、BorrowRecord、Admin │ ├── com/book/dao/ // 数据访问层:BookDao、ReaderDao、BorrowDao、AdminDao │ ├── com/book/service/ // 业务层:BookService、BorrowService、AdminService │ ├── com/book/servlet/ // 控制器层:LoginServlet、BookServlet、BorrowServlet │ ├── com/book/util/ // 工具类:DBUtil、DateUtil │ └── com/book/filter/ // 编码过滤器:EncodingFilter ├── WebContent/ │ ├── admin/ // 管理员页面:book_list.jsp、book_add.jsp、borrow_list.jsp │ ├── reader/ // 读者页面:index.jsp、book_search.jsp、my_borrow.jsp │ ├── WEB-INF/ │ │ ├── web.xml │ │ └── lib/ // mysql-connector、druid、jstl等jar包 │ ├── css/ │ ├── js/ │ ├── login.jsp │ └── index.jsp └── sql/ └── book_system.sql每层只做一件事:entity里的类对应数据库表的字段,dao只负责SQL和执行,service里写业务规则,servlet里只做参数接收、调用service、跳转页面,JSP拿数据展示。这四层职责不乱串,答辩时你讲架构图一句话的事。项目里的sql目录是很多人不建的,我强烈建议你建,把建库脚本放进去,既是交付物的一部分,也是你换电脑之后的最强后悔药。
2.3 开发环境与版本搭配:JDK、Tomcat、MySQL的版本配不对,后面全白干
环境搭建这一块,出问题最多不在于软件难装,而在于版本之间互相不认。这里是经过大量实践验证的版本组合,照抄即可。JDK用1.8,Tomcat用8.5或者9.0,MySQL用5.7或者8.0,连接器用对应版本。具体对应关系是:Tomcat 8.5支持Servlet 3.1,JDK 8完全够用;如果MySQL是8.0,那mysql-connector-java必须用8.0.x版本,同时驱动类名要写成com.mysql.cj.jdbc.Driver,并且连接URL里必须带时区参数,这是老版本连接器和新版本MySQL之间最大的坑。
数据库连接的配置我放在DBUtil里统一管理,使用Druid连接池,配置文件放src/druid.properties。注意:如果你用的是8.0版本的MySQL,驱动类、URL时区、连接器版本这三样必须一起改,缺一个都会在启动时报错。
# druid.properties driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/book_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username=root password=your_password initialSize=5 minIdle=2 maxActive=20 maxWait=60000serverTimezone=Asia/Shanghai是MySQL 8.0的硬性要求,不写就会报Server returns invalid timezone。initialSize=5表示启动时初始化5个连接,maxActive=20是最大连接数。图书管理系统并发量不大,20足够,调大了反而浪费数据库连接资源。
3. 数据库设计与DAO层:图书管理系统的数据地基怎么打
3.1 表结构设计:四张核心表怎么建,为什么我建议逻辑外键
图书管理系统的数据模型不复杂,核心就四张表:管理员表、图书表、读者表、借阅记录表。管理员表不跟借阅记录发生关系,它是独立的登录凭证。图书表和读者表是一对多关系——一本书可以被不同读者在不同时间借阅,但同一时刻只能被一个人持有。借阅记录表是图书和读者之间的关联实体,每次借书生成一条记录,还书时更新这条记录的还书时间和状态。
下面是建库建表SQL。注意我在借阅记录表里没有加物理外键约束,注释里写了原因,这个是后面避坑章节要展开的重点,先按这个建。
CREATE DATABASE book_system DEFAULT CHARACTER SET utf8mb4; CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, book_name VARCHAR(200) NOT NULL, author VARCHAR(100), publisher VARCHAR(100), isbn VARCHAR(30) UNIQUE, total_count INT DEFAULT 1, available_count INT DEFAULT 1, create_time DATETIME ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE reader ( id INT PRIMARY KEY AUTO_INCREMENT, reader_no VARCHAR(30) UNIQUE, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, phone VARCHAR(20), max_borrow_count INT DEFAULT 5, current_borrow_count INT DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, reader_id INT NOT NULL, book_id INT NOT NULL, borrow_time DATETIME, due_time DATETIME, return_time DATETIME, status TINYINT DEFAULT 0 COMMENT '0-借出中 1-已归还 2-已逾期', -- 有意不加外键约束,原因见避坑清单 INDEX idx_reader (reader_id), INDEX idx_book (book_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;available_count这个字段是借书逻辑的关键。每次借书成功,它减1;还书成功,它加1。判断一本书能不能借,就看available_count是否大于0。这是用空间换简单的方式,查询在役图书时直接WHERE available_count > 0,不用关联借阅记录表统计,性能和写法都更友好。
3.2 连接池参数:Druid的核心配置项,按这个调不吃亏
Druid连接池在Java Web里的使用率很高。它比DBCP和C3P0的优势在于自带监控统计功能,答辩的时候你可以多说一句“用了Druid可以看SQL执行统计”。具体配置已经在前面写过了,这里补充三个关键参数:maxWait=60000是拿连接的超时时间,单位毫秒,如果60秒拿不到连接就抛异常,避免线程无限等下去;testWhileIdle=true是空闲时检测连接是否有效,默认值就是true;还有一个validationQuery=SELECT 1需要加上,否则空闲检测不知道以什么SQL为准。
// DBUtil.java public class DBUtil { private static DruidDataSource dataSource; static { try { Properties props = new Properties(); props.load(DBUtil.class.getClassLoader().getResourceAsStream("druid.properties")); dataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError("Druid初始化失败: " + e.getMessage()); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }DBUtil用静态代码块初始化,类第一次被加载时就会执行,保证连接池只建一次。后续所有DAO拿连接都走这个类,不会出现“这里new一个连接、那里又new一个”的失控局面。注意一个细节:Druid的DruidDataSourceFactory.createDataSource(props)要求driverClassName、url、username、password这几个key必须和配置文件里完全一致,拼错任何一个key,Druid不会报错,但连接池会静默失败,等实际拿连接的时候才炸,排查起来很恶心。
3.3 DAO层实现:用一个BookDao把JDBC的重复代码收干净
很多新手写DAO,每个方法里都是getConnection、PreparedStatement、ResultSet、close四件套,一个类写完两三页,哪哪都是复制粘贴。这里给一个简化版BookDao,用PreparedStatement防SQL注入,把关闭资源的动作放到finally块里。
// BookDao.java public class BookDao { public List<Book> findBooks(String keyword, int offset, int limit) { String sql = "SELECT * FROM book WHERE book_name LIKE ? OR author LIKE ? ORDER BY id DESC LIMIT ?, ?"; List<Book> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); ps.setString(2, "%" + keyword + "%"); ps.setInt(3, offset); ps.setInt(4, limit); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Book b = new Book(); b.setId(rs.getInt("id")); b.setBookName(rs.getString("book_name")); b.setAuthor(rs.getString("author")); b.setPublisher(rs.getString("publisher")); b.setIsbn(rs.getString("isbn")); b.setTotalCount(rs.getInt("total_count")); b.setAvailableCount(rs.getInt("available_count")); list.add(b); } } } catch (SQLException e) { throw new RuntimeException("查询图书失败", e); } return list; } public int countBooks(String keyword) { String sql = "SELECT COUNT(*) FROM book WHERE book_name LIKE ? OR author LIKE ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); ps.setString(2, "%" + keyword + "%"); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { return rs.getInt(1); } } } catch (SQLException e) { throw new RuntimeException("统计图书数量失败", e); } return 0; } }JDK 7之后的try-with-resources语法让代码看着舒服了很多,Connection、PreparedStatement、ResultSet都实现了AutoCloseable,能自动释放。三个参数offset和limit配合实现分页,keyword用LIKE模糊匹配。这里有个性能习惯要养成:查询只取需要的字段,不要什么都SELECT *,这个库表结构简单就算了,以后做真实项目要把这个习惯带在身上。
4. 从借书到还书:业务层与页面层把主流程跑通
4.1 Service层:事务边界划在哪,才不至于借了书却扣不了库存
Service层是整个系统里最容易翻车的地方,因为事务的坑几乎全都埋在这里。借书这个动作,在业务上包含两件事:往borrow_record表插一条记录,同时把book.available_count减1。这两件事必须同时成功或者同时失败。如果先减库存再插记录,插记录失败,库存就莫名其妙少了;先插记录再减库存,减库存失败,书就凭空多了一本已借出记录。
用Connection.setAutoCommit(false)手动开启事务,两条SQL都用同一个Connection执行,最后统一commit或rollback,这是Servlet+JDBC时代最正统的写法。
// BorrowService.java public void borrowBook(int readerId, int bookId) { String insertRecord = "INSERT INTO borrow_record (reader_id, book_id, borrow_time, due_time, status) VALUES (?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 30 DAY), 0)"; String updateBook = "UPDATE book SET available_count = available_count - 1 WHERE id = ? AND available_count > 0"; String updateReader = "UPDATE reader SET current_borrow_count = current_borrow_count + 1 WHERE id = ? AND current_borrow_count < max_borrow_count"; try (Connection conn = DBUtil.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps1 = conn.prepareStatement(insertRecord); PreparedStatement ps2 = conn.prepareStatement(updateBook); PreparedStatement ps3 = conn.prepareStatement(updateReader)) { ps1.setInt(1, readerId); ps1.setInt(2, bookId); int recordRows = ps1.executeUpdate(); ps2.setInt(1, bookId); int bookRows = ps2.executeUpdate(); ps3.setInt(1, readerId); int readerRows = ps3.executeUpdate(); if (recordRows == 1 && bookRows == 1 && readerRows == 1) { conn.commit(); } else { conn.rollback(); throw new RuntimeException("借书失败:库存不足或超出可借数量"); } } catch (SQLException e) { conn.rollback(); throw new RuntimeException("借书异常,已回滚", e); } } catch (SQLException e) { throw new RuntimeException("获取连接失败", e); } }三个executeUpdate的返回值分别是影响行数。UPDATE book SET available_count = available_count - 1 WHERE id = ? AND available_count > 0这行SQL是灵魂:库存为0时影响行数为0,直接在数据库层面拦住了超借。同样的,读者表加了current_borrow_count < max_borrow_count条件,防止超量借书。三个操作影响行数全是1才提交,任何一个不是1就整体回滚。这里的事务边界就是“借书”这个业务动作的完整范围。
4.2 控制器与页面:登录、图书检索、借还书的前后端数据流怎么接
Servlet层的作用是接收参数、调Service、把结果写到Request或Session里、然后转发或重定向到JSP。一个常见的坏味道是把大量业务代码堆在Servlet里,这里用LoginServlet给你看标准写法。
// LoginServlet.java @WebServlet("/login") public class LoginServlet extends HttpServlet { private AdminService adminService = new AdminService(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); Admin admin = adminService.login(username, password); if (admin != null) { req.getSession().setAttribute("admin", admin); resp.sendRedirect(req.getContextPath() + "/admin/book_list.jsp"); } else { req.setAttribute("errorMsg", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } }@WebServlet("/login")是Servlet 3.0的注解方式,省掉在web.xml里写一长串<servlet>和<servlet-mapping>。注意/login前面不能带项目名,req.getContextPath()会自动拼上,避免硬编码。登录失败用forward到login.jsp,这样Request里的errorMsg能在页面上显示;登录成功用sendRedirect,这样刷新页面不会重复提交表单。借书和还书的Servlet结构完全一样,一个@WebServlet("/borrow")一个@WebServlet("/returnBook"),前面Service层的方法准备好,Servlet里只做参数解析、调用和跳转。
4.3 跑通主流程后自测:哪些环节最容易像黑匣子一样让人抓瞎
这套东西跑通之后,我建议你先自己走五条路径,而不是直接拿给同学测试:第一,正常登录,正确密码和错误密码各一次;第二,正常搜书,关键词有结果和没结果各一次;第三,借一本库存不为0的书,确认库存减1、借阅记录多一条;第四,借一本库存为0的书,确认它借不出去且页面有提示;第五,还书,确认库存加1、记录状态变成已归还。这五条全过,主流程就是稳的。
第3和第4条很容易出现只走了表面、没验证数据库的情况。页面提示“借书成功”,数据库里库存没变,这种就是典型黑匣子翻车。每次测试完直接去看一眼book表的available_count和borrow_record表的最新记录,别只瞄页面。
5. 避坑清单:图书管理系统开发与部署的5个常见翻车点
5.1 中文乱码乱成一锅粥:前端传进来是好的,数据库里就变问号
现象:JSP页面显示正常,但往数据库里插入中文书名后,查出来全是??。
原因:三层编码不一致导致的。页面是UTF-8,Servlet接收参数时没有设置request.setCharacterEncoding("UTF-8"),Tomcat默认按ISO-8859-1解码;数据库那边表虽然建成了utf8mb4,但连接URL里缺了characterEncoding=utf8。
解决:三层全改成UTF-8。第一层,JSP页面头部写pageEncoding="UTF-8",表单用POST提交;第二层,写一个EncodingFilter,在doFilter里对每个请求和响应统一设置编码:
// EncodingFilter.java public class EncodingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); chain.doFilter(request, response); } }第三层,连接URL里带上useUnicode=true&characterEncoding=utf8。这个过滤器在web.xml里配置,映射到/*。这里要特别强调:这个Filter一定要注册,否则你前面两个地方都改了,请求参数还是乱码,因为Servlet容器默认按ISO-8859-1处理POST参数,这个玄学问题几乎每个做图书管理系统的人都遇到过。
5.2 404和500交替轰炸:注解写了却进不去页面,问题藏在路径细节里
现象:点击“图书管理”菜单,浏览器地址栏变成http://localhost:8080/admin/book_list.jsp,页面报404;或者访问Servlet直接500。
原因:404分两种情况,一种是路径写错了,比如JSP放在WebContent/admin/下,访问时写成了admin/manager/book_list.jsp;另一种是Servlet映射冲突,比如两个Servlet都映射到/book,Tomcat启动时不会报错,但访问时它不知道该找谁。500通常是Servlet里抛了空指针或者ClassNotFoundException,最常见的是jar包没放进WEB-INF/lib。
解决:404先确认路径和实际文件位置一一对应,多一层少一层都会翻车;500去Tomcat的logs/localhost.日期.log里看堆栈,不要只看浏览器里的错误页。这里给个血泪经验:WEB-INF下面的JSP不能通过浏览器地址栏直接访问,只能通过Servlet内部forward到达。你要是把页面放到了WEB-INF/pages/下面,所有写jsp/book_list.jsp的地方都会404,这不是路径错,是Servlet容器的安全机制。
5.3 启动连接池报错:MySQL 8的时区和驱动,两个坑连着踩
现象:Tomcat一启动报Cannot create PoolableConnectionFactory,堆栈里有Public Key Retrieval is not allowed或者Server returns invalid timezone。
原因:MySQL 8.0的驱动和连接方式跟5.7不一样。Public Key Retrieval is not allowed是8.0默认使用caching_sha2_password认证,需要客户端在第一次连接时获取公钥;时区报错则是连接串里没有指定serverTimezone。
解决:连接URL里加两个参数,serverTimezone=Asia/Shanghai和allowPublicKeyRetrieval=true。这两个是MySQL 8.0的专属需求,5.7不需要,如果你用的是8.0而且不想加allowPublicKeyRetrieval=true,可以在MySQL端把用户的认证插件改成mysql_native_password,但最省事的是在URL里加参数。还有个容易被忽略的:8.0的驱动类名是com.mysql.cj.jdbc.Driver,老版本是com.mysql.jdbc.Driver,少了中间那个cj就会报ClassNotFoundException。
5.4 事务回滚不生效:三条SQL都执行了,报错后数据还是写进去了
现象:借书时第2条SQL故意写错,按理应该回滚,但查数据库发现第1条已经插进去了。
原因:三条PreparedStatement虽然写在一个方法里,如果分别从DBUtil.getConnection()拿了三次连接,那就是三个独立连接,commit和rollback根本不在同一个Connection上,怎么可能一起回滚。
解决:一个事务内所有SQL必须用同一个Connection对象。前面BorrowService的写法已经示范过了,在方法开头拿一次连接,setAutoCommit(false),所有preparedStatement都从这个conn创建,末尾统一commit或rollback。这里要说透一点:连接池里拿到的连接是复用的,后一个用户拿到的是前一个用户还回去的那条连接。如果你前一个用户没commit就关了连接,连接池回收时会把未提交的事务回滚掉,但你已经关了连接再想回滚就晚了。所以事务操作里,conn.close()必须放在commit或rollback之后。
5.5 外键与级联:删一本被借出的书,整个借阅记录直接失效
现象:管理员在后台把一本状态为“借出中”的书删了,成功提示。但读者端还能看到借阅记录,点进去图书信息全部为空,借阅记录变成了孤儿数据。
原因:如果建表时加了FOREIGN KEY (book_id) REFERENCES book(id)以及ON DELETE CASCADE,删书会把关联的借阅记录连坐删除,读者端那张借阅历史直接少一行,数据凭空消失。
解决:在设计阶段就选择不加物理外键,用逻辑外键——只建普通索引,不加REFERENCES约束,然后在Service层手动判断:删书之前先查borrow_record里有没有status=0且book_id=目标书的记录,有就拒绝删除,提示“该图书尚有借出记录,请先处理”。已经把物理外键加上去了的同学再加一个判断就好:
public boolean canDeleteBook(int bookId) { String sql = "SELECT COUNT(*) FROM borrow_record WHERE book_id = ? AND status = 0"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, bookId); try (ResultSet rs = ps.executeQuery()) { return !(rs.next() && rs.getInt(1) > 0); } } catch (SQLException e) { throw new RuntimeException("检查借阅记录失败", e); } }这样做的好处是,业务规则由自己控制,删除保护逻辑在代码里可见,不会像外键级联那样“静默处理”——数据库替你做了,但你可能根本不知道发生了什么。这也是为什么我在建表SQL里特意不加物理外键。面试或者答辩时如果被问,你就说“线上系统对数据删除操作要更谨慎,物理外键的级联策略不可控,所以在应用层实现引用完整性约束”,这比“老师教的原样照抄”要好得多。
6. 进阶:从“能跑”到“扛用”,给图书检索加一层Redis缓存
这个系统做到前面那一步,已经是一个完整可交付的项目了。但如果你想让它在性能或架构意识上看着更“高级”一点,有个性价比很高的改进:给图书列表的查询加一层Redis缓存。图书管理系统的数据量不大,加缓存的实际收益很有限,这点我不骗你。但答辩时能讲清楚缓存怎么回事、缓存和数据库的一致性怎么处理,比单纯多一个功能更能体现你的设计能力。我一般会这样做:把热门搜索的关键词对应的图书列表缓存进Redis,key是book:list:{keyword}:{page},value是JSON序列化后的列表,过期时间60秒。查图书的时候先查Redis,命中就直接返回,没命中再查数据库并把结果写回Redis。注意缓存的是“列表查询”,借书还书这种写操作不能只更新Redis、不同步数据库,正确做法是:写操作走数据库,然后删除对应关键词的缓存。删除比更新简单,下次查询会自然把新数据加载回Redis,这样逻辑上不出错。
public List<Book> searchWithCache(String keyword, int page, int limit) { String cacheKey = "book:list:" + keyword + ":" + page; String cached = jedis.get(cacheKey); if (cached != null) { return JSON.parseArray(cached, Book.class); } List<Book> result = bookDao.findBooks(keyword, (page - 1) * limit, limit); if (!result.isEmpty()) { jedis.setex(cacheKey, 60, JSON.toJSONString(result)); } return result; }这里jedis是Jedis客户端,60秒过期时间,setex是带过期时间的写入。加了这个之后,系统在“高频率搜索相同关键词”的场景下,数据库压力会明显下降。如果你是内网使用、几十个读者,这个优化可有可无;但写进论文里的意义在于,你展示了“知道系统瓶颈在哪、有什么手段解决、代价是什么”的工程判断力。我自己的习惯是,每次写完一个功能,会先跑一遍操作路径再谈下一步优化,先把路径覆盖全了再装饰功能。如果这一步走急了,后面排查问题时会付出几倍时间。这个教训是在一次线上事故里用加班换回来的,写在这里希望你少走这一步弯路。希望帮到你。
本文还有配套的精品资源,点击获取