背景痛点:CRUD 堆出来的“玩具系统”
做毕业设计最怕老师一句:“你的系统跟玩具似的”。
我当年差点就踩坑:把“服装租赁”想成简单的“增删改查”,页面一摆,字段一填,以为大功告成。结果师兄一句反问把我噎住——“100 人同时租同一件限量礼服,你怎么保证不超租?”
痛点瞬间暴露:
- 只有 CRUD,没有业务闭环:下单、支付、归还、赔偿四个状态机全靠前端按钮硬切,后台无状态校验。
- 并发控制缺失:库存字段赤裸裸躺在商品表,高并发下直接负数。
- 异常流程没闭环:用户付完押金就掉线,衣服没人来取,库存永远被锁死。
想拿优秀,必须跳出“表驱动”思维,用真实业务流量去验证架构。
技术选型:为什么不是 Django,也不是纯 JPA
- 语言生态:实验室只给 Java 服务器,用 Python 部署还得自己装 uWSGI,多一事不如少一事。
- 框架成熟度:Spring Boot 的自动装配 + 监控全家桶,在毕设答辩现场可一键展示健康检查端点,老师一看“/actuator”就默认你懂运维。
- OR 映射策略:
- JPA 的懒加载在循环序列化时容易 N+1,毕设时间紧,没空调二级缓存。
- MyBatis-Plus 只加强原生 SQL,写库存扣减语句时可以精准
UPDATE ... WHERE stock > 0 AND version = #{version},减少心智负担。
一句话:能跑 SQL、能写存储过程、还能快速生成 CRUD 接口,MyBatis-Plus 对本科生最友好。
核心实现:库存扣减的原子化方案
业务模型简化
- 服装表 t_costume:id、name、total_stock、available_stock、version(乐观锁)。
- 订单表 t_order:id、costume_id、user_id、status、create_time。
乐观锁方案(适合读多写少)
// CostumeMapper.java int decreaseStock(@Param("id") Long id, @Param("version") int version); <!-- CostumeMapper.xml --> <update id="decreaseStock"> UPDATE t_costume SET available_stock = available_stock - 1, version = version + 1 WHERE id = #{id} AND available_stock > 0 AND version = #{version} </update> // OrderService.java @Transactional(rollbackFor = Exception.class) public String createOrder(Long costumeId, Long userId){ Costume c = costumeMapper.selectById(costumeId); if(c.getAvailableStock() <= 0){ throw new BizException("已租完"); 非得自己写SQL,因为JPA自动生成的是“先查后改”,高并发下100%超租。悲观锁方案(秒杀场景)
@Transactional public String createOrderPessimistic(Long costumeId, Long userId){ // SELECT ... FOR UPDATE 锁住行 Costume c = costumeMapper.selectForUpdate(costumeId); if(c.getAvailableStock() <= 0) throw new BizException("已租完"); c.setAvailableStock(c.getAvailableStock() - 1); costumeMapper.updateById(c); // 写订单、写押金流水... }两段代码都保留统一异常捕获,保证幂等:订单表对 (user_id, costume_id, status='PRE_PAY') 建唯一索引,重复点击直接返回“订单已存在”。
性能与安全:把“并发”与“渗透”一起压测
压测结果:
- 4C8G 笔记本,100 线程循环租同一件商品,乐观锁方案 TPS 约 420,平均延迟 160 ms;悲观锁 TPS 降到 290,延迟 260 ms,但零超租。
- 库存热点行成瓶颈,引入 Redis 缓存“剩余件数”做前置漏斗,读 QPS 从数据库 4k 提升到缓存 2w,后端压力骤降。
JWT 刷新策略:
- 访问令牌 15 min,刷新令牌 2 h,存 Redis 并设“续期窗口”——剩余 5 min 内访问才给新令牌,防止无限制滚动。
- 用户表 mobile、id_card 字段返回前端前统一走 Jackson 脱敏序列化器:
@JsonSerialize(using = MaskSerializer.class) private String mobile;
越权防护:
订单详情接口加@PreAuthorize("#order.userId == authentication.name"),Spring Security 直接帮你挡非法拉单。
生产避坑指南:毕设也要讲“可运维”
本地事务失效:
默认@Transactional只在 public 方法生效;同类的内部调用this.xxx()会绕掉代理。解决:抽 Service 层,或者开 AspectJ。缓存双写不一致:
采用“先写库,再删缓存”+ 延迟双删策略,删失败则抛事件到 RabbitMQ 重试,保证最终一致。测试数据:
写 Maven 插件java-faker,在@PostConstruct里批量插入 10w 服装,再使用 MySQL 的ORDER BY RAND()把库存随机打散,压测更贴近真实。日志与排障:
给关键步骤打MDC.put("orderId", ...),ELK 聚合时能把同一次租赁链路串起来,答辩现场可现场检索错误日志,老师直呼专业。
可扩展方向:多租户 SaaS & 小程序
把数据库拆成tenant_id水平分表,MyBatis-Plus 的TenantLineInterceptor一行配置即可自动追加条件;再配合微信小程序云开发,前端直接调云函数,后端只要暴露内网 HTTPS,证书都省了。
写在最后
整套流程跑通后,我的毕设被答辩组直接评为优秀——不是因为界面炫酷,而是能拿出压测报告、事务脚本、异常日志,证明系统真的“抗打”。
如果你也在做服装租赁或类似共享库存项目,不妨先思考:
- 如何把单租户改成 SaaS,让校门口其他服装店老板也能注册即用?
- 或者动手把前端搬到微信小程序,利用云开发免运维,你会更深刻理解“全栈”到底意味着什么。
代码已上传 GitHub,去把并发锁改成 Redisson 分布式锁,再测一遍 TPS,你会看到另一幅风景。祝编码顺利,答辩不慌。