返回笔记列表
Note Detail

库存一致性怎么落地:UPSERT、Redis 锁与事务回滚

一致性不是简单加一把锁,而是让幂等入口、数据库原子写入、事务回滚和业务状态共同约束关键流程。

分类:Java 后端发布时间:2026年7月16日更新:2026年7月16日原创内容
PostgreSQLJSONBRedisTransaction

先说结论

门诊系统里的患者、处方、库存和结算不是四组独立 CRUD。只要收费成功、库存未扣,或者一个处方被重复结算,页面看起来正常也不代表业务成立。

一致性需要多层共同完成:

  1. 业务 ID 把患者、就诊单、处方和库存记录关联起来。
  2. 状态字段限制单据可以从哪一步走到哪一步。
  3. Redis 锁或幂等键减少同一业务请求被并发重复执行。
  4. 数据库原子写入和条件更新守住最终数据边界。
  5. Spring 事务让一组操作要么全部成功,要么全部回滚。

关系字段和 JSONB 怎么分工

患者 ID、就诊单 ID、药品编码、状态、金额等高频关联和强约束字段,适合保留在普通关系列中。它们需要清楚的类型、索引和外键语义。

过敏史、症状补充、既往史等结构可能变化、字段相对稀疏的病历内容,可以放入 PostgreSQL JSONB。这样能减少频繁改表,同时保留结构化查询能力。

JSONB 不是“所有字段都塞进去”的理由。稳定字段放进 JSONB 后,会让约束、关联和统计变得更困难。

GIN 索引适合加速 JSONB 包含、键和值检索,但它有写入和存储成本,也不是所有查询都会自动变快。是否加索引应该由真实查询条件决定,而不是看到 JSONB 就机械添加。

UPSERT 与库存扣减不是同一件事

UPSERT 适合处理“存在则更新,不存在则插入”的原子写入,例如某些可重复提交的业务记录。相比先查再插,它能减少并发窗口和重复记录风险。

库存扣减更关键的是条件更新,例如只有剩余数量足够时才执行扣减。数据库影响行数为 0 时,业务层就应该判断库存不足并终止流程。

因此,不能把“用了 UPSERT”简单等同于“库存绝不会出错”。不同写入场景需要不同的数据库原子语义。

Redis 锁为什么不是最终真相

对同一就诊单或结算单加 Redis 锁,可以减少用户连点、网络重试和多实例并发带来的重复处理。锁粒度应该落在具体业务单据,而不是把同一个患者的所有操作都串行化。

但 Redis 锁不能代替数据库约束:

  • 锁可能超时。
  • 服务可能在持锁期间宕机。
  • 解锁实现不当可能误删别人的锁。
  • 其他写入口可能绕开 Redis。

因此数据库条件更新、唯一约束和事务仍然是最终防线。Redis 更像入口处的削峰与防重层。

事务回滚解决什么

假设一张处方包含三种药:第一种扣减成功,第二种发现库存不足。如果每次扣减各自提交,系统会留下“部分发药”的中间状态。

把库存扣减、结算记录和状态更新放进清晰的事务边界后,第二种药失败时应抛出异常,让第一种药的扣减一起回滚。这样用户看到的是明确失败,而不是数据已经悄悄改变。

事务也有边界。跨外部服务、消息队列或第三方支付后,单库事务不再足够,需要幂等、补偿、事件状态或更完整的分布式一致性方案。当前项目并没有把自己包装成这一规模的生产医疗系统。

状态为什么不能只写“完成”

患者、就诊和库存真正串起来,需要状态能表达业务阶段。当前实现使用了简化状态;如果面向更严格的生产流程,更合理的表达应至少区分:

待缴费 -> 已缴费待发药 -> 已发药 -> 已归档

状态拆清楚后,系统才能判断哪些操作允许执行、失败应该回到哪里、重复请求是忽略还是报错。

项目边界

该项目基于 RuoYi 生态进行业务化二次开发,重点展示门诊流程、PostgreSQL 建模和一致性措施,不把通用权限框架包装为个人原创,也不宣称已经达到成熟医疗 SaaS 的生产合规水平。

查看对应项目页