接手这个"无人共享图书借阅平台"的Java项目时,我还没意识到后面会踩这么多坑。表面上这是一个典型的图书管理业务系统,真正深挖下去,无人值守带来的分布式锁、会话保活、异常订单补偿、书格状态一致性,每个点都能单独写一篇排查实录。这篇博文就把整个从零搭建的过程拆开揉碎,从需求建模、核心模块设计到高并发借阅场景下的踩坑修复,完整复现一遍,希望能给正在做同类Java源码项目的朋友省点时间。
1. 无人共享借阅平台的核心需求域拆解:借什么书、谁来还、怎么确保书不丢
先别急着写代码。做这类平台型Java项目,第一件事是把"无人"两个字背后的业务语义想清楚。传统的图书管理系统是管理员坐在前台,借书还书都有人工确认环节,而共享图书平台完全依赖用户自助操作,平台方只能通过硬件设备和后端逻辑去约束用户行为。
这个平台的核心参与方有三类:借阅者、图书所有者(可能也是普通用户)、平台运营方。需求域可以拆成四个主要板块:
-
图书资源管理:图书上架、下架、库存状态维护、封面与ISBN信息管理。共享场景下,图书来源有两类,一是平台自营书库,二是用户个人共享上架。这两类书在后续的借阅规则、押金策略、损坏赔付上需要区分处理。
-
自助借阅流程:用户扫码或搜索找到图书、提交借阅申请、系统校验资格、分配具体书格、生成取书码、用户到柜取书。整个流程必须无人介入,因此每个环节都要设计超时和异常兜底。
-
自助还书流程:用户还书、选择空余书格、柜门打开、用户放入图书、系统确认还书成功、更新订单状态。这个流程比借阅更容易出问题,因为涉及物理世界的不确定性,比如用户放错格子、柜门故障、图书RFID读取失败等。
-
信用与违约管理:借阅超时未取、逾期未还、图书损坏丢失,都需要自动化的信用扣减和赔付结算体系。
在技术建模上,我们最终确定了五张核心业务表:用户表、图书表、书格表、借阅订单表、违规记录表。订单表是整个系统的核心,几乎所有并发问题都围绕订单状态流转展开。
订单状态机需要精心设计,借阅侧的流转是"待取书 -> 已借出 -> 已逾期 -> 已归还",还书侧是"待还书 -> 已确认归还 -> 已结算"。每个状态的迁移必须有明确的时间戳和操作来源,这样后续做超时任务、对账、异常补偿时才能有据可依。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型和项目骨架搭建:Spring Boot 3 + MyBatis-Plus + Redis + MySQL的组合逻辑
2.1 为什么选这套组合而不是更重的微服务架构
很多人一看到"无人共享平台"就想上微服务,把用户、图书、订单、支付拆成四个服务,再用Spring Cloud Alibaba做注册发现。如果你是学习性质或者中小规模部署,这个架构在当前阶段没有意义,反而引入分布式事务、服务间调用链追踪等一大堆复杂度。
这个项目我用的是单体内聚 + 模块化分包的单体架构,固定技术栈为Spring Boot 3.2.x + MyBatis-Plus 3.5.x + Redis 6.x + MySQL 8.x + Redisson 3.x。单体架构在这个阶段有明确优势:事务边界清晰,借书流程中扣减库存、创建订单、分配书格可以放在同一个本地事务里直接完成,不需要考虑分布式事务;部署运维简单,一个Jar包加一个Nginx就能跑完整套服务;调试方便,业务链路在同一进程内能快速定位问题。
那Redis用来做什么?有两个不可替代的用途。第一是分布式锁,后面会详细讲,借书和还书时对同一本书、同一个书格的操作必须加锁;第二是订单状态机和用户会话的缓存加速,高频读取的低变数据放到Redis里,能显著降低数据库压力。
2.2 项目目录结构与关键配置
分包设计上采用了按业务域划分的package结构,而不是传统的controller/service/mapper三层平铺:
code复制com.example.bookshare
├── common // 通用返回体、异常处理、工具类
├── config // Redis、Redisson、MybatisPlus、线程池配置
├── controller // 接口层
├── service // 业务逻辑层,接口与实现分离
├── mapper // 数据访问层
├── entity // 数据库实体
├── dto // 入参出参对象
├── enums // 订单状态、书格状态、用户角色枚举
├── task // 定时任务(超时未取、逾期提醒等)
├── mq // 事件生产者与消费者(基于Redis Stream)
└── lock // 分布式锁封装
配置文件里几个关键项需要注意。MyBatis-Plus的逻辑删除全局配置和乐观锁插件要提前配好,因为共享图书场景下可能会有管理后台删书、用户并发借同一本书这类操作,没有乐观锁很容易出现脏更新。数据源连接池我用的HikariCP,MySQL的隔离级别设置为读已提交(READ_COMMITTED),而不是MySQL默认的可重复读(REPEATABLE_READ)。原因在于借阅场景的核心诉求是防止超卖,并不需要事务内多次读取结果一致,读已提交配合行锁已经足够,还能减少间隙锁带来的死锁概率。
Redis方面,key设计要带着业务前缀和技术前缀,比如 book:stock:detail:{bookId}、order:lock:borrow:{bookId}、cell:status:{cellId}。所有key必须设置TTL,即使业务上看起来是永久数据,也需要加一个很长的过期时间作为兜底,防止Redis内存泄漏。
3. 图书共享领域的核心业务建模:书格、库存与订单状态机
3.1 书格模型:一个格子一本书的物理约束
实体书柜是共享借阅平台的物理载体,一个柜子通常有几十个格子,每个格子放一本书。格子门锁通过物联网模块控制,后端系统下发开锁指令,用户才能打开指定的柜门。
在设计数据模型时,书格表 book_cell 既要描述物理位置,也要维护实时状态。字段至少有:柜ID、格子编号、格子状态(空闲/占用/禁用/维护中)、当前存放图书ID、最后操作时间、开锁密码等。
一个关键业务规则是"一个格子同一时刻只能关联一本书",这既是物理约束也是逻辑约束。但数据库层面这个约束只能通过逻辑代码保证,没办法建唯一索引,因为在历史记录中同一个格子确实会关联过很多不同书籍。实现时要靠两条路保证:一是在还书分配格子和借书释放格子时加分布式锁,确保格子状态的并发安全;二是引入格子操作流水表,每次格子状态变更都记录一条流水,包括动作类型、操作前状态、操作后状态、操作人ID、时间戳,用于事后追溯和数据对账。
3.2 库存扣减方案:从普通减库存到预占模式
共享图书的库存模型和电商不一样。电商商品的SKU库存通常是一个数字,而这个平台的库存不是"图书可借总数量",而是具体的书格维度。比如一本《Java编程思想》有3本副本,分别放在柜A的1号格、柜B的5号格、柜C的9号格,用户借阅时必须确认自己愿意去哪个柜取书。
因此在提交借阅申请阶段,系统的库存扣减要做两个动作:一是对图书维度加锁,防止两人同时申请同副本;二是将某个具体书格标记为"预占"状态。预占不等同于已借出,只表示这个副本被某个订单锁定了。
预占期的设计直接关系到用户体验和资源利用率。如果用户提交申请后迟迟不来取书,格子一直预占会导致图书资源闲置。常规方案设定一个30分钟的预占有效期,超时未取则自动释放库存、取消订单,并对用户做一次违规记录。这个30分钟的超时释放由定时任务扫描实现,周期设在每分钟一次,扫描条件是订单状态为待取书且创建时间早于当前时间30分钟。
释放预占我使用的是先更新数据库订单状态,再释放Redis锁,最后更新格子状态的多段式操作,且完整执行在一个事务方法内。这样能确保不会有中间状态遗漏,即使某一步失败也能通过定时补偿任务兜底。
3.3 订单状态机的可扩展设计
订单状态机如果只用switch判断会非常难受,后续每加一种状态或一个流转条件,就要改动一堆散落的逻辑。更好的做法是引入状态机配置表 + 统一的流转入口。
这个平台的核心订单状态按方向分为两条主链路。借阅链路:WAIT_PICKUP(待取书) 到 BORROWED(已借出) 到 OVERDUE(已逾期) 到 RETURNED(已归还) 到 SETTLED(已结算)。还书异常链路需要额外记录:WAIT_RETURN、RETURN_ABNORMAL(还书异常)、DISPUTED(争议中)。我维护了一个枚举类,把"当前状态 + 触发事件 -> 下一状态 + 执行动作"集中映射,后续每次状态流转统一调用 orderStateMachine.transit(order, event),再
