4. 事件生产者设计
事件生产者是事件驱动医疗集成链路的起点。它的职责不是“把数据发出去”这么简单,而是要在EHR(电子病历系统)内部准确识别“什么发生了变化、变化是否值得通知、通知谁、以什么格式通知”。如果生产者设计不当,下游再强的消费者也只能处理噪声、重复或语义缺失的事件。本节围绕四个关键问题展开:FHIR事件从哪里触发、资源变更如何捕获、消息中间件如何选型、事件载荷如何标准化。
一、从EHR触发FHIR事件:触发点决定事件语义
FHIR本身不是消息中间件,它定义的是资源模型与交互方式。事件驱动集成中,EHR通常通过三种方式产生事件:一是业务动作触发,例如患者入院登记、医嘱开立、检验报告审核发布;二是资源状态变化触发,例如Encounter.status从in-progress变为finished;三是订阅机制触发,即FHIR Subscription或Topic-based Subscription在资源满足条件时推送通知。
设计触发点时,必须区分“业务事件”与“资源事件”。业务事件是临床语义,如“报告已审核”;资源事件是技术语义,如“DiagnosticReport资源被更新”。一个业务事件可能对应多个资源变更,例如报告审核会同时更新DiagnosticReport、Observation和Provenance。若只按资源变更发事件,下游会收到碎片化通知;若只按业务事件发,又可能丢失资源级细节。推荐做法是:以业务事件为事件类型,以FHIR资源为载荷,并在事件头中携带资源引用与版本号。
触发时机上,应避免在数据库事务提交前发事件。若事务回滚,事件已发出,会造成下游脏数据。正确做法是采用事务性发件箱(Transactional Outbox):业务数据与待发事件写入同一数据库事务,再由独立投递器读取发件箱并发送到消息队列。这样可保证“业务成功则事件最终发出,业务失败则事件不发出”。
二、资源变更捕获:CDS Hooks与审计事件
资源变更捕获有三种常见手段。第一种是数据库变更数据捕获(CDC),通过解析数据库日志获取行级变化。它侵入性低,但只能看到表字段,难以还原FHIR资源语义。第二种是应用层拦截,在EHR服务层对FHIR资源的create/update/patch操作埋点。它语义清晰,但需要改造现有代码。第三种是审计事件,利用FHIR AuditEvent或IHE ATNA审计日志,从审计流中提取资源变更。审计事件覆盖面广,但延迟较高,且通常不包含完整资源内容,只适合做补充或合规追溯。
CDS Hooks则提供了另一种“临床工作流内触发”的机制。它不是在资源变更后异步通知,而是在临床决策点同步调用。例如医生打开处方界面时,EHR向CDS服务发起order-select钩子,CDS服务返回卡片提示。CDS Hooks适合实时干预,不适合作为异步集成的事件源。若要把CDS Hooks结果转化为事件,可在钩子响应后由EHR生成一条内部事件,再投递到消息队列。
三、消息队列选型:Kafka与RabbitMQ的取舍
Kafka与RabbitMQ是医疗集成中最常见的两类中间件,但它们的模型不同。Kafka是分布式提交日志,消息按主题分区持久化,消费者按offset拉取,支持重放。RabbitMQ是传统消息代理,消息被消费后通常删除,支持复杂路由、优先级和延迟队列。
| 维度 | Kafka | RabbitMQ |
|---|---|---|
| 消息保留 | 按时间/大小保留,可重放 | 消费后删除,重放需额外设计 |
| 吞吐量 | 高,适合大量事件流 | 中高,适合任务分发 |
| 路由能力 | 主题+分区,较简单 | Exchange+Binding,灵活 |
| 顺序保证 | 分区内有序 | 队列内有序 |
| 典型场景 | 资源变更流、审计流、数据管道 | 任务队列、通知分发、RPC |
医疗集成中,若事件需要被多个下游独立消费、需要回放以重建状态、或需要长期留存用于审计,Kafka更合适。若事件是“一次性任务”,例如发送一条短信提醒、调用一次外部接口,且需要失败重试与死信队列,RabbitMQ更直接。实际项目中也可两者并用:Kafka承载FHIR资源变更主事件流,RabbitMQ承载通知类任务。
四、事件格式标准化:CloudEvents与FHIR Bundle
事件格式标准化要解决两个层面:信封(envelope)与载荷(payload)。信封描述“谁在什么时候发了什么类型的事件”,载荷承载FHIR资源。推荐信封采用CloudEvents 1.0规范,核心字段包括:id、source、type、subject、time、datacontenttype、data。其中type建议采用反向域名,例如cn.hospital.ehr.diagnosticreport.reviewed.v1。
载荷建议使用FHIR Bundle,类型为collection或transaction。若事件只通知“发生了什么”,可只携带资源引用;若下游需要完整数据,则携带资源本体。为避免大消息,可设置阈值:资源超过256KB时只发引用,由下游按需拉取。同时必须在信封中携带resourceVersion或meta.versionId,以便下游做幂等与顺序判断。
{
"specversion": "1.0",
"id": "evt-20240612-000871",
"source": "/ehr/order-service",
"type": "cn.hospital.ehr.diagnosticreport.reviewed.v1",
"subject": "DiagnosticReport/12345",
"time": "2024-06-12T10:23:41+08:00",
"datacontenttype": "application/fhir+json",
"data": {
"resourceType": "Bundle",
"type": "collection",
"entry": [
{ "resource": { "resourceType": "DiagnosticReport", "id": "12345", "status": "final" } }
]
}
}
标准化还包括命名与版本策略。事件类型应带版本号,如.v1;资源结构变化时新增版本而非修改旧版本。消费者按版本路由,避免升级导致全链路中断。此外,所有事件应携带traceparent或correlationId,便于跨系统追踪。
最后,生产者设计必须包含可观测性。每个事件应记录发送时间、队列确认时间、重试次数与失败原因。建议在发件箱投递器中暴露指标:待发送事件数、发送延迟P95、失败率。当失败率超过阈值时告警,而不是静默重试。只有把触发、捕获、传输、标准化四层都设计清楚,事件生产者才能成为可靠的数据源头,而不是集成链路中最薄弱的环节。