本文目录导读:

- 文章标题:从零到一:Java库存系统实战案例拆解——架构设计、核心代码与性能优化
- 目录导读
- 案例背景与需求痛点
- 系统架构设计:分层清晰,职责单一
- 核心业务模块实现:防超卖的精髓
- 高并发性能优化:从秒杀到平稳
- 常见问题与问答(FAQ)
- 总结与扩展思考
从零到一:Java库存系统实战案例拆解——架构设计、核心代码与性能优化
目录导读
- 案例背景与需求痛点:为什么传统Excel管理库存会崩溃?电商与制造业的核心诉求。
- 系统架构设计:Spring Boot + MyBatis Plus + Redis 三层架构如何落地。
- 核心业务模块实现:出入库事务、库存扣减防超卖(乐观锁+Redis原子操作)、流水追踪。
- 高并发性能优化:读写分离、缓存预热、异步对账策略。
- 常见问题与问答(FAQ):针对“库存负数”“缓存穿透”“分布式锁失效”的深度解答。
- 总结与扩展思考:从单体到微服务的演进路径。
案例背景与需求痛点
在真实的电商大促场景中,某日用品公司库存数据频繁出现“超卖”——后台显示有货,用户下单后却无法发货,其根源在于:多线程并发下,传统的select后update操作存在竞态条件,库存剩余10件,10个用户同时下单,每个线程都读到10,然后分别执行UPDATE stock SET count = count - 1,最终库存可能变为0甚至负数。
本案例的Java库存系统,需满足以下硬性指标:
- 支撑每秒1000+订单请求,库存准确率100%。
- 提供完整的操作日志(谁、什么时间、改了什么)。
- 支持分布式部署,防止单点故障。
系统架构设计:分层清晰,职责单一
采用经典Spring Boot 2.x + MyBatis Plus + MySQL + Redis组合,核心分层如下:
Controller层(接收请求)
↓
Service层(业务逻辑:校验参数、加锁、事务处理)
↓
Mapper层(数据持久化,SQL优化)
↓
利用Redis做缓存与分布式锁,MySQL做最终数据落盘
关键设计决策:
- 数据库表结构:
product(商品表)、stock(库存表,含version乐观锁字段)、stock_flow(流水表)。 - 避免大事务:将“预扣库存”和“生成订单”分离,降低锁持有时间。
核心业务模块实现:防超卖的精髓
库存扣减——乐观锁+DQL(条件更新)
这是最核心的代码片段,相比直接update,乐观锁能保证数据一致性:
// Service层方法
@Transactional
public boolean deductStock(Long productId, Integer quantity) {
// 1. 检查库存是否充足(可走Redis)
Stock stock = stockMapper.selectById(productId);
if (stock.getCount() < quantity) {
return false;
}
// 2. 乐观锁更新:version作为条件,更新成功说明未被其他线程修改
int updated = stockMapper.deductStockByVersion(productId, quantity, stock.getVersion());
return updated > 0; // 影响行数为0则扣减失败,重试
}
// Mapper XML
<update id="deductStockByVersion">
UPDATE stock
SET count = count - #{quantity}, version = version + 1
WHERE product_id = #{productId} AND count >= #{quantity} AND version = #{version}
</update>
缓存与锁的协同:解决“缓存穿透”
高并发下,如果库存为0,请求直接穿透Redis打到MySQL,属于恶意攻击,本系统采用缓存空值策略:若库存为0,在Redis写入一个KEY: "stock:null:{id}",过期时间设为3分钟,有效拦截无效流量。
高并发性能优化:从秒杀到平稳
- 读写分离:主库负责写事务,从库用于后台报表查询,减少行锁争用。
- Redis库存预热:系统启动时,将数据库库存加载到Redis,使用
Lua脚本保证扣减的原子性(DECRBY命令)。 - 异步流水记录:使用
ThreadPoolExecutor异步写入stock_flow表,避免影响主流程响应时间。
常见问题与问答(FAQ)
Q1:使用Redis DECRBY扣减库存后,如果订单取消,怎么加回库存?
A:需要区分“可用库存”和“冻结库存”,下单成功后,执行DECRBY(可用-1),同时向Redis的一个Set集合写入订单ID,取消订单时,只有该订单ID确实存在于集合中,才执行INCRBY加回,且需要以数据库流水为准做最终一致性校验。
Q2:如果Redis的缓存数据跟MySQL不一致,如何处理?
A:本系统采用“缓存为主,DB兜底”策略,每次扣减后,主动将结果同步到DB(异步),若Redis宕机,则走DB乐观锁方案降级,设置定时任务每5分钟比对缓存和DB的库存总量,差异超过阈值则报警。
Q3:乐观锁冲突频繁,导致大量重试,影响吞吐怎么办?
A:可以引入Redisson的RLock分布式锁,将“读库存+扣库存”作为临界区锁住,但注意锁粒度要细化(按productId),且设置合理过期时间(如5秒),避免死锁,对于极端高并发(如秒杀),可直接用Redis Lua脚本替代数据库乐观锁,扣减成功后再异步落库。
总结与扩展思考
本案例通过数据库乐观锁 + Redis缓存 + 异步解耦,成功解决了库存不准、性能瓶颈、日志追踪三大问题。核心经验是:不要为了技术而技术,要根据业务量级选择合适的一致性方案——低并发用简单update,高并发用Redis原子操作,最终以数据库为准做对账。
扩展方向:
- 若业务扩展至多仓库,需引入分库分表,库存维度变为
product_id + warehouse_id。 - 可采用
Seata分布式事务框架,处理“扣库存-生成订单-扣减优惠券”跨服务的事务一致性。
一个健壮的Java库存系统,本质是并发控制与数据最终一致性的博弈,希望本文的案例拆解,能帮你避开“超卖”“死锁”等深坑,如果你在实践中有更好的方案,欢迎在评论区留言讨论。