SpringVortexNotes

商品购买要加锁也要条件更新

zjc 于 2026-08-24 发布

代码环境

商品购买是这套项目里第一个真正需要跨实例互斥的业务。Provider 多实例部署后,同一个商品的扣库存请求可能被 LoadBalancer 分到不同 JVM,本地锁失效,库存就可能超卖。

这次实现的链路是:

Consumer -> GoodsFeignApi -> Provider
        -> 按商品 ID 获取 Redisson 锁
        -> 事务内校验商品
        -> 数据库条件更新扣库存
        -> 保存订单主表和明细
        -> 提交后清理商品详情缓存

https://github.com/springvortex/spring-cloud-alibaba/blob/release/v1.0.0/service-provider/src/main/java/com/zjc/provider/service/impl/GoodsPurchaseServiceImpl.java

锁按商品维度拆分:

redissonClient.getLock("zjc:provider:goods:purchase:lock:" + goodsId);

https://github.com/springvortex/spring-cloud-alibaba/blob/release/v1.0.0/service-provider/src/main/java/com/zjc/provider/service/impl/GoodsPurchaseServiceImpl.java

不同商品不互相等待,同一商品在集群内串行执行。这个版本的锁等待为 10 秒,超时返回 503 和“当前购买人数过多”;后续压测提交又把等待调整到 20 秒。

只靠锁还不够。数据库层使用条件更新:

UPDATE t_goods
   SET stock = stock - #{quantity}
 WHERE goods_id = #{goodsId}
   AND status = 1
   AND is_deleted = 0
   AND stock >= #{quantity}

https://github.com/springvortex/spring-cloud-alibaba/blob/release/v1.0.0/service-provider/src/main/java/com/zjc/provider/mapper/GoodsMapper.java

影响行数不是 1 就说明商品不可购买或库存不足,事务直接失败回滚。这样即使 Redis 锁异常失效、看门狗续期异常或锁被误释放,数据库条件也能保证库存不会被扣成负数。

订单主表和明细使用已有的 saveWithDetails 同事务保存,明细保留商品名称、单价和数量快照。事务提交后再清理 provider:goods:id 详情缓存;缓存清理失败不影响购买结果,但会记录 ERROR,提示旧库存可能保留到 TTL 到期。

购买请求本身也有边界:用户 ID 必须有效,购买数量限制在 1 到 100。Consumer 侧只是 Feign 入口,不本地兜底下单,Provider 不可用时统一返回业务繁忙。

经验总结

分布式锁负责减少并发冲突,数据库条件更新负责守住最后一致性边界。两层能力解决的是不同问题,购买链路里都需要存在。

评论

评论由 GitHub Discussions 承载,需要 GitHub 账号登录。