项目实习技术总结
目录
- 项目概述
- 技术架构设计
- 性能优化实践
- 设计模式应用
- 业务流程设计
- 技术难点与解决方案
- 面试话术建议
一、项目概述
1.1 项目背景
HBOS(Hospital Business Operating System) 是一个大型医疗信息系统,采用微服务架构设计,覆盖医院核心业务场景:
- DTC(诊疗中心):医生开立医嘱、处方、检查检验申请
- EMR(电子病历):病历书写、模板管理、质量控制
- HSC(服务项目中心):检查检验项目管理、计费规则
- OTC(订单中心):订单创建、履约、结算
- MCC(医保中心):医保合规检查、结算
- STC(结算中心):费用结算、对账
1.2 技术栈
| 技术分类 |
选型 |
应用场景 |
| RPC框架 |
Dubbo |
微服务间通信 |
| 缓存 |
Caffeine + Redis |
多级缓存策略 |
| 消息队列 |
RocketMQ |
异步解耦、顺序消息 |
| 工作流引擎 |
NatureFlow |
审批流程管理 |
| 数据转换 |
MapStruct |
DTO高性能转换 |
| 分布式锁 |
Redisson |
并发控制 |
| 配置中心 |
BDP |
动态配置管理 |
| ORM |
MyBatis-Plus |
数据访问层 |
二、技术架构设计
2.1 微服务架构 - DDD领域驱动设计
设计思路
采用领域驱动设计(DDD),按业务领域拆分微服务,每个服务负责特定业务领域。
架构分层
1 2 3 4 5 6 7
| hbos-doctor-workstation ├── hbos-dtc ├── hbos-emr ├── hbos-hsc ├── hbos-otc ├── hbos-mcc └── hbos-stc
|
代码示例
1 2 3 4 5 6 7 8
| @RpcConsumer(group = DubboConfigConstant.DTC_GROUP, version = DubboConfigConstant.DEFAULT_VERSION) private IOutpatientPrescriptionApiService prescriptionApiService;
public Result<List<PrescriptionDTO>> queryPrescription(QueryRequest request) { return RpcHelper.handle(prescriptionApiService.queryPrescription(request)); }
|
设计优势
- ✅ 业务解耦:按领域拆分,降低系统复杂度
- ✅ 独立扩展:不同业务模块可独立扩容
- ✅ 故障隔离:单个服务故障不影响整体系统
- ✅ 团队并行开发:支持多团队并行开发
2.2 适配器模式统一服务调用
设计思路
为每个外部服务创建Adapter类,封装RPC调用细节,统一异常处理和日志记录。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Component public class MedicationInquiryAdapter { @RpcConsumer(group = DubboConfigConstant.MC_GROUP, version = DubboConfigConstant.DEFAULT_VERSION) private IMedicationApiService medicationApiService;
public List<MedicationInventoryDTO> queryByIdList(List<Long> ids) { return RpcHelper.handle(medicationApiService.queryByIdList(ids)); } }
@Component public class OrderAdapter { @RpcConsumer(group = RpcConfigConstants.OTC_GROUP, version = "1.0.0") private ITradeOrderApiService tradeOrderApiService;
public List<Long> createOrder(TradeOrderCreateRequest request) { return RpcHelper.handle(tradeOrderApiService.createTradeOrder(request)); } }
|
统一异常处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class RpcHelper { public static <T> T handle(Result<T> result) { if (result == null) { throw new BusinessException("RPC调用返回null"); }
if (!result.isSuccess()) { LoggerUtil.error("RPC调用失败: {}", result.getMessage()); throw new BusinessException(result.getCode(), result.getMessage()); }
return result.getData(); } }
|
设计优势
- ✅ 统一管理:所有RPC调用统一管理,便于监控和治理
- ✅ 解耦依赖:业务代码不直接依赖RPC接口
- ✅ 易于测试:可以轻松Mock Adapter进行单元测试
- ✅ 统一异常处理:避免重复的异常处理代码
2.3 扩展点机制支持定制化
设计思路
采用SPI扩展点机制,支持不同医院的定制化需求,实现可插拔设计。
扩展点接口定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Description("检查申请单扩展") public interface IExaminationApplyThirdExtension extends IExtension {
@Description(title = "构建检查申请单扩展信息", value = "检查申请单推送|获取检查申请单扩展信息") default Map<Long,String> builderExaminationApplyThirdExtension( ExaminationApplyThirdExtensionRequest request) { return Collections.emptyMap(); }
@Description("申请单查询、推送前置过滤扩展") default List<ExaminationApplyDTO> beforeFilter( List<ExaminationApplyDTO> applyList) { return applyList; } }
|
标准实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Service public class ExaminationApplyServiceImpl {
@Extension(defaultBeanName = "defaultExaminationApplyExtension") private IExaminationApplyThirdExtension extension;
public void pushExaminationApply(List<ExaminationApplyDTO> applyList) { List<ExaminationApplyDTO> filteredList = extension.beforeFilter(applyList);
Map<Long, String> extensionMap = extension.builderExaminationApplyThirdExtension(request);
for (ExaminationApplyDTO apply : filteredList) { String extensionInfo = extensionMap.get(apply.getId()); } } }
|
定制化实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Component("customExaminationApplyExtension") public class CustomExaminationApplyExtensionImpl implements IExaminationApplyThirdExtension {
@Override public Map<Long, String> builderExaminationApplyThirdExtension( ExaminationApplyThirdExtensionRequest request) { Map<Long, String> extensionMap = new HashMap<>(); for (ExaminationApplyDTO apply : request.getApplyList()) { String extension = buildCustomExtension(apply); extensionMap.put(apply.getId(), extension); } return extensionMap; } }
|
设计优势
- ✅ 开闭原则:对扩展开放,对修改关闭
- ✅ 可插拔:不同医院实现自己的扩展,不影响标准实现
- ✅ 类型安全:通过接口定义,编译期检查
- ✅ 降低维护成本:不影响标准实现
三、性能优化实践
3.1 大事务瘦身优化 - TransactionSynchronizationManager
业务场景
医嘱提交需要执行:扣减库存、保存医嘱、发送MQ消息、调用第三方接口、发送短信通知等操作。
原有实现(大事务):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Transactional(rollbackFor = Exception.class) public Result submitDoctorOrder(DoctorOrderRequest request) { inventoryService.deductInventory(request.getMedicationList());
DoctorOrder order = buildOrder(request); doctorOrderMapper.insert(order);
rocketMQTemplate.syncSend("order-topic", message);
healthCommissionService.syncOrder(order);
notificationService.sendSms(order.getPatientId(), "医嘱已提交");
return Result.success(); }
|
问题分析:
- 数据库连接持有时间过长:750ms
- 高并发下连接池耗尽:连接池20个,理论最大并发26请求/秒
- 数据库锁持有时间过长:行锁持有750ms
优化方案
核心思想:事务内只做数据库操作,网络IO操作移到事务提交后执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| @Service @Slf4j public class DoctorOrderService {
@Transactional(rollbackFor = Exception.class) public Result submitDoctorOrder(DoctorOrderRequest request) {
inventoryService.deductInventory(request.getMedicationList());
DoctorOrder order = buildOrder(request); doctorOrderMapper.insert(order);
TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronizationAdapter() { @Override public void afterCommit() { try { rocketMQTemplate.syncSend("order-topic", message);
healthCommissionService.syncOrder(order);
notificationService.sendSms(order.getPatientId(), "医嘱已提交");
} catch (Exception e) { log.error("afterCommit执行失败:orderId={}", order.getId(), e); recordCompensationTask(order); } } } );
return Result.success(); } }
|
封装工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| @Component @Slf4j public class TransactionSyncHelper {
public static void registerAfterCommit(Runnable callback) { if (!TransactionSynchronizationManager.isSynchronizationActive()) { callback.run(); return; }
TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronizationAdapter() { @Override public void afterCommit() { try { callback.run(); } catch (Exception e) { log.error("afterCommit回调执行失败", e); } } } ); }
public static void registerAfterCommitAsync(Runnable callback, Executor executor) { registerAfterCommit(() -> executor.execute(callback)); } }
|
优化效果
- ✅ 连接持有时间:从750ms降低到150ms,减少80%
- ✅ 系统吞吐量:从26请求/秒提升到133请求/秒,提升5倍
- ✅ 锁持有时间:从750ms降低到150ms,减少80%
- ✅ 系统稳定性:连接池不再报警
3.2 多级缓存策略
设计思路
- 本地缓存(Caffeine):缓存热点数据,减少网络开销
- 分布式缓存(Redis):跨服务共享数据
- 缓存穿透保护:未命中时从数据库加载并回写
实现代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @Configuration @EnableCaching public class CaffeineCacheConfig { @Bean("caffeineCache") public CacheManager cacheManager() { CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager(); Caffeine<Object, Object> caffeine = Caffeine.newBuilder() .initialCapacity(1000) .expireAfterWrite(600, TimeUnit.SECONDS) .maximumSize(5000); caffeineCacheManager.setCaffeine(caffeine); return caffeineCacheManager; } }
@Service public class DoctorOrderQueryService {
@Autowired private DoctorOrderMapper doctorOrderMapper;
@Cacheable(value = "doctorOrder", key = "#orderId") public DoctorOrderDTO queryById(Long orderId) { return doctorOrderMapper.selectById(orderId); } }
|
优化效果
- ✅ 响应时间从几百毫秒降低到几十毫秒
- ✅ 数据库QPS降低60%以上
- ✅ 支持高并发查询场景
3.3 异步处理优化
设计思路
使用CompletableFuture并行调用多个服务,提高响应速度。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @Service public class DoctorOrderQueryService {
@Autowired private Executor asyncExecutor;
public DoctorOrderDetailDTO queryDetail(Long orderId) { List<CompletableFuture<Void>> futures = new ArrayList<>();
futures.add(CompletableFuture.runAsync(() -> queryMedicationInfo(), asyncExecutor));
futures.add(CompletableFuture.runAsync(() -> queryServiceItemInfo(), asyncExecutor));
futures.add(CompletableFuture.runAsync(() -> queryPatientInfo(), asyncExecutor));
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
return buildDetail(); } }
|
优化效果
- ✅ 多个服务并行调用,总耗时等于最慢的那个服务
- ✅ 提高系统响应速度
- ✅ 提升用户体验
3.4 检验业务并发校验优化 - CompletableFuture实战
业务场景
在医生开立检验医嘱时,需要同时调用多个第三方系统进行合规性校验:
- 库存系统:校验试剂库存是否充足
- 医保系统:校验医保报销资格
- 执行科室:校验执行科室是否接收
- 收费系统:校验收费标准是否存在
- 限价系统:校验项目是否限价
- 资质系统:校验医生是否有开立资质
原有实现(串行调用)
问题:串行调用导致总耗时过长,用户体验差。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| public ValidationResult validate(LabValidationRequest request) { long startTime = System.currentTimeMillis();
ValidationResult inventoryResult = inventoryAdapter.checkInventory(request); if (!inventoryResult.isSuccess()) { return inventoryResult; }
ValidationResult medicareResult = medicareAdapter.checkMedicare(request); if (!medicareResult.isSuccess()) { return medicareResult; }
ValidationResult departmentResult = departmentAdapter.checkDepartment(request); if (!departmentResult.isSuccess()) { return departmentResult; }
ValidationResult chargeResult = chargeAdapter.checkCharge(request); if (!chargeResult.isSuccess()) { return chargeResult; }
ValidationResult priceResult = priceLimitAdapter.checkPriceLimit(request); if (!priceResult.isSuccess()) { return priceResult; }
ValidationResult qualificationResult = qualificationAdapter.checkQualification(request); if (!qualificationResult.isSuccess()) { return qualificationResult; }
long endTime = System.currentTimeMillis(); log.info("串行校验总耗时: {}ms", endTime - startTime);
return ValidationResult.success(); }
|
优化方案(并行校验)
核心思路:利用CompletableFuture并行调用所有校验接口,总耗时等于最慢的那个接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
| @Service @Slf4j public class LabValidationService {
private final ThreadPoolExecutor validationExecutor = new ThreadPoolExecutor( 10, 20, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100), new ThreadFactoryBuilder().setNamePrefix("lab-validation-").build(), new ThreadPoolExecutor.CallerRunsPolicy() );
@Autowired private InventoryAdapter inventoryAdapter; @Autowired private MedicareAdapter medicareAdapter; @Autowired private DepartmentAdapter departmentAdapter; @Autowired private ChargeAdapter chargeAdapter; @Autowired private PriceLimitAdapter priceLimitAdapter; @Autowired private QualificationAdapter qualificationAdapter;
public ValidationResult validateConcurrent(LabValidationRequest request) { long startTime = System.currentTimeMillis();
try { CompletableFuture<ValidationResult> inventoryFuture = CompletableFuture.supplyAsync(() -> inventoryAdapter.checkInventory(request), validationExecutor);
CompletableFuture<ValidationResult> medicareFuture = CompletableFuture.supplyAsync(() -> medicareAdapter.checkMedicare(request), validationExecutor);
CompletableFuture<ValidationResult> departmentFuture = CompletableFuture.supplyAsync(() -> departmentAdapter.checkDepartment(request), validationExecutor);
CompletableFuture<ValidationResult> chargeFuture = CompletableFuture.supplyAsync(() -> chargeAdapter.checkCharge(request), validationExecutor);
CompletableFuture<ValidationResult> priceLimitFuture = CompletableFuture.supplyAsync(() -> priceLimitAdapter.checkPriceLimit(request), validationExecutor);
CompletableFuture<ValidationResult> qualificationFuture = CompletableFuture.supplyAsync(() -> qualificationAdapter.checkQualification(request), validationExecutor);
CompletableFuture.allOf( inventoryFuture, medicareFuture, departmentFuture, chargeFuture, priceLimitFuture, qualificationFuture ).join();
ValidationResult inventoryResult = inventoryFuture.get(); ValidationResult medicareResult = medicareFuture.get(); ValidationResult departmentResult = departmentFuture.get(); ValidationResult chargeResult = chargeFuture.get(); ValidationResult priceLimitResult = priceLimitFuture.get(); ValidationResult qualificationResult = qualificationFuture.get();
if (!inventoryResult.isSuccess()) { return inventoryResult; } if (!medicareResult.isSuccess()) { return medicareResult; } if (!departmentResult.isSuccess()) { return departmentResult; } if (!chargeResult.isSuccess()) { return chargeResult; } if (!priceLimitResult.isSuccess()) { return priceLimitResult; } if (!qualificationResult.isSuccess()) { return qualificationResult; }
long endTime = System.currentTimeMillis(); log.info("并发校验总耗时: {}ms", endTime - startTime);
return ValidationResult.success();
} catch (Exception e) { log.error("并发校验异常", e); return ValidationResult.error("校验异常: " + e.getMessage()); } } }
|
技术要点
-
线程池配置:
- 核心线程数10:根据服务器CPU核心数和IO等待时间调整
- 最大线程数20:应对突发流量
- 队列容量100:缓冲任务,避免频繁创建线程
- 拒绝策略CallerRunsPolicy:保证任务不丢失,由调用者线程执行
-
CompletableFuture使用:
supplyAsync():异步执行有返回值的任务
allOf().join():等待所有任务完成
get():获取任务结果(会阻塞)
-
异常处理:
- 使用try-catch包裹,避免线程池中的异常影响主线程
- 统一返回错误信息
优化效果对比
| 指标 |
串行调用 |
并行调用 |
提升 |
| 总耗时 |
2500ms |
800ms |
68% |
| 用户体验 |
差(等待2.5秒) |
好(等待0.8秒) |
显著提升 |
| 系统吞吐量 |
40请求/秒 |
125请求/秒 |
3倍 |
面试话术
"在检验业务性能优化中,我发现原有实现采用串行调用6个第三方校验接口,总耗时2.5秒,用户体验很差。
我通过CompletableFuture实现了并行校验:
- 自定义线程池:根据IO密集型特点,配置核心线程10、最大线程20、队列100
- 异步提交任务:使用
supplyAsync()并行提交所有校验任务
- 等待所有完成:使用
allOf().join()等待所有任务完成
- 结果汇总:按优先级顺序检查结果
优化效果:总耗时从2.5秒降低到0.8秒,性能提升68%,系统吞吐量提升3倍。
这个优化让我深入理解了CompletableFuture的原理和应用场景,也掌握了线程池的配置技巧。"
四、设计模式应用
4.1 责任链模式 - 验证器链
设计思路
采用责任链模式实现医嘱验证,每种操作类型对应不同的验证链。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class CreateDoctorOrderValidator extends BaseValidationChain<DoctorOrder> { @PostConstruct public void init() { List<BaseValidation<DoctorOrder>> nodeLists = new ArrayList<>(); nodeLists.add(dataCompletionValidation); nodeLists.add(patientHealthcareStatusValidation); nodeLists.add(medicationDataCompletionValidation); nodeLists.add(authorityValidation); nodeLists.add(qualificationValidation); nodeLists.add(medicationAllergyValidation); setNodeList(nodeLists); } }
CreateDoctorOrderValidator validator = new CreateDoctorOrderValidator(); validator.validate(doctorOrder);
|
设计优势
- ✅ 业务复杂:支持几十种验证规则的灵活组合
- ✅ 可扩展性:新增验证规则只需添加新的验证器节点
- ✅ 职责分离:每个验证器只负责一个验证点
- ✅ 易于测试:可以单独测试每个验证器
4.2 策略模式 - 动态配置
设计思路
通过配置中心(BDP)动态配置验证规则,支持不同医院、不同场景的定制化需求。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Service public class DoctorOrderService {
@Autowired private FactorAdapter factorAdapter;
public Result submitDoctorOrder(DoctorOrderRequest request) { Map<String, Object> configBatch = bdpConfig.getMapValue( healthcareType, null, Maps.newHashMap(), SHORTCUT_BUTTON_OPERATION_CODE.getCode());
if (configBatch.containsKey("skipValidation")) { }
return processOrder(request); } }
|
设计优势
- ✅ 动态配置:无需重启服务即可修改规则
- ✅ 灵活控制:支持不同医院、不同场景的定制化
- ✅ 降低维护成本:避免频繁发版
4.3 模板方法模式 - AOP实现
设计思路
通过AOP切面实现接口熔断和降级,统一处理横切关注点。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| @Aspect @Component public class ThirdInterfaceAspect {
private static final Map<Class<?>, ThirdInterface> CLASS_CACHE = new ConcurrentHashMap<>(32);
@Pointcut("execution(* com.c2f.hbos.thirdparty.client.standard..*.*(..))") public void pointcut() { }
@Around("pointcut()") public Object around(ProceedingJoinPoint point) throws Throwable { ThirdInterface classAnnotation = CLASS_CACHE.computeIfAbsent( point.getTarget().getClass(), aClazz -> AnnotationUtils.findAnnotation(aClazz, ThirdInterface.class) );
if (Objects.isNull(classAnnotation)) { return point.proceed(); }
Map thirdInterfaceConfigMap = factorAdapter.getConfigRegistryValue( ExtensionConfigEnum.THIRD_INTERFACE_CONFIG.getParamCode(), Map.class );
String interfaceCode = classAnnotation.value() + "." + method.getName();
Object value = Optional.ofNullable(thirdInterfaceConfigMap.get(interfaceCode)) .orElse(thirdInterfaceConfigMap.get(classAnnotation.value()));
if (Objects.equals(value, 1)) { return Result.error("接口已关闭", interfaceCode); } else if (Objects.equals(value, 2)) { return fallback(method); } else { return point.proceed(); } }
private Object fallback(Method method) { if (method.getReturnType() == Result.class) { ResolvableType returnType = ResolvableType.forMethodReturnType(method); ResolvableType genericType = returnType.getGeneric(0); Class<?> genericClass = genericType.getRawClass();
if (Collection.class.isAssignableFrom(genericClass)) { return genericClass.equals(Set.class) ? Result.success(Collections.emptySet()) : Result.success(Collections.emptyList()); } return Result.success(); } return null; } }
|
注解定义
1 2 3 4 5 6
| @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface ThirdInterface { String value(); String desc(); }
|
使用示例
1 2 3 4 5 6 7 8
| @ThirdInterface(value = "hemodialysis", desc = "血透系统") public class HemodialysisServiceImpl {
@ThirdInterface(value = "syncApply", desc = "申请单同步") public Result<Void> syncApply(SyncApplyRequest request) { } }
|
设计优势
- ✅ 动态控制:通过配置中心动态控制,无需重启服务
- ✅ 细粒度控制:支持类级别和方法级别的控制
- ✅ 性能优化:使用缓存避免重复反射获取注解
- ✅ 灵活降级:根据返回类型返回合适的默认值
4.4 MapStruct实现高效数据转换
设计思路
使用MapStruct在编译时生成转换代码,实现高效、类型安全的数据转换。
代码实现
1 2 3 4 5 6 7 8 9 10
| @Mapper(componentModel = "spring") public interface DoctorOrderMapStruct { DoctorOrderDTO toDTO(DoctorOrder entity);
OptDoctorOrderDTO toOptDTO(DoctorOrderIntegralDTO source, HealthcareRecordDTO healthcareRecord, DoctorStationContext context);
List<DoctorOrderDTO> toList(List<DoctorOrder> entities); }
|
使用示例
1 2 3 4 5 6 7 8 9 10 11
| @Service public class DoctorOrderService {
@Autowired private DoctorOrderMapper doctorOrderMapper;
public DoctorOrderDTO getById(Long orderId) { DoctorOrder entity = doctorOrderMapper.selectById(orderId); return DoctorOrderMapStruct.INSTANCE.toDTO(entity); } }
|
设计优势
- ✅ 性能优化:编译时生成代码,运行时无反射开销
- ✅ 类型安全:编译期检查,避免运行时错误
- ✅ 代码简洁:自动生成转换代码,减少样板代码
- ✅ 性能提升:相比手动转换性能提升10倍以上
五、业务流程设计
5.1 工作流引擎驱动业务流程
设计思路
集成NatureFlow工作流引擎处理审批流程,支持动态流程配置。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @EnableWorkflow @SpringBootApplication public class Application { }
@Service public class ApprovalProcessService {
@Autowired private WorkflowEngine workflowEngine;
public void startApproval(ProcessStartRequest request) { WorkflowInstance instance = workflowEngine.startProcess( "medical_approval", request.getBusinessKey(), request.getVariables() ); } }
|
设计优势
- ✅ 业务流程灵活配置:通过配置定义流程,无需修改代码
- ✅ 可追溯性:记录流程执行历史,便于审计
- ✅ 满足合规要求:医疗行业需要严格的流程审批
5.2 消息队列实现事件驱动
设计思路
使用RocketMQ实现异步消息处理,实现事件驱动架构。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @MqConsumer( topic = "ace", selectorExpression = "hbos_approval_process", consumerGroup = "HBOS_DTC_GROUP_CONSUMER", consumeMode = ConsumeMode.ORDERLY, maxReconsumeTimes = 1 ) public void onMessage(MqMessage message) { WorkFlowEventDataDTO eventData = JSON.parseObject( new String(message.getBody()), WorkFlowEventDataDTO.class);
approvalProcessService.handleEvent(eventData); }
|
设计优势
- ✅ 异步处理:医嘱提交后异步通知多个系统
- ✅ 削峰填谷:高峰期消息可以暂存,平滑处理
- ✅ 可靠性:MQ保证消息不丢失,支持重试机制
- ✅ 系统解耦:实现松耦合的事件驱动架构
5.3 订单中心业务流程
订单创建流程
graph TD
A[医嘱提交] --> B[订单中心创建订单]
B --> C[计费中心计算费用]
C --> D[执行中心生成执行计划]
D --> E[订单履约]
E --> F[费用结算]
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| @Transactional(rollbackFor = Exception.class) public Result submitDoctorOrder(DoctorOrderRequest request) { DoctorOrder order = buildOrder(request); doctorOrderMapper.insert(order);
TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronizationAdapter() { @Override public void afterCommit() { OrderMessage orderMessage = buildOrderMessage(order); rocketMQTemplate.syncSend("order-create-topic", orderMessage);
BillingMessage billingMessage = buildBillingMessage(order); rocketMQTemplate.syncSend("billing-calculate-topic", billingMessage);
ExecutionPlanMessage planMessage = buildExecutionPlanMessage(order); rocketMQTemplate.syncSend("execution-plan-topic", planMessage); } } );
return Result.success(order); }
|
六、技术难点与解决方案
6.1 分布式锁保证并发安全
问题场景
医嘱执行、计费等操作需要保证同一患者同一时间只有一个操作。
解决方案
使用Redisson实现分布式锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| @Component public class DistributeLockUtil {
public boolean lock(String key, Long expireSecond) { RLock lock = redissonClient.getLock(key); return lock.tryLock(expireSecond, TimeUnit.SECONDS); }
public void unlock(String key) { RLock lock = redissonClient.getLock(key); if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }
String redisKey = generateRedisKey(orgId, campusId, healthcareRecordId); if (!distributeLockUtil.lock(redisKey, 10L)) { return Result.error("操作正在进行,请勿重复提交"); }
try { processOrder(order); } finally { distributeLockUtil.unlock(redisKey); }
|
6.2 医嘱数据模型复杂
问题
- 医嘱包含20+种类型(药品、检查、检验、手术等)
- 每种类型有独特的扩展属性
- 需要支持父子医嘱、组合医嘱等复杂关系
解决方案
采用**扩展字段(Extension)**存储不同类型医嘱的特殊属性。
1 2 3 4 5 6 7 8 9 10 11
| public class DoctorOrderBasicDTO { private Long id; private String managementCategoryCode; private String extension; }
public enum DoctorOrderExtendsionPropertyEnum { INFORMED_CONSENT_INSTANCE_ID("informedConsentInstanceId"), EXAMINATION_APPLY_ID("examinationApplyId"); }
|
6.3 多系统数据一致性
问题
医嘱提交需要同步到订单中心、计费中心、执行计划等多个系统,需要保证数据一致性。
解决方案
- 采用最终一致性,通过消息队列异步同步
- 关键操作使用**分布式事务(Seata)**或补偿机制
- 提供数据对账和修复机制
6.4 病案归档流程完善 - MQ顺序消费+定时补偿+分布式锁
业务背景
根据卫健委监管要求,医院需要将病案首页数据定期上报到卫健委平台。上报失败需要重试,且要求数据不能重复、不能遗漏、顺序一致。
原有实现及问题
问题:
- 上报接口偶尔超时,导致数据上报失败
- 重试逻辑混乱,可能出现重复上报
- 没有补偿机制,数据遗漏无法发现
- 多实例部署时,并发上报导致数据重复
优化方案
6.4.1 RocketMQ顺序消息保证数据顺序
核心思路:将同一患者的病案数据发送到同一个队列分区,保证同一患者的病案按顺序消费。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| @Service @Slf4j public class MedicalArchiveService {
@Autowired private RocketMQTemplate rocketMQTemplate;
public void sendArchiveMessage(MedicalRecord record) { ArchiveMessage message = ArchiveMessage.builder() .recordId(record.getId()) .patientId(record.getPatientId()) .archiveData(buildArchiveData(record)) .build();
SendResult sendResult = rocketMQTemplate.syncSendOrderly( "medical-archive-topic", message, record.getPatientId() );
if (!sendResult.getSendStatus().equals(SendStatus.SEND_OK)) { log.error("病案归档消息发送失败: recordId={}", record.getId()); throw new BusinessException("消息发送失败"); }
medicalRecordMapper.updateStatus(record.getId(), ArchiveStatus.PENDING); } }
|
消费端顺序消费:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| @Component @Slf4j public class ArchiveMessageConsumer {
@Autowired private ArchiveService archiveService;
@MqConsumer( topic = "medical-archive-topic", consumerGroup = "ARCHIVE_CONSUMER_GROUP", consumeMode = ConsumeMode.ORDERLY, // 顺序消费模式 maxReconsumeTimes = 3 // 最多重试3次 ) public void onMessage(MqMessage message) { try { ArchiveMessage archiveMessage = JSON.parseObject( new String(message.getBody()), ArchiveMessage.class );
archiveService.reportToHealthCommission(archiveMessage);
} catch (Exception e) { log.error("病案归档消息消费失败", e); throw e; } } }
|
6.4.2 定时任务补偿机制
问题:即使有MQ重试机制,仍然可能出现消息丢失的情况(极端情况)。
解决方案:定时任务扫描未上报的病案,重新发送MQ消息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| @Component @Slf4j public class ArchiveCompensationTask {
@Autowired private MedicalRecordMapper medicalRecordMapper;
@Autowired private RocketMQTemplate rocketMQTemplate;
@Scheduled(cron = "0 */30 * * * * ?") public void compensateFailedArchive() { log.info("开始执行病案归档补偿任务");
try { List<MedicalRecord> failedRecords = medicalRecordMapper.selectList( new LambdaQueryWrapper<MedicalRecord>() .in(MedicalRecord::getArchiveStatus, ArchiveStatus.PENDING, ArchiveStatus.FAILED) .eq(MedicalRecord::getDeleted, 0) .orderByAsc(MedicalRecord::getCreateTime) .last("LIMIT 100") );
if (CollectionUtils.isEmpty(failedRecords)) { log.info("没有需要补偿的病案数据"); return; }
log.info("扫描到{}条需要补偿的病案数据", failedRecords.size());
for (MedicalRecord record : failedRecords) { try { sendArchiveMessage(record); } catch (Exception e) { log.error("重新发送病案消息失败: recordId={}", record.getId(), e); } }
} catch (Exception e) { log.error("病案归档补偿任务执行失败", e); } } }
|
6.4.3 分布式锁防止重复处理
问题:多实例部署时,定时任务会同时执行,导致同一病案被重复处理。
解决方案:使用Redisson分布式锁,保证同一时间只有一个实例执行补偿任务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| @Component @Slf4j public class ArchiveCompensationTask {
@Autowired private RedissonClient redissonClient;
@Scheduled(cron = "0 */30 * * * * ?") public void compensateFailedArchive() { String lockKey = "archive:compensation:lock";
RLock lock = redissonClient.getLock(lockKey); boolean locked = false;
try { locked = lock.tryLock(0, 30, TimeUnit.SECONDS);
if (!locked) { log.info("获取分布式锁失败,其他实例正在执行补偿任务"); return; }
log.info("获取分布式锁成功,开始执行补偿任务");
doCompensate();
} catch (InterruptedException e) { log.error("分布式锁获取失败", e); Thread.currentThread().interrupt(); } finally { if (locked && lock.isHeldByCurrentThread()) { lock.unlock(); log.info("释放分布式锁"); } } }
private void doCompensate() { } }
|
6.4.4 幂等性设计防止重复上报
问题:即使有分布式锁,仍然可能出现极端情况导致重复上报(如消息重复发送)。
解决方案:在上报接口中增加幂等性校验,使用数据库状态字段防止重复处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| @RestController @RequestMapping("/api/archive") @Slf4j public class ArchiveController {
@PostMapping("/report") public Result<Void> reportArchive(@RequestBody ArchiveReportRequest request) { Long recordId = request.getRecordId();
String lockKey = "archive:report:" + recordId; RLock lock = redissonClient.getLock(lockKey); boolean locked = false;
try { locked = lock.tryLock(0, 10, TimeUnit.SECONDS); if (!locked) { return Result.error("操作正在进行,请勿重复提交"); }
MedicalRecord record = medicalRecordMapper.selectById(recordId); if (record == null) { return Result.error("病案不存在"); }
if (ArchiveStatus.SUCCESS.equals(record.getArchiveStatus())) { log.info("病案已上报,跳过: recordId={}", recordId); return Result.success(); }
boolean success = healthCommissionService.report(record); if (success) { medicalRecordMapper.updateStatus(recordId, ArchiveStatus.SUCCESS); return Result.success(); } else { medicalRecordMapper.updateStatus(recordId, ArchiveStatus.FAILED); return Result.error("上报失败"); }
} catch (Exception e) { log.error("病案上报异常: recordId={}", recordId, e); return Result.error("上报异常"); } finally { if (locked && lock.isHeldByCurrentThread()) { lock.unlock(); } } } }
|
6.4.5 卫健委回调接口 - MinIO临时文件
问题:卫健委需要主动拉取病案数据,且文件较大(几十MB)。
解决方案:
- 上传病案PDF到MinIO对象存储
- 生成临时访问URL(有效期7天)
- 卫健委通过回调接口获取URL和校验和
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| @RestController @RequestMapping("/api/archive/callback") @Slf4j public class HealthCommissionCallbackController {
@Autowired private MedicalRecordService medicalRecordService;
@Autowired private MinioService minioService;
@PostMapping("/getArchiveInfo") public Result<ArchiveInfoResponse> getArchiveInfo(@RequestBody ArchiveInfoRequest request) { Long recordId = request.getRecordId();
MedicalRecord record = medicalRecordService.getByRecordId(recordId); if (record == null) { return Result.error("病案不存在"); }
String fileUrl = minioService.getPresignedUrl( record.getMinioBucket(), record.getMinioObjectName(), 7, TimeUnit.DAYS );
ArchiveInfoResponse response = ArchiveInfoResponse.builder() .recordId(recordId) .patientId(record.getPatientId()) .fileUrl(fileUrl) .checksum(record.getChecksum()) .build();
return Result.success(response); } }
|
技术要点总结
-
MQ顺序消息:
- 使用患者ID作为hashKey,保证同一患者的消息有序
- 顺序消费模式(consumeMode=ORDERLY)
- 失败重试机制(maxReconsumeTimes=3)
-
定时任务补偿:
- 每30分钟扫描未上报数据
- 批量处理(LIMIT 100)避免积压
- 结合分布式锁防止多实例重复执行
-
分布式锁:
- Redisson实现分布式锁
- 细粒度锁(record级别)
- 自动释放机制(tryLock timeout)
-
幂等性设计:
- 数据库状态字段校验(已上报直接返回)
- 分布式锁防止并发
- 医委回调接口只做查询,不做状态变更
面试话术
"在病案归档模块中,我负责优化上报流程,解决数据重复、遗漏、顺序不一致等问题。
核心挑战:卫健委要求病案上报不能重复、不能遗漏、顺序一致,且需要支持卫健委主动拉取数据。
我的解决方案:
- RocketMQ顺序消息:使用患者ID作为hashKey,保证同一患者的病案按顺序消费
- 定时任务补偿:每30分钟扫描未上报数据,重新发送MQ消息,避免数据遗漏
- 分布式锁:多实例环境下,防止定时任务重复执行
- 幂等性设计:通过状态字段校验,防止重复上报
- MinIO临时文件:生成7天有效期的临时URL,支持卫健委主动拉取
效果:系统上线后运行稳定,未出现数据重复或遗漏情况,顺利通过卫健委验收。
这个方案让我深入理解了分布式系统中数据一致性的保证机制,也掌握了RocketMQ顺序消息、分布式锁、幂等性设计等核心技术。"
七、面试话术建议
7.1 项目介绍
"我参与开发的HBOS医生工作站是一个大型医疗信息系统,采用微服务架构设计,涵盖医生开立医嘱、处方、检查检验申请等核心业务。
项目采用Spring Boot + Dubbo微服务架构,按业务领域拆分为DTC诊疗中心、EMR电子病历、HSC服务项目中心、OTC订单中心等6个核心服务。
我的职责包括核心业务开发、性能优化、技术架构设计等,主要负责医嘱提交、检验检查申请、第三方系统集成等核心模块。"
7.2 大事务瘦身优化
"在做系统稳定性治理时,我发现高并发下数据库连接池经常报警。排查发现是长事务导致的。
业务场景:在医嘱提交、费用结算等核心链路中,需要执行一系列操作:扣减库存、写入数据库、发送MQ消息通知下游、调用第三方接口(如医保局)同步数据、发送短信通知等。
问题分析:原有代码将所有操作都包在一个@Transactional大事务里。虽然数据库操作很快(150ms),但MQ发送和第三方接口调用涉及网络IO,耗时较长(600ms)。在这段时间内,数据库连接一直被持有无法释放。高并发下,数据库连接池迅速被占满,导致后续请求获取不到连接而报错。
解决方案:我利用Spring的TransactionSynchronizationManager进行了优化:
- 事务剥离:识别出非强一致性、非DB操作(如发MQ、调三方接口、发短信),这些操作不需要在事务内执行。
- afterCommit回调:使用
TransactionSynchronizationManager.registerSynchronization注册afterCommit回调钩子,将这些耗时操作移到事务提交后执行。
- 异步执行:在afterCommit中使用异步线程池执行网络IO操作,不阻塞主线程。
效果:数据库连接持有时间从750ms降低到150ms,缩短80%;系统并发吞吐量从26请求/秒提升到133请求/秒,提升5倍。"
7.3 设计模式应用
"在项目中广泛应用设计模式解决复杂问题:
- 适配器模式:统一封装20+种第三方系统的接口调用,通过Adapter层解耦业务代码与第三方系统
- 责任链模式:实现医嘱验证器链,支持几十种验证规则的灵活组合
- 策略模式:通过配置中心动态配置验证规则,支持不同医院的定制化需求
- 模板方法模式:通过AOP实现接口熔断和降级机制,统一处理横切关注点
- MapStruct:实现高性能数据转换,编译时生成代码,性能提升10倍以上"
7.4 性能优化
"通过多种手段提升系统性能:
- 多级缓存:Caffeine本地缓存 + Redis分布式缓存,响应时间从几百毫秒降低到几十毫秒
- 异步处理:使用CompletableFuture并行调用多个服务,提高响应速度
- 批量查询:减少RPC调用次数,降低网络开销
- 数据库优化:索引优化、分页查询,数据库QPS降低60%以上"
7.5 技术难点
"项目中遇到的主要技术难点:
- 医嘱数据模型复杂:20+种医嘱类型,每种类型有独特的扩展属性,采用JSON扩展字段解决
- 多系统数据一致性:采用最终一致性 + 消息队列 + 补偿机制保证
- 高并发查询:多级缓存 + 批量查询 + 并行调用优化
- 定制化需求:通过SPI扩展点机制支持不同医院的定制化需求"
八、总结
8.1 核心技术亮点
- 微服务架构设计优秀:采用DDD领域驱动设计,服务划分合理
- 性能优化到位:多级缓存、大事务瘦身、批量查询、并行调用等
- 代码质量高:大量使用设计模式,代码结构清晰
- 可扩展性强:适配器模式、扩展点机制等设计支持灵活扩展
- 业务理解深入:对医疗业务理解深入,解决方案贴合实际需求
8.2 技术收获
- 掌握微服务架构设计和DDD领域驱动设计
- 熟练使用各种设计模式解决实际问题
- 深入理解Spring事务机制和优化技巧
- 掌握性能优化的多种手段
- 理解医疗业务流程和行业特点
本文档整合了熙牛医疗HBOS项目的核心技术内容,涵盖架构设计、性能优化、设计模式、业务流程等方面,适合用于面试准备和技术总结。