ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Cloud、Redis、Kafka 医疗高并发架构实战

Spring Cloud、Redis、Kafka 医疗高并发架构实战 聊Java大厂面试Spring Cloud、Redis、Kafka这三个词绑在一起出现的频率比很多人想象中要高得多。最近帮几个准备跳槽的朋友做面试复盘发现“互联网医疗系统”这个业务场景正在成为面试官的新宠原因很简单它既有极端高并发的瞬间流量挂号秒杀、问诊高峰又有极高的数据一致性和安全要求还带着复杂的业务链路问诊、处方、订单、支付、电子病历一个系统能把研发、架构、稳定性、数据这块的能力全考一遍。这篇文章就把这个高频组合题完整拆开从面试官为什么会问到架构怎么设计再到Redis和Kafka如何协同扛住流量峰值最后把我在生产环境踩过的坑一并整理出来。无论你是准备面试的Java工程师还是正在做系统设计选型这份实战解析都能直接拿来用。1. 先拆题面试官为什么偏偏选中这三个组件1.1 互联网医疗系统的业务特性决定了技术选型互联网医疗系统跟普通电商系统最大的区别在于它的业务场景自带两个极端一个是“瞬时流量极高”一个是“数据准确性要求极其变态”。拿在线挂号来说三甲医院的专家号源可能只有二三十个放号瞬间同时涌进来的请求可能轻松破几千甚至上万这跟电商“库存多卖几个无所谓”的逻辑完全不同——号源超卖一个患者到了医院才知道没号那就是重大事故。另一个典型场景是图文问诊和电子处方医生在App端开完处方药房需要实时收到消息去准备药品整个链路跨了用户端、医生端、药房端、支付中心、病历中心好几个子系统任何一个环节掉了患者体验都会崩。这时候你就能看出技术选型的逻辑了系统必须拆成微服务否则几个业务团队没法并行迭代拆完之后服务之间的调用、治理、容错必须有完整方案这是Spring Cloud出场的理由流量峰值必须扛住热点数据必须缓存这是Redis的活系统内部大量异步通知、削峰、解耦、事件流转这是Kafka的核心价值。面试官选这三个组件本质上是想通过一个真实场景看你有没有完整的分布式系统设计能力。电商的通用方案套不到医疗场景这是很多人面试时答偏的地方。比如有人张口就说“MySQL加索引硬扛”或者“Redis做缓存就行”这种回答在普通CRUD项目里可能过得去但放在医疗系统里面试官会继续追问号源扣减的原子性怎么做医生排班变更了缓存怎么通知问诊订单状态流转丢了消息怎么办连环追问之下如果没真正做过很容易露馅。1.2 三个组件在系统中的分工边界要清楚面试时最忌讳把三个组件混在一起说。我给一个清晰的边界划分你可以直接记Spring Cloud负责“把系统拆开并治理好”Redis负责“把热点流量扛住并守住并发底线”Kafka负责“把异步链路和流量削峰串起来”。三者是协作关系不是替代关系。具体来说Spring Cloud在整个系统里承担服务注册发现Nacos、配置管理、网关路由Spring Cloud Gateway、熔断降级Sentinel、服务间调用OpenFeign这几件事。Redis承担的是业务缓存医生信息、科室排班、分布式锁号源扣减、接口幂等防重复提交、分布式限流滑动窗口、验证码等短生命周期数据。Kafka承担的是消息解耦挂号成功通知、问诊会话创建、处方审核结果推送、流量削峰高并发写请求先入队消费端按能力处理、事件回溯操作审计日志、业务事件流。组件核心职责在医疗系统中的典型场景最常见面试考点Spring Cloud服务治理与系统拆分挂号服务、问诊服务、药房服务、支付服务的注册发现与容错服务拆分粒度、Gateway路由、Sentinel降级规则Redis热点缓存与并发控制号源余量缓存、排班缓存、分布式锁、接口限流缓存穿透/击穿/雪崩、分布式锁、持久化策略Kafka异步解耦与削峰填谷挂号成功通知、处方流转、问诊状态事件、日志采集消息可靠性、顺序性、重复消费、堆积处理这个表格如果能在面试开头用两三句话说清楚基本就建立了“这个人做过真实架构设计”的第一印象。切记不要一上来就背组件特性而是先说“在这个场景下这个组件解决什么问题”这才是加分项。2. 整体架构设计与微服务拆分思路2.1 一套可以直接复用的分层架构我见过不少候选人描述架构时“只有一张图没有细节”面试官追问就卡壳。这里给一套你可以在纸上画出来的层次结构每一层的职责和关键组件都明确接入层是Spring Cloud Gateway负责统一入口、鉴权、灰度发布和Sentinel限流应用层是一组按业务域拆分的微服务包括用户服务、挂号服务、问诊服务、处方服务、药房服务、支付服务、消息服务支撑层是Nacos注册中心加配置中心Kafka集群和Redis集群数据层是MySQL主从加ES检索以及对象存储电子病历附件、检查报告图片。几个容易忽略的细节所有服务必须接入同一个Nacos但配置要按namespace隔离比如dev、test、prod各一套防止配置串环境Gateway上要挂全局过滤器做Token解析不要在业务服务里重复做Redis和Kafka都是独立集群不要部署在应用服务器同一台机器上否则大促时互相抢CPU。这套架构本身不复杂但面试官真正关心的是你能不能解释“为什么这样拆”以及流量如何穿透这几层。建议顺着“用户请求从App进来经过Gateway鉴权落到挂号服务先读Redis缓存号源再通过Kafka异步创建订单最后推送通知给用户”这条主线讲一遍比背十页架构图都管用。2.2 服务拆分怎么分才不显得外行微服务拆分是最容易暴露水平的地方。很多人一说拆分就是“按模块拆”结果拆出来的服务互相调用成蜘蛛网。我的经验是医疗系统优先按业务域拆分同时考虑“变更频率”和“数据边界”。用户服务管注册登录和基本信息挂号服务管号源、排班、预约订单问诊服务管会话、图文消息、音视频通话处方服务管处方开立、审核、作废药房服务管库存和发药支付服务管费用结算和退款。数据上是完全隔离的每个服务只能访问自己的库跨服务数据通过接口或消息传递。判断拆分粒度是否合理可以问自己三个问题这个服务能不能独立部署不能则说明耦合严重这个服务的数据库表有没有被其他服务直接访问有则说明边界没划清如果这个服务挂了影响的用户范围是否可控影响面过大说明拆得不够细在面试中快速说出这套判断标准比堆一堆概念有用得多。另外要主动提到“拆分不是越多越好”。我经历过把一个挂号服务拆成预约单服务和号源服务结果一个事务要跨两个服务调三次接口性能和一致性反而更差。正确的做法是在核心链路上允许适度粗粒度比如挂号服务保留“排班号源预约单”的聚合能力因为这三个数据强相关、变更频率同步。2.3 网关与注册中心的高可用配置网关和注册中心是流量的命门面试官通常会在这里挖细节。注册中心我推荐Nacos它的CP模式临时实例采用临时节点心跳续约比Eureka更适合医疗这种要求实时发现变更的场景服务下线最多几秒内就能被摘除而Eureka的自我保护机制在极端情况下会导致调用打到已下线节点。Nacos集群部署至少三节点用内网域名互相通信建议部署模式选“AP模式下的临时实例”这是Nacos的默认临时实例行为支持动态注册故障实例会自动摘除控制台和管理端走独立端口。服务端配置心跳时间建议保持默认但把“保存实例状态的时间间隔”调小一些可以加快故障感知。Gateway侧重点关注超时和重试参数。路由级超时建议设为2000ms重试机制开启但限制重试次数最多2次并且只对GET请求自动重试写请求重试要非常谨慎否则可能造成重复挂号。还有一点很关键Gateway的默认线程池大小要根据压测数据调整不要让网关成为第一个被压垮的节点。我见过一次线上事故网关线程池默认200高峰时期全部线程阻塞在调用挂号服务上后续请求全部超时最后是通过增加服务实例和调大线程池上限才缓解。3. 核心场景实战挂号秒杀场景下的Redis与Kafka协同方案3.1 挂号场景为什么难做挂号秒杀是医疗系统里最能体现架构功力的场景几乎每个面试官都会围绕它出题。难点有三个第一热key集中一个专家的号源在放号瞬间被大量请求同时访问Redis单key的访问会瞬间飙到每秒上万次第二一致性要求高号源不能被超卖也不能“扣了号但订单没生成”第三下游依赖脆弱创建订单成功后要通知多条业务线如果所有操作都在一次请求里同步完成任何一个下游环节缓慢都会导致整体超时。这三个难点对应三个核心技术点也是面试官期待听到的答案Redis缓存加分布式锁解决热key和超卖问题Lua脚本保证扣减原子性Kafka异步化解决下游通知问题。三者是串在一起的缺任何一个环节都不完整。我记得有一次模拟压测纯同步实现下每个请求的耗时要800ms左右因为订单创建、库存扣减、通知医生、记录日志全部串行。改成Redis预扣号源加Kafka异步落单后核心路径耗时降到50ms以内吞吐量提升了近十倍。面试时如果能掏出这种实测数据说服力完全不一样。3.2 基于Redis缓存与分布式锁的第一道防线号源数据的特点决定了它必须提前预热到Redis里不能每次请求都查MySQL。放号前通过定时任务把当日排班和号源余量写入Rediskey可以设计成clinic:schedule:slot:{doctorId}:{date}value用Hash存剩余号数和总号数。扣减号源不能用“先读后写”的朴素逻辑并发环境下两个线程同时读到余量1各自走完业务逻辑再回写就会超卖。正确做法是用Lua脚本在Redis内原子完成校验和扣减local key KEYS[1] local current tonumber(redis.call(hget, key, remain)) if current nil then return -1 end if current 0 then return 0 end redis.call(hincrby, key, remain, -1) redis.call(hincrby, key, sold, 1) return 1这段脚本的逻辑很直观先读余量没有则返回-1表示key不存在余量已空返回0表示无号否则原子扣减返回1表示成功。Redis单线程特性保证了这个脚本在并发下不会产生竞态条件这就是分布式锁在这里的“内功”。外部再套一层分布式锁的时候要注意锁粒度。对“某个医生某天的号源”加锁key就是lock:schedule:{doctorId}:{date}而不是对整个服务加锁。锁的实现用Redisson的公平锁或普通锁都可以但要设置合理 leaseTime 和等待时间我一般设leaseTime 30秒、waitTime 2秒超过等待时间直接返回“系统繁忙”而不是无限阻塞。锁内只做Redis扣减不做数据库操作这样锁的占用时间极短不会拖垮吞吐。3.3 Kafka异步化从同步扣号到异步落单扣减成功之后绝对不能在请求线程里直接创建数据库订单否则数据库连接池会成为短板。我的方案是扣号成功后马上把一条“挂号预约消息”发给Kafka的appointment-order-topic然后直接返给用户“预约中请稍后查看结果”同时把订单状态置为“处理中”写入Redis。消费者拿到消息后执行真正的业务插入预约订单表、扣减MySQL中的号源库存、更新Redis中的订单状态、通过消息给用户推送挂号结果。这里有个关键设计消息中必须携带唯一业务键比如userId doctorId date timeSlot生成的订单号消费端通过唯一索引保证同一笔预约不会重复创建。有人会问Kafka异步化之后如果消费者挂了怎么办答案是消息不会丢它滞留在Kafka里消费者恢复后继续消费。这种“最终一致”的思路在医疗场景里是实践过的核心结论是“不要求MySQL和Redis每时每刻严格一致但事件驱动最终一定一致”。面试官如果追问“消费者什么时候可能丢消息”你就可以展开讲提交时机和重试机制这部分后面专门细说。3.4 订单状态流转的消息设计挂号不只是“创建一个订单”这么简单它后续还有支付、改签、取消、就诊提醒条状态变化这些变化驱动着大量下游动作。我的做法是把每一条状态变化都作为事件发到Kafka对应topic各下游服务只订阅自己关注的事件。比如订单创建事件发到appointment-created支付服务订阅后触发支付请求支付成功发到appointment-paid问诊服务订阅后创建问诊会话医生添加问诊结论后发到consultation-completed处方服务订阅后生成处方待审核事件。这套事件驱动模式的好处是新业务接入时不用改老服务的代码只需要订阅新topic比如未来加上“用药提醒”能力订阅支付成功事件即可。topic分区设计上按用户维度做分区key保证同一个用户的事件进入同一分区消费时就能做到局部有序。比如一个用户不可能同时支付和取消但如果事件乱序就可能出现“取消在前、支付在后”的脏逻辑。分区数建议设为消费者数的整数倍方便水平扩展我常用的配置是3个副本、12个分区。4. 高频面试题分布式锁、缓存一致性、消息可靠性4.1 Redis分布式锁的三问三答面试官关于分布式锁至少有四个必问点为什么不能用数据库锁setnx和redisson有什么区别锁过期了业务没执行完怎么办锁粒度怎么控制第一问的答案是性能和数据量数据库行锁在单机事务里很好用但在并发量破万时锁等待、死锁检测会把数据库拖垮而且跨服务跨库后数据库锁直接失效。第二问setnx只是最底层原语直接用会踩两个坑忘记设置过期时间导致死锁或者业务执行超过过期时间导致锁提前释放。Redisson的核心价值是“看门狗”机制自动续期默认30秒过期每10秒检查一次业务没执行完就续期到下一个30秒。第三问如果锁到期但业务还没执行完Redisson已经处理了如果是自研锁要么把过期时间设得足够大要么在finally里释放并检查value。第四问才是拉开差距的地方。锁粒度不能太粗对整个医生加锁会导致同一个医生所有患者互斥也不能太细比如对“某个时段”加锁如果时段划分过密会导致大量锁冲突。我一般的经验是按“医生日期时段”维度加锁既不牺牲并发度又保证号源扣减安全。这组问答能答顺基本就能证明你不是背概念。4.2 缓存与数据库一致性双删和延时双删的原理与取舍缓存一致性问题在医疗系统里远比电商敏感比如医生停诊后排班缓存没删患者会挂上已停诊的号支付成功但订单缓存还是待支付用户端展示就会错乱。面试官爱问“更新数据库之后怎么更新缓存”最常见的坑是“先更新数据库再更新缓存”这会导致两个操作之间有一个窗口期并发读请求读到旧值。我推荐的思路是Cache Aside Pattern加延迟双删。具体流程更新数据库→删除Redis缓存→等待几百毫秒→再次删除缓存。为什么用删而不是更新因为删除操作天然幂等没有旧值覆盖新值的问题。为什么延迟双删因为先删除缓存后另一个线程可能刚好读数据库并写回旧缓存延迟再删一次能把这个顽固脏数据清掉。延迟时间不能拍脑袋定建议设置为“读请求写缓存的最慢耗时”加上几百毫秒缓冲。我一般在压测环境统计读接口P99耗时如果P99是200ms延迟删就设500ms。这套方案能覆盖99%的场景如果碰到极端并发导致仍不一致就要靠消息驱动的异步补偿了比如监听Binlog变更后刷新缓存这可以作为加分项提出来。4.3 Kafka的消息可靠性三端配置缺一不可Kafka消息可靠性的面试问答核心要答清楚“消息从生产到消费哪个环节会丢怎么防”。三个环节都是风险点生产者发送失败、Broker副本未同步、消费者拉取后没提交就宕机。生产者端配置acksall意思是必须等到所有ISR副本都写入成功才返回成功同时设置retries3和enable.idempotencetrue后者通过producer的PID和sequence number避免重试导致的重复消息。Broker端设置min.insync.replicas2要求至少两个副本同步配合副本因子3使用这样即使一个Broker宕机也不会丢数据。消费者端是重灾区很多人为了省事把enable.auto.commit设为true系统每5秒自动提交offset如果消息处理到一半宕机恢复后offset已经提交了这批消息就丢了。生产环境必须手动提交try { // 处理业务逻辑比如创建订单 process(message); // 业务成功后提交offset ack.acknowledge(); } catch (Exception e) { // 记录错误并重试重试失败后进入死信Topic dlp.sendToDlq(message); ack.acknowledge(); // 死信处理完成后也提交避免无限重试 }注意这里的原则是“业务成功才能提交offset”并且异常消息进死信队列而不是一直重试阻塞。死信Topic加一张消息追踪表人工或定时任务定期捞出来补偿这才是可靠消息的完整闭环。4.4 消息重复消费与顺序性幂等设计是关键即使做到上述配置重复消费依然会发生因为Kafka至少一次的投递语义下消费者处理完但提交offset前崩溃重启后会再消费一次。面试官问到这里期盼的答案是“幂等设计”而不是“保证消息不重复”。医疗系统里最典型的幂等场景是“同一笔挂号请求被消费两次只能产生一个订单”。我的做法是消费开始时先查Redis里是否存在order:create:{orderNo}的key存在说明已处理则直接返回不存在则用SET key value NX EX 300尝试占位只有占位成功才能走业务逻辑。这一步的本质是“分布式锁的另一种用法”面试时点破这一点会加很多分。顺序性问题在挂号场景不太突出但处方流转场景很关键一张处方要先创建再审核如果审核消息先到业务就会报错。解决方案是topic分区key用处方号同一张处方的所有消息进入同一分区分区内消息顺序有保证。消费者如果是多线程处理需要在内存中按key排队如果单线程消费天然有序。我在项目里用的是分区key加单分区消费多实例的模式也就是每条消息都带处方号消费者数等于分区数保证单一消费者线程处理同一处方的事件。5. 生产环境的坑Redis超时、Kafka积压与性能调优实录5.1 Redis command timed out排查实录热词里有“Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException”这个异常我在生产环境遇到不下三次几乎每次都是同一个根因大Key阻塞了Redis事件循环。第一次排查时监控显示Redis平均延迟只有2ms但总有少量请求超时超过3秒。后来用redis-cli --bigkeys扫描发现有一个Hash类型的key存了近百万个字段——是排班数据每个医生每天一个字段长期累积下来没人清理。Redis是单线程处理命令执行这个超大Hash的遍历操作时其他所有请求都得排队超时就是这样来的。解决方法是拆分大Key按日期维度拆成多个key每次只读写当天的数据同时给每个key设置合理的过期时间并加定时任务清理历史数据。另一个常见的超时原因是Redis连接池配置不合理。Lettuce是异步连接默认池大小只有8高并发下连接不够用会排队等待表现为偶发超时。建议生产配置最小空闲连接数16最大连接数64连接超时2秒读取超时3秒。注意最大连接数不是越大越好连接数过大会增加服务端线程开销最好压测找出拐点。5.2 Kafka消息延迟高与堆积的处理思路“Kafka消息延迟高”是另一个高频痛点面试官问到这个通常是想看你的排查路径。我先说我踩过的案例某次问诊高峰期挂号成功消息延迟从秒级飙升到分钟级用户反馈“号挂上了但一直收不到确认”。排查分四步。第一步看Kafka监控消费组Lag是否持续增长确认是消费慢还是生产慢第二步看消费者线程数与分区数是否匹配当时我们只有3个消费者但分区有12个9个分区处于“有人订阅但没线程消费”的假死状态把消费者线程调到12后吞吐立刻翻倍第三步看消费耗时发现消费端里面有个MySQL慢查询每条消息处理耗时200ms以上把这个SQL的联合索引加上降到20ms第四步看网络和GC太频繁的Full GC也会导致消费者停顿。如果堆积已经发生最快的补救手段是临时增加消费能力两个方向增加消费者实例数前提是分区数足够分摊或者开消费者端批量处理配置比如max.poll.records从默认500调到2000。还有一个“快速排出”的临时方案是把堆积消息先转到单独的“重放Topic”用专门的临时服务以更高吞吐处理和修复等堆积清完再停掉。5.3 从压测数据看参数调优压测是检验架构的唯一标准没有压测数据支撑的方案在面试里会显得单薄。我做挂号场景压测时用过一套比较标准的参数组合分享出来可以参考Redis方面连接池最大连接数64Lua脚本扣号不耗时单Redis实例可以支撑每秒两万次扣减操作但前提是没有大Key和慢命令。Kafka方面生产者batch.size16384、linger.ms5小消息聚合发送能显著提升吞吐消费者fetch.min.bytes1024、fetch.max.wait.ms500控制拉取频率。Spring Cloud Gateway方面线程池最大线程数从默认的200调到400但配合Sentinel的QPS阈值按压测结果×0.8防止把下游打死。压测过程中最值得注意的是“雪崩效应”的验证。我们压到1.5倍容量后手动关停一台挂号服务观察Sentinel是否会触发降级、Kafka是否堆积暴增、Gateway是否把请求正确分发到存活节点。这一轮演练跑下来你才能说自己对系统稳定性有底。面试时聊这段历程比空谈“高可用”有力得多。6. 面试答题心法与几个加分扩展点6.1 答题结构场景→方案→细节→风险面试官不是要听你说答案是要听你说思路。我的建议是每一次回答都固定用这套结构先交代业务场景再给出整体方案然后用两三句话讲实现细节最后主动暴露和补上风险点。比如问“Redis如何支撑高并发”别上来就说“缓存用户信息”而是拆成挂号放号瞬间用户流量集中在几十个热key上→先用Redis缓存号源并把扣减逻辑收敛到Lua脚本→同时用Sentinel做单key限流保护Redis→同时预热阶段提前加载并设置过期时间避免穿透。答完主干主动说“这套方案的一个风险点是缓存和数据库的一致窗口我是用延迟双删加异步补偿兜底的”。这样回答的层次感远超“Redis快”三字经。这个结构也适用于你向面试官提问的环节。比如反问“你们这边的峰值QPS大概是多少分布式锁的粒度是按什么维度划分的”既显得有实战经验又能判断团队水平。我见过很多候选人专业技能不差但回答没有层次被面试官一路追问到最后乱了阵脚其实只要把“场景→方案→细节→风险”四个节点把控好大部分追问都能提前覆盖到。6.2 几个不容易想到的加分扩展点如果你前面都答稳了想再拉开一点差距可以主动抛出以下扩展点。一是“缓存与数据库最终一致的补偿机制”不只是双删还可以讲监听Binlog变更、用增量消息刷新缓存二是“分布式事务在问诊支付场景的取舍”不用讲Seata全套重点说为什么挂号这个场景适合用事务消息本地事务表实现最终一致而不是强一致方案三是“灰度发布在医疗系统的特殊性”比如新版本挂号服务先灰度10%流量给内部测试账号并做AB对比异常率医疗系统容不得全量上线事故。对于Kafka可以加一条“Schema演进”的见解消息体用统一的事件模型字段变更兼容旧消费者消费者侧做字段默认值兜底避免上游改字段立刻炸掉下游服务。对于Spring Cloud可以讲Sentinel规则的动态配置如何基于Nacos持久化以及熔断降级的Metrics如何接入监控大屏这些细节都能体现你真的维护过生产系统。我个人在实际项目里的体会是Java大厂面试问这套组合最后拼的往往不是你会多少个组件API而是面对一个陌生业务场景时能不能快速定位流量瓶颈、选出关键组件、并讲出每一步设计的约束和代价。互联网医疗系统恰好把这三种组件的使用场景压缩到了一个系统里如果你能把挂号这一个核心链路从头到尾讲透应付大多数架构类问题都会轻松很多。这个题背后真正的价值是逼你把“技术栈”转化成“解决问题的能力”想明白这一点面试就已经赢了一半。
返回列表