BorrowService核心是业务层校验:先查bookId是否已被借、再查用户未还数是否超限;事务内先插入借阅记录再更新书状态;归还时需加return_date IS NULL条件防重复;分页用游标替代OFFSET;定时任务处理断网等中间态。

如何设计 BorrowService 处理借书逻辑
核心是控制“同一本书不能被重复借出”和“用户借阅数不超过上限”,不能只靠数据库唯一约束,得在业务层做校验。
- 先查
BorrowRecord表中该bookId是否已有未归还记录(returnDate IS NULL) - 再查该用户当前未归还记录数,对比系统设定的上限(如 5 本),用
SELECT COUNT(*)而非加载全部对象 - 校验通过后,才插入新
BorrowRecord,并更新Book.status为"BORROWED" - 所有操作必须包裹在
@Transactional中,避免中间状态不一致
为什么 updateBookStatus() 必须放在 insertBorrowRecord() 之后
顺序错会导致并发问题:两个线程同时判断书可借 → 同时更新 status → 同时插入记录 → 一本变两借。
- 正确顺序:
INSERT INTO borrow_record→ 成功后再UPDATE book SET status = 'BORROWED' - 如果先改 status,万一插入失败(如用户超限、网络中断),书就卡在“已借出”状态,无法恢复
- 更稳妥的做法是把 status 字段去掉,完全靠
SELECT COUNT(*) FROM borrow_record WHERE book_id = ? AND return_date IS NULL判断是否可借
归还时如何避免 returnDate 被多次覆盖
常见错误是直接执行 UPDATE borrow_record SET return_date = NOW() WHERE id = ?,但没加条件,导致已归还记录又被更新一次。
- 必须加上
AND return_date IS NULL条件:UPDATE borrow_record SET return_date = NOW() WHERE id = ? AND return_date IS NULL
- 执行后检查
updateCount:若为 0,说明该记录已归还过,抛出IllegalStateException("该借阅记录已归还") - 归还成功后,建议异步触发
UPDATE book SET status = 'AVAILABLE',或干脆删掉 status 字段,查表即知
借阅列表分页查询为什么不能用 OFFSET LIMIT
当借阅记录频繁增删时,LIMIT 20 OFFSET 100 可能跳过或重复显示某些记录,尤其在管理后台刷新时明显。
立即学习“Java免费学习笔记(深入)”;
- 改用游标分页:按
create_time DESC, id DESC排序,下一页传入上一页最后一条的create_time和id - 查询语句示例:
SELECT * FROM borrow_record WHERE create_time < ? OR (create_time = ? AND id < ?) ORDER BY create_time DESC, id DESC LIMIT 20
- 注意 MySQL 的
TIMESTAMP默认精度是秒,高并发下可能撞时间,建议用DATETIME(3)或直接依赖id主键排序
实际最难的不是写完功能,而是处理“张三借书后断网,页面没反馈,但记录已插入”的这类中间态——得靠定时任务扫 create_time > NOW() - INTERVAL 30 MINUTE AND return_date IS NULL 的异常记录,再人工核对。










