Java 大厂面试:Spring Cloud+Redis+Kafka 互联网医疗系统架构实战解析

前言

在互联网医疗行业高速发展的今天,Java 工程师面临着更高的技术要求。本文通过一场真实的面试场景,带你深入了解互联网医疗系统的技术架构设计。


面试场景

面试官:(严肃脸)你好,请先做个自我介绍。

谢飞机:(紧张)面试官好,我叫谢飞机,毕业于...那个...家里蹲大学计算机系,有 3 年 Java 开发经验...

面试官:(点头)好,那我们直接进入技术环节。今天我们要聊的是互联网医疗场景下的系统架构设计。


第一轮:基础架构与服务治理

面试官:第一个问题,互联网医疗系统中,患者挂号、问诊、处方流转涉及多个服务,你们用什么做服务注册发现?

谢飞机:(自信)这个简单!我们用 Nacos 做服务注册中心,比 Eureka 新,支持配置中心,一举两得!

面试官:(微笑)不错,那服务之间怎么调用?

谢飞机:OpenFeign 啊,声明式调用,加个@FeignClient 注解就完事了!

面试官:(点头)好,那如果问诊服务调用处方服务超时了怎么办?

谢飞机:(挠头)呃...加个超时时间配置?实在不行...重试几次?

面试官:(严肃)只配置超时和重试够吗?如果处方服务挂了,问诊服务会不会被拖垮?

谢飞机:(冒汗)这个...那个...应该有个熔断机制吧?具体怎么配我回去查查文档...

面试官:(记录)好,下一个问题。医疗系统对数据一致性要求很高,你们数据库用的什么连接池?

谢飞机:(放松)HikariCP!性能最好,Spring Boot 默认就是这个!

面试官:(满意)不错,那数据库版本管理怎么做?

谢飞机:Flyway!每次发布自动执行 SQL 脚本,不会漏掉表结构变更!


第二轮:缓存与消息队列

面试官:(翻简历)好,基础不错。现在说缓存。患者查询医生排班信息,QPS 很高,你们怎么优化?

谢飞机:(来劲了)Redis 缓存啊!医生排班信息不常变,缓存起来,查询速度嗖嗖的!

面试官:缓存过期时间设多少?

谢飞机:(思考)排班一般一周不变,设 7 天?不对,医生可能临时调班...那设 1 小时?

面试官:(点头)可以,那缓存穿透、击穿、雪崩怎么预防?

谢飞机:(流利)穿透用布隆过滤器,击穿用互斥锁,雪崩给过期时间加随机值!

面试官:(满意)不错,看来缓存这块有研究。那患者问诊完成后,需要发送短信通知、生成电子病历、推送给保险公司,这些怎么解耦?

谢飞机:(自信)消息队列!Kafka 吞吐量高,适合这种场景!

面试官:为什么不用 RabbitMQ?

谢飞机:(犹豫)Kafka...Kafka 更...更流行?而且我们数据量大...

面试官:(追问)那消息可靠性怎么保证?患者没收到短信投诉怎么办?

谢飞机:(含糊)开启 ack 机制...重试...死信队列...具体配置我得问问运维...

面试官:(记录)好,那电子病历需要长期存储,你们用的什么?

谢飞机:MySQL 主表,Elasticsearch 做搜索,OSS 存 PDF 文件!


第三轮:安全、监控与分布式事务

面试官:(严肃)医疗系统涉及患者隐私,安全怎么做?

谢飞机:(认真)Spring Security+JWT!接口都加权限控制,敏感数据加密存储!

面试官:加密用什么算法?

谢飞机:AES...还是 RSA?反正 Spring Security 都支持,配置一下就行...

面试官:(皱眉)患者处方药涉及医保结算,如果扣款成功但处方状态没更新怎么办?

谢飞机:(紧张)分布式事务...Seata!AT 模式,无侵入...

面试官:AT 模式适合高并发场景吗?

谢飞机:(冒汗)这个...高并发可能用 TCC 更好?但 TCC 要写三个方法...我们项目用 AT 凑合着...

面试官:(继续)系统上线后怎么监控?

谢飞机:Prometheus+Grafana!CPU、内存、JVM 指标都监控,告警发钉钉!

面试官:链路追踪呢?一个请求经过多个服务,出问题怎么定位?

谢飞机:(模糊)SkyWalking...还是 Zipkin?我们好像装了一个,具体名字我忘了...

面试官:(合上简历)最后一个问题,JVM 调优做过吗?

谢飞机:(松口气)做过!-Xms -Xmx 设成一样,避免动态扩容,GC 日志打开分析...

面试官:Full GC 频繁怎么排查?

谢飞机:(卡壳)这个...用 jmap dump 内存...然后 MAT 分析...但我没实际遇到过...


面试结束

面试官:(站起来)好了,今天先到这里。技术面表现...(停顿)还行,有些问题需要再深入。HR 会联系你,回家等通知吧。

谢飞机:(起身)谢谢面试官!那个...大概多久有通知啊?

面试官:(微笑)一周内吧。对了,回去把 Resilience4j 熔断配置和 Seata TCC 模式好好看看。

谢飞机:(内心 OS)完了,刚才熔断和事务那块露馅了...


详细答案解析

第一轮答案:基础架构与服务治理

1. 服务注册发现 - Nacos

业务场景:互联网医疗系统包含患者服务、医生服务、问诊服务、处方服务、支付服务等 10+ 微服务,需要统一的服务注册与发现机制。

技术方案

# application.yml
spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        namespace: medical-prod
        group: MEDICAL_GROUP

核心优势

  • 支持 AP/CP 模式切换
  • 集成配置中心,减少组件
  • 支持权重路由,实现灰度发布
  • 健康检查更及时
2. 服务调用 - OpenFeign

业务场景:问诊服务需要调用患者服务获取病史、调用处方服务开具处方。

技术方案

@FeignClient(name = "prescription-service", fallback = PrescriptionFallback.class)
public interface PrescriptionClient {
    @PostMapping("/api/prescription/create")
    Result<PrescriptionVO> createPrescription(@RequestBody PrescriptionDTO dto);
}

// 熔断降级实现
@Component
public class PrescriptionFallback implements PrescriptionClient {
    @Override
    public Result<PrescriptionVO> createPrescription(PrescriptionDTO dto) {
        return Result.fail("处方服务繁忙,请稍后重试");
    }
}
3. 容错保护 - Resilience4j

业务场景:处方服务响应慢或宕机时,防止问诊服务被拖垮。

技术方案

resilience4j:
  circuitbreaker:
    instances:
      prescriptionService:
        slidingWindowSize: 10
        failureRateThreshold: 50
        waitDurationInOpenState: 30000
        permittedNumberOfCallsInHalfOpenState: 5
  timelimiter:
    instances:
      prescriptionService:
        timeoutDuration: 3s
  retry:
    instances:
      prescriptionService:
        maxAttempts: 3
        waitDuration: 1s

熔断状态说明

  • CLOSED:正常状态,请求正常处理
  • OPEN:熔断状态,直接返回降级结果
  • HALF_OPEN:半开状态,允许少量请求探测服务是否恢复
4. 数据库连接池 - HikariCP

业务场景:医疗系统读写频繁,需要高性能连接池。

技术方案

spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 10
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
5. 数据库版本管理 - Flyway

业务场景:多环境部署,确保数据库表结构一致。

技术方案

src/main/resources/db/migration/
├── V1.0.0__create_patient_table.sql
├── V1.0.1__create_doctor_table.sql
├── V1.0.2__create_prescription_table.sql
└── V1.0.3__add_insurance_column.sql

第二轮答案:缓存与消息队列

1. Redis 缓存架构

业务场景:医生排班信息查询 QPS 达 5000+,数据库无法承受。

技术方案

@Service
public class ScheduleService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    // 三级缓存:Caffeine + Redis + 数据库
    @Cacheable(value = "schedule", key = "#doctorId + '_' + #date", 
               unless = "#result == null")
    public ScheduleVO getSchedule(Long doctorId, LocalDate date) {
        // 缓存穿透保护
        String cacheKey = "schedule:" + doctorId + "_" + date;
        ScheduleVO schedule = (ScheduleVO) redisTemplate.opsForValue().get(cacheKey);
        
        if (schedule == null) {
            // 布隆过滤器检查
            if (!bloomFilter.mightContain(cacheKey)) {
                return null; // 肯定不存在
            }
            
            // 互斥锁防止缓存击穿
            RLock lock = redissonClient.getLock("lock:" + cacheKey);
            if (lock.tryLock()) {
                try {
                    schedule = scheduleMapper.selectByDoctorAndDate(doctorId, date);
                    if (schedule == null) {
                        // 空值也缓存,防止穿透
                        redisTemplate.opsForValue().set(cacheKey, null, 5, TimeUnit.MINUTES);
                    } else {
                        // 随机过期时间防止雪崩
                        int expireTime = 3600 + new Random().nextInt(600);
                        redisTemplate.opsForValue().set(cacheKey, schedule, expireTime, TimeUnit.SECONDS);
                    }
                } finally {
                    lock.unlock();
                }
            } else {
                // 等待后重试
                Thread.sleep(100);
                return getSchedule(doctorId, date);
            }
        }
        return schedule;
    }
}

缓存问题解决方案: | 问题 | 解决方案 | 代码实现 | |------|----------|----------| | 缓存穿透 | 布隆过滤器 + 空值缓存 | bloomFilter.mightContain() | | 缓存击穿 | 互斥锁 | redissonClient.getLock() | | 缓存雪崩 | 随机过期时间 | 3600 + random(600) |

2. 消息队列 - Kafka

业务场景:问诊完成后需要异步处理短信通知、病历生成、保险推送等任务。

技术方案

# Kafka 配置
spring:
  kafka:
    bootstrap-servers: 127.0.0.1:9092
    producer:
      acks: all  # 所有副本确认
      retries: 3
      properties:
        enable.idempotence: true  # 幂等性
    consumer:
      auto-offset-reset: earliest
      enable-auto-commit: false  # 手动提交
// 生产者
@Service
public class ConsultationEventProducer {
    @Autowired
    private KafkaTemplate<String, Object> kafkaTemplate;
    
    public void sendConsultationComplete(ConsultationEvent event) {
        kafkaTemplate.send("consultation-complete", event.getOrderId(), event)
            .addCallback(
                success -> log.info("消息发送成功:{}", event.getOrderId()),
                failure -> log.error("消息发送失败:{}", event.getOrderId(), failure)
            );
    }
}

// 消费者
@Component
public class ConsultationEventConsumer {
    @KafkaListener(topics = "consultation-complete", groupId = "medical-group")
    public void consume(ConsultationEvent event, Acknowledgment ack) {
        try {
            // 发送短信
            smsService.send(event.getPatientPhone());
            // 生成病历
            medicalRecordService.generate(event);
            // 推送保险
            insuranceService.push(event);
            
            ack.acknowledge(); // 手动提交
        } catch (Exception e) {
            log.error("处理消息失败", e);
            // 发送死信队列
            kafkaTemplate.send("consultation-complete-dlx", event);
        }
    }
}

消息可靠性保证

  1. 生产者 acks=all,确保所有副本写入
  2. 开启幂等性,防止重复发送
  3. 消费者手动提交 offset
  4. 死信队列处理失败消息
  5. 消息落盘,持久化存储
3. 电子病历存储架构

业务场景:电子病历需要支持全文搜索、长期存储、合规审计。

技术方案

┌─────────────────────────────────────────────┐
│              电子病历存储架构                │
├─────────────┬───────────────┬───────────────┤
│    MySQL    │ Elasticsearch │     OSS       │
│  结构化数据  │   全文检索     │  PDF/影像文件  │
│  病历元数据  │   症状搜索     │   长期存储     │
└─────────────┴───────────────┴───────────────┘

第三轮答案:安全、监控与分布式事务

1. 安全认证 - Spring Security + JWT

业务场景:患者、医生、管理员不同角色权限控制,敏感数据加密。

技术方案

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
            .antMatchers("/api/patient/**").hasRole("PATIENT")
            .antMatchers("/api/doctor/**").hasRole("DOCTOR")
            .antMatchers("/api/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated()
            .and()
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // 密码加密
    }
}

// JWT 工具类
@Component
public class JwtUtil {
    private static final String SECRET = "medical-secret-key";
    private static final long EXPIRE = 7200000; // 2 小时
    
    public String generateToken(UserDetails userDetails) {
        return Jwts.builder()
            .setSubject(userDetails.getUsername())
            .claim("role", userDetails.getAuthorities())
            .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
            .signWith(SignatureAlgorithm.HS256, SECRET)
            .compact();
    }
}

敏感数据加密

@Component
public class DataEncryptionUtil {
    // 患者身份证号加密
    public String encryptIdCard(String idCard) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.ENCRYPT_MODE, getSecretKey());
        byte[] encrypted = cipher.doFinal(idCard.getBytes());
        return Base64.getEncoder().encodeToString(encrypted);
    }
}
2. 分布式事务 - Seata

业务场景:医保扣款与处方状态更新需要保证原子性。

AT 模式方案

@GlobalTransactional
public void processInsurancePayment(PaymentDTO dto) {
    // 1. 扣减医保账户
    insuranceAccountService.deduct(dto.getPatientId(), dto.getAmount());
    // 2. 更新处方状态
    prescriptionService.updateStatus(dto.getPrescriptionId(), "PAID");
    // 3. 记录交易流水
    transactionLogService.save(dto);
}

TCC 模式方案(高并发场景):

@TwoPhaseBusinessAction(name = "insurancePaymentTCC", commitMethod = "commit", rollbackMethod = "rollback")
public boolean prepare(BusinessActionContext context, @Param("paymentDTO") PaymentDTO dto) {
    // 一阶段:预留资源
    insuranceAccountService.freeze(dto.getPatientId(), dto.getAmount());
    return true;
}

public boolean commit(BusinessActionContext context) {
    // 二阶段提交:正式扣款
    PaymentDTO dto = (PaymentDTO) context.getActionContext("paymentDTO");
    insuranceAccountService.deduct(dto.getPatientId(), dto.getAmount());
    return true;
}

public boolean rollback(BusinessActionContext context) {
    // 二阶段回滚:解冻资金
    PaymentDTO dto = (PaymentDTO) context.getActionContext("paymentDTO");
    insuranceAccountService.unfreeze(dto.getPatientId(), dto.getAmount());
    return true;
}

事务模式对比: | 模式 | 适用场景 | 性能 | 代码侵入 | |------|----------|------|----------| | AT | 一般业务 | 中 | 低 | | TCC | 高并发核心业务 | 高 | 高 | | Saga | 长流程业务 | 中 | 中 |

3. 监控体系 - Prometheus + Grafana

业务场景:实时监控服务健康状态,快速定位问题。

技术方案

@Configuration
public class MetricsConfig {
    @Bean
    public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
        return registry -> registry.config()
            .commonTags("application", "medical-system", "env", "prod");
    }
}

// 自定义业务指标
@Component
public class BusinessMetrics {
    private final MeterRegistry meterRegistry;
    private final Counter consultationCounter;
    private final Timer consultationTimer;
    
    public BusinessMetrics(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.consultationCounter = Counter.builder("consultation.total")
            .description("问诊总数")
            .register(meterRegistry);
        this.consultationTimer = Timer.builder("consultation.duration")
            .description("问诊耗时")
            .register(meterRegistry);
    }
    
    public void recordConsultation(long duration) {
        consultationCounter.increment();
        consultationTimer.record(duration, TimeUnit.MILLISECONDS);
    }
}

Grafana 监控面板

  • JVM 内存使用率
  • GC 次数与耗时
  • 接口 QPS 与响应时间
  • 错误率统计
  • 数据库连接池状态
4. 链路追踪 - SkyWalking

业务场景:一个问诊请求经过网关→问诊服务→患者服务→处方服务→支付服务,需要追踪全链路。

技术方案

# agent.config
agent.service_name=medical-${HOSTNAME}
collector.backend_service=127.0.0.1:11800

# 应用启动参数
-javaagent:/path/to/skywalking-agent.jar
-Dskywalking.agent.service_name=consultation-service
-Dskywalking.collector.backend_service=127.0.0.1:11800

追踪效果

[Trace ID: 1a2b3c4d5e6f]
├─ Gateway (5ms)
├─ Consultation Service (120ms)
│  ├─ Patient Service (30ms)
│  └─ Prescription Service (80ms)
│     └─ Payment Service (50ms)
└─ Notification Service (15ms)
5. JVM 调优

业务场景:医疗系统高峰期 GC 频繁,响应变慢。

技术方案

# JVM 启动参数
-Xms4g -Xmx4g                    # 堆内存固定,避免动态扩容
-XX:MetaspaceSize=256m           # 元空间大小
-XX:MaxMetaspaceSize=256m        # 元空间上限
-XX:+UseG1GC                     # G1 垃圾收集器
-XX:MaxGCPauseMillis=200         # 最大 GC 停顿时间
-XX:+HeapDumpOnOutOfMemoryError  # OOM 时 dump 内存
-XX:HeapDumpPath=/logs/heapdump  # dump 文件路径
-Xloggc:/logs/gc.log             # GC 日志
-XX:+PrintGCDetails              # 详细 GC 信息

Full GC 排查步骤

  1. jstat -gcutil 查看 GC 统计
  2. jmap -histo 查看对象分布
  3. jmap -dump 导出堆内存
  4. MAT 分析内存泄漏
  5. 定位大对象和长生命周期对象

学习路线建议

🌱 初级工程师(0-2 年)

  • Spring Boot 基础开发
  • RESTful API 设计
  • MySQL 基本操作
  • Redis 基础使用
  • Git 版本控制

🌿 中级工程师(2-5 年)

  • Spring Cloud 微服务架构
  • 消息队列原理与使用
  • 分布式缓存设计
  • 分布式事务理解
  • JVM 基础调优

🌳 高级工程师(5 年+)

  • 高可用架构设计
  • 性能优化与调优
  • 容灾降级方案
  • 全链路监控体系
  • 技术选型与决策

总结

通过这场面试,我们涵盖了互联网医疗系统的核心技术点:

服务治理:Nacos + OpenFeign + Resilience4j ✅ 缓存设计:Redis 三级缓存 + 问题预防 ✅ 消息队列:Kafka 可靠性保证 ✅ 安全认证:Spring Security + JWT + 数据加密 ✅ 分布式事务:Seata AT/TCC 模式 ✅ 监控运维:Prometheus + Grafana + SkyWalking ✅ JVM 调优:参数配置 + 问题排查

希望这篇文章能帮助正在准备面试的同学,祝大家都能拿到心仪的 Offer!🎉


参考资料

  • Spring Cloud 官方文档
  • Redis 设计与实现
  • Kafka 权威指南
  • 深入理解 JVM 虚拟机
  • 分布式事务实战手册
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐