医疗系统高并发架构实战:从防超卖到AI工程化的面试深度解析
1. 高并发场景与防超卖实现思路
技术方案设计思路
- 原子性库存控制:通过 Lua 脚本将库存的查询、判断、扣减以及预扣记录写入打包成原子操作,避免多线程并发导致超卖。
- 读取 Redis 中的可售库存后判断是否 ≥ 购买数量,不够则返回失败,够的话,直接扣取库存并记录一条预扣信息,订单支付成功后,只需要把那条预扣记录删除即可。如果支付失败,需要把预扣的库存加回去。
- 分布式锁防并发:在 Redis + Lua 原子扣减的基础上,引入了 Redisson 的分布式锁作为外层保护。对同一个商品的库存操作,先用 Redisson 加锁串行化请求,再在锁内执行 Lua 脚本完成原子扣减。这样既保证了单次操作的原子性,又解决了多应用实例并发和 Redis 主从切换可能导致的超卖问题
- 延迟消息处理超时:通过RocketMQ的延迟消息功能处理未支付订单。订单创建后发送一个延迟消息,消费者监听这些消息并检查订单状态。如果订单超时未支付,则自动取消并回退库存。
2. 异步处理与性能优化
实现思路
- RocketMQ 事务消息保证最终一致性:
挂号支付和药房发药是典型的跨服务调用场景:
挂号支付:创建订单 → 扣减库存 → 调用支付 → 更新挂号状态 → 通知患者
药房发药:医生开方 → 扣减药品库存 → 药房配药 → 更新处方状态 → 通知取药
对于挂号、发药这类场景,业务上允许短暂的不一致,但必须保证最终一致,所以选用更轻量的事务消息:
- 发送Half 消息(半消息):确认 MQ 服务正常、Topic 存在
- 执行本地事务:把本地事务的执行结果(Commit 或 Rollback)回传给 MQ。
- 消息投递消息投递:Commit 状态的消息变为可见,消费者可以消费。
- 事务状态回查:如果生产者因为宕机、网络中断等原因,没有及时回传状态,MQ 会主动回查。
生产者需要实现一个回查接口,根据本地数据(比如订单表)判断事务是否成功。
消息可能重复投递(网络重试、回查后重新投递等),消费者必须做幂等处理:
- 业务唯一键:订单ID就是天然的唯一键。
- 处理前先查状态:查订单表,如果已经是已支付/已发药,直接返回成功,不再重复处理。
- 数据库唯一约束:给订单ID加唯一索引,重复写入自然失败。
Rollback 状态的消息被丢弃,消费者永远看不到。
3. CompletableFuture并行查询优化:
医生工作站的首页是一个信息聚合页面,需要展示:患者历次就诊记录、过敏史、检查报告、 检验结果、当前处方、医嘱信息等信息。传统串行调用每个接口,假设每个耗时 400ms,6 个接口就是 2.4 秒,加上页面渲染就超过 2.5 秒,体验很差。
通过 Java提供的异步编程工具CompletableFuture进行并行查询优化
关键方法:
supplyAsync():提交一个异步任务,有返回值。
allOf():等待所有任务完成。
join():获取结果。
thenAccept():任务完成后回调处理。
exceptionally():某个任务异常时的降级处理。
实现思路
- 把每个查询包装成独立任务
- 并行执行所有任务
每个任务提交到自定义线程池(不能默认用 ForkJoinPool,IO 密集任务需要更大的线程池) - 等待所有任务完成
用CompletableFuture.allOf()组合所有 Future。
设置总超时时间(比如 1 秒),防止某个服务卡死拖垮整个接口。 - 组装结果返回
每个 Future 调join()获取结果。
即使个别查询失败,也要有降级返回(比如返回空列表),保证首页至少能展示出来。 - 异常处理
每个异步任务单独做exceptionally,防止一个查询失败影响全局。
降级策略:记录日志、返回兜底数据或空列表。
- 性能优化成果:
串行耗时:6 个请求 × 400ms ≈ 2400ms,加上框架开销 ≈ 2.5 秒。
并行耗时:最慢的那个请求 400ms,加上线程调度、结果组装、网络开销 ≈ 800ms。
3. 分布式锁与任务幂等
实现思路
-
Redisson分布式锁应用场景:
- 过期号源回收:防止多节点重复回收
- 日终对账:确保每天只对账一次
- 数据同步:避免并发写入冲突
-
Redisson RLock 的核心优势:
- 看门狗机制自动续期:每隔 10 秒检查线程是否还持有锁,如果持有重置锁过期时间为 30 秒
- 可重入性:Redisson 的 RLock 是可重入的,同一个线程多次获取同一把锁不会阻塞,内部用计数器记录重入次数,释放时也要释放同样次数。
-
Redis Hash实现任务幂等:
锁只解决了“同时只能有一个执行”,但没解决“锁过期后再次获取时怎么知道已经执行过了”的问题。需要引入任务状态记录。
定时任务触发
↓
尝试获取分布式锁(lockKey = "lock:task:XXX")
↓
获取锁成功
↓
查询 Redis Hash 中的任务状态
├─ 状态为 SUCCESS → 任务已完成,释放锁,直接返回
├─ 状态为 RUNNING → 可能是上次执行异常中断,判断是否超时再决定
└─ 状态为空/FAILED → 设置为 RUNNING,开始执行
↓
执行业务逻辑
↓
执行成功 → 更新状态为 SUCCESS,记录结束时间
执行失败 → 更新状态为 FAILED,记录错误信息
↓
释放锁
可能问到的面试题
-
Redis 挂了怎么办?
存数据需持久化(RDB/AOF),同时可用数据库作为最终兜底,Redis 恢复后从 DB 初始化。 -
预扣和真实库存不一致怎么办?
需要对账机制,比如定时把 Redis 预扣数据和 DB 订单状态做比对,修正差异。 -
为什么用了 Lua 还要加锁?
Lua 保证了命令级原子,但在 Redis 集群模式下,主从异步复制可能导致数据回退。Redisson 的锁把并发请求先串行化,降低了复制延迟带来的风险,同时它的看门狗机制解决了锁的续期问题,比自旋锁更健壮。 -
红锁是干什么的?
红锁是在多个独立的 Redis Master 节点上同时加锁,过半数成功才算获取锁。它解决的是单点 Redis 宕机后锁状态丢失的问题。在我们的库存扣减里,如果对一致性要求极高,可以用红锁替代普通锁。 -
线程池怎么配的?
我们是 IO 密集场景,线程数设为 CPU 核心数的 2 倍。用有界队列防止内存溢出,拒绝策略选 CallerRunsPolicy,保证任务不丢。还做了熔断降级,某个服务超时不影响其他数据展示。 -
如果某个查询特别慢怎么办?
“给 allOf 设了超时时间,超时就降级返回已拿到的那部分数据,首页至少能先展示出来。同时慢查询的监控报警会被触发,后续优化那个具体接口。” -
Redisson看门狗机制的优缺点?
- 优点:自动续期,防止业务未完成锁过期
- 缺点:客户端崩溃可能导致锁无法释放(有最大续期次数限制)
- 改进:结合业务设置合理的超时时间
-
幂等性设计的常见方案?
- 数据库唯一索引
- 状态机(状态流转控制)
- Token机制(一次性令牌)
- 分布式锁+状态记录(本项目方案)
-
Redis Hash相比String存储状态的优势?
- 结构化存储,便于扩展字段
- 原子操作支持(HSETNX、HINCRBY等)
- 内存效率更高(ziplist编码优化)
-
为什么选择异步架构而不是同步调用?
- AI推理耗时不确定(可能几秒到几十秒)
- 避免阻塞主业务流程
- 提高系统吞吐量和可用性
- 支持削峰填谷
-
LLM调用超时如何处理?
- 设置合理的超时时间(根据历史数据统计)
- 分级降级策略:超时后返回简化结果或默认值
- 熔断机制:连续失败后暂时跳过AI服务
- 异步回调:超时后继续处理,结果后续更新
- 如何保证AI推理结果的准确性?
- 多模型投票机制(集成多个LLM)
- 人工审核队列(不确定结果转人工)
- 反馈学习循环(医生修正结果用于模型优化)
- 置信度评分(低置信度结果特殊标记)
- Elasticsearch在医疗场景的特殊考虑?
- 数据安全:字段级别权限控制
- 检索性能:针对病历文本优化分词器
- 数据隐私:敏感信息脱敏存储
- 合规要求:审计日志完整记录
- RocketMQ事务消息的工作流程?
- 发送半消息(Half Message)
- 执行本地事务
- 根据本地事务结果提交或回滚消息
- 消费端保证幂等消费
- CompletableFuture的线程池如何配置?
- 根据IO密集型/CPU密集型选择线程池类型
- 设置合理的核心线程数和队列容量
- 监控线程池状态,防止资源耗尽
- 并行查询的数据一致性如何保证?
- 设置统一的超时时间
- 部分失败时的降级策略
- 结果合并时的异常处理
- 还有哪些性能优化手段?
- 缓存热点数据(Redis二级缓存)
- 数据库查询优化(索引、分页)
- 前端懒加载和分片加载
- 你项目中遇到过什么难点
“我讲两个比较有代表性的难点。第一个是库存超时回滚的可靠性问题。早期我们靠 Redis key 过期来触发回滚,但发现过期通知会丢失,导致号源迟迟不释放。我分析后认为问题根源是让 Redis 同时承担了存储和定时触发两个职责,于是把触发逻辑迁移到 RocketMQ 延迟消息上,下单后发一条延迟消息精准触发回滚检查,Redis 只存状态不负责触发,再配合定时任务兜底,三层保障彻底解决了问题。
第二个是 AI 预问诊链路的稳定性问题。LLM 接口响应时间极不稳定,有时超过 30 秒,卡住了消息消费线程造成堆积。我做了独立推理线程池加超时熔断,配合 Redis 幂等记录保证重试不产生脏数据。另外还遇到 Java 和 Python 跨语言序列化字段丢失的问题,我们通过约定严格 JSON Schema 并做入口校验解决了。这两个问题让我深刻体会到,分布式系统设计必须为每个不可控依赖留好兜底。”
表述
第一条
在挂号抢号场景中,我们采用了一套三层防护来解决超卖和库存回收问题。第一层用 Redisson 分布式锁对同一号源做请求串行化,避免多实例并发竞争;第二层在锁内执行 Redis + Lua 脚本,将查询、判断、扣减、记录预扣四步原子化,杜绝超卖;第三层用 RocketMQ 延迟消息替代传统的 Redis key 过期通知,下单成功后发一条延迟 30 分钟的消息,到期精准触发回滚检查,未支付则加锁回退库存,同时预扣记录保留更长 TTL 做最后兜底。
第二条
在挂号支付和药房发药场景,用 RocketMQ 事务消息实现跨服务调用的最终一致性,通过 half 消息、本地事务执行、状态回查三步保证了消息和本地事务的原子绑定。在医生工作站首页加载优化上,用 CompletableFuture 把多个无依赖的数据源查询改成并行执行,配合自定义线程池和超时降级,把响应时间从 2.5 秒优化到 800 毫秒。这两个技术分别解决了分布式环境下的写一致性和读性能问题。
第三条
“我们的定时任务,比如日终对账和过期号源回收,部署在多节点上。如果不加控制,每个节点都会执行,导致重复处理。同时执行时间不确定,如果用简单的 Redis SET NX 锁,设了固定过期时间可能不够,设太久又怕死锁。”
“我们用了 Redisson 的 RLock 分布式锁,每个任务在启动时先获取一把锁,只有获取成功的节点才执行。Redisson 的看门狗机制会每隔 10 秒自动续期,只要任务还在运行锁就不会释放,完美解决了执行时间不确定的问题。”
“只靠锁还不够,因为锁过期后任务可能被再次触发。我们在获取锁后,先查 Redis Hash 中的任务执行状态。如果状态是 SUCCESS,说明已经执行过了,直接跳过。如果状态是空或 FAILED,就设置为 RUNNING 然后执行。锁内做状态检查,保证了判断和设置是原子的。”
“这样就形成了双重保障:分布式锁解决了同一时刻的并发问题,任务状态记录解决了跨时间的重复执行问题。两个机制配合,保证了所有核心调度任务的幂等性。”
第四条
智能预问诊的场景是患者在挂号或候诊时填写主诉,系统提前生成 AI 问诊摘要供医生参考。由于大模型推理耗时较长,如果同步调用会阻塞患者端体验,所以我设计了一条基于 RocketMQ 的异步推理链路:患者提交主诉后,服务端直接返回受理成功,同时将主诉封装成消息投递到 MQ,由 AI 推理服务异步消费。推理服务调用 LLM 时,我做了超时重试和幂等处理,保证消息重复消费不会产生脏数据,并通过跨语言联调解决了 Java 服务与 Python AI 服务之间的通信协议问题。推理结果不落库而是直接写入 Elasticsearch,医生工作站通过搜索实时拉取,形成“异步推理 + 搜推展示”的闭环。我还优化了 Prompt,让模型稳定返回标准化 JSON,前端无需二次清洗就能直接渲染。这个方案上线后,医生接诊时能看到预生成的病情摘要,单次问诊时长平均缩短约两分钟,患者也无需反复口述病情,整体就诊体验提升明显。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)