尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java互联网医院系统实战:高并发架构、分布式事务与线上事故复盘

Java互联网医院系统实战:高并发架构、分布式事务与线上事故复盘 简介这是一套基于 Java 与 SpringBoot 构建的互联网医院系统源码面向医疗信息化开发者、Java 后端工程师及需要搭建在线问诊平台的机构。资源围绕图文、视频、语音问诊在线病历、处方模板、远程会诊、复诊购药等核心业务展开同时包含订单、运营、医政、财务、用户管理等后台模块适用于快速搭建高效医疗服务平台的学习与二次开发场景。包内共 2054 个文件压缩包约 383MB以 Java 源码1256 个、XML 配置540 个为主辅以 HTML 页面、JavaScript 脚本、CSS 样式及数据库 SQL 文件覆盖后端逻辑、前端界面与数据表结构整体目录清晰便于按模块阅读和调试。已有 163 人浏览学习适合具有一定 Java 基础、希望掌握医疗系统全栈实现的开发者参考可据此了解业务功能设计、权限控制与数据安全处理思路。 做Java互联网医院系统这几年最深刻的一次教训来自上线后的第一个周一早高峰。8点整放号并发瞬间涌进来我看着监控屏上网关线程被打满、Redis CPU飙到90%多紧接着告警群里开始刷“订单服务超时”。那天之后我重写了号源扣减和订单链路的整块逻辑也彻底明白了互联网医院不是一个把线下流程搬到线上的管理后台它本质上是一套需要扛住真实流量冲击的Java分布式系统。这篇文章我把整套系统从业务梳理、技术选型、核心代码、线上事故排查到数据安全控制完整复盘一遍适合正在做或准备做医疗信息化项目的Java工程师参考也适合准备面试医疗类互联网项目的人拿来当实战素材。1. 先盘业务再谈技术互联网医院的链路与角色1.1 互联网医院不是把线下医院照搬上网很多人一听到“互联网医院”就以为是做个App把挂号、缴费、查报告这几个功能堆上去就完事。真正做起来才发现这个系统的核心难点在于它是长流程、多角色、强状态的撮合平台。一条完整的在线复诊主流程是这样的患者提交复诊申请系统根据科室和病情做分诊医生接诊后在线图文或视频问诊问诊过程中可以开具电子处方处方需要药师在线审核审核通过后患者支付药品费用然后药房配药、物流配送最后进入康复随访环节。这中间还没算上预约挂号这条线医院排班、放号、抢号、支付、取号、线下就诊、开具检查检验单、患者在线查看报告。每一条主流程背后对应的是一个又一个业务对象的状态机。比如问诊单就有待分诊、待接诊、问诊中、待开方、待支付、已完成、已超时关闭、已取消这些状态。订单也有待支付、已支付、已出号、已退款、已关闭。这些状态之间的流转规则如果前期不梳理清楚后面代码就是一堆又臭又长的if-else改一个需求崩一片。1.2 四个核心端与系统边界从使用角色上拆分这套系统至少要覆盖四个端患者端小程序或App负责注册登录、实名认证、在线问诊、预约挂号、报告查询、药品订单、在线支付。医护工作台医生接诊、书写病历、开具处方、查看患者历史诊疗记录。药师审方台处方审核、合理用药校验、退方与改方。运营管理端科室排班、号源规则配置、医生资质管理、数据统计。另外还有一堆外部系统要对接院内HIS系统、医保结算平台、微信支付宝支付、短信服务商、物流平台。这里最需要明确的是互联网医院系统和HIS的边界。HIS管的是院内诊疗闭环比如挂号收费、医嘱、检验检查、电子病历而互联网医院是院外服务延伸核心价值在于“连接”连接医患、连接院内外数据、连接药品供应链。边界划不清楚后面接口设计会让你痛苦到怀疑人生。2. Java技术选型与架构落地这套系统最终是怎么搭出来的2.1 从单体到微服务的演进逻辑这套系统我接手时已经跑了两年单体应用所有功能都在一个Spring Boot工程里高峰期最难受的是一处慢SQL能把整个服务拖死一处Full GC能让所有接口集体超时。后来用户量上来业务也想做更多扩展我们才逐步拆成微服务。如果你现在从零开始做类似系统我的建议是初期不要一上来就搞十几个微服务先做模块化单体会更稳妥。等到明确了业务边界、团队也扩大到能维护多个服务的时候再拆。我们最终采用的是Spring Boot 2.7 Spring Cloud Alibaba这套组合Nacos注册中心加配置中心一体省一套运维组件。Spring Cloud Gateway统一入口做路由、鉴权、跨域、限流。OpenFeign服务间调用。Sentinel接口限流与熔断放号高峰就靠它挡掉一部分非核心流量。Seata预留的分布式事务方案但实际核心链路没用它原因后面细说。2.2 核心中间件的分工中间件选型不用花哨稳定和团队熟悉度优先。这套系统里各组件干了各自的活分工明确中间件用途选型理由MySQL订单、问诊单、处方、用户等核心业务数据业务主库独立库加按月分表事务能力强Redis号源扣减、分布式锁、验证码、token、接口限流原子操作与高并发读写性能好RocketMQ支付结果通知、异步写审计日志、短信与站内信支持事务消息消息轨迹清晰Elasticsearch问诊记录、患者病案全文检索复杂检索条件组合查询速度快MinIO处方图片、检查报告等文件存储私有化部署医疗数据不出院区这里有个原则大文件绝不进MySQL热点数据绝不打DB主库。处方单照片、检验报告动辄几MB存MySQL不仅浪费空间还会把缓冲池撑爆。普通业务数据也尽量先读RedisDB扛底层最终一致性。2.3 为什么关键服务要拆开独立部署问诊、订单支付、处方这几个核心链路我们拆成了独立服务。原因是它们的负载特征完全不同订单支付在早高峰会突然飙高而问诊服务全天的负载相对平稳处方服务只有在医生集中开方时段有写入压力。独立部署之后各自可以做独立的限流阈值和扩容策略不会再出现一个服务的Full GC把其他无关键业务也拖垮的情况。还有一个容易被忽略的点文件上传服务必须独立拆出去。如果不拆几个大文件上传请求就能占满Tomcat线程池导致正常业务接口全部等待。这是我们在一次线上OOM事故里用血换来的教训第四部分会详细展开。3. 号源扣减与分布式事务这套系统里最容易翻车的两个战场3.1 号源扣减Redis加Lua挡峰值数据库唯一索引兜底挂号抢号是典型的秒杀场景。每天8点放号一个热门科室的几十个号源瞬间会被抢光。这里最核心的要求是不能超卖患者抢到1个号数据库里就必须恰好扣掉1个号。我们第一版实现直接用数据库行锁“SELECT ... FOR UPDATE”上线第一周就出问题。高并发下DB连接被占满主库CPU飙升慢查询把整个HIS数据库都拖下水。后来改成Redis预扣减DB做最终校验。Redis扣减必须用Lua脚本保证原子性不能先get再decrby否则并发下余量会错乱。核心脚本如下-- KEYS[1]号源余量keyARGV[1]扣减数量 local remain tonumber(redis.call(GET, KEYS[1]) or 0) if remain 0 then return -1 end if remain tonumber(ARGV[1]) then return -1 end redis.call(DECRBY, KEYS[1], ARGV[1]) return remain - tonumber(ARGV[1])Java侧通过RedisTemplate调用DefaultRedisScriptLong script new DefaultRedisScript(); script.setLocation(new ClassPathResource(decrementStock.lua)); script.setResultType(Long.class); Long result redisTemplate.execute(script, Arrays.asList(stockKey), 1L); if (result ! null result 0) { // 扣减成功执行下单逻辑 } else { // 余量不足直接返回“号源已被抢完” }这套方案上线后又踩了一个隐蔽的坑Redis计数器报错“ERR value is not an integer or out of range”。排查了半天发现是RedisTemplate的序列化配置问题。默认的JdkSerializationRedisSerializer会把数字序列化成二进制Lua脚本里拿到的是乱码串自然做不了运算。解决办法很粗暴操作计数器的key和value统一用StringRedisSerializer简单场景直接用StringRedisTemplate。但Redis扣减只是挡了第一层流量数据库层面还必须兜底。我们在号源表上建了“排班ID号序”的唯一索引同一个号源最多只能插入一条订单记录。万一Redis扣减和DB落库不一致唯一索引会挡住重复数据后面用对账任务做修正。3.2 挂号到支付出号的分布式事务我为什么没上Seata TCC创建挂号订单、微信支付、支付回调后出号这条链路跨了订单服务和号源服务天然是个分布式事务问题。一开始团队讨论过Seata的AT和TCC模式最终我拍板不用原因有三个第一链路长补偿逻辑太复杂。支付环节调用外部微信接口如果微信支付成功但回调没送到TCC的Cancel根本不知道该不该退钱。第二全局锁会拖垮体验。AT模式的全局锁在高峰放号场景下会锁住很多订单资源并发一高反而变成瓶颈。第三业务上能接受短暂状态不一致。给用户的感觉是“订单已提交正在处理中”而不是强一致要求支付后立刻出号。最终方案是本地消息表加RocketMQ保证最终一致订单服务在本地一个数据库事务里同时插入订单记录和一条消息记录两张表的写入要么都成功要么都失败。定时任务把状态为“待投递”的消息发到RocketMQ发送成功后更新消息状态为“已投递”。出号服务消费消息后先查幂等表如果事件已经处理过就直接返回否则占用号源并写幂等记录。消费失败的消息进入重试队列多次重试仍失败进入死信队列由人工处理。订单超时未支付时定时关单并释放Redis号源余量。这个方案牺牲了实现上的优雅换来了稳定性。运行一年多线上出过几次消息积压但从来没有出现过钱付了号没有、或者号占了钱没付的资损问题。3.3 支付回调和MQ消费的幂等用唯一约束挡住重复通知支付回调天然会重复推送MQ也默认是至少一次消费语义所以幂等是硬性要求。我们的做法很传统幂等表加唯一索引。支付回调处理时先以“订单号支付流水号”为唯一键插入回调处理记录插入成功才执行后续业务逻辑插入失败说明已经处理过直接跳过。MQ消费端也是同一个套路每个业务的幂等表单独建以业务事件ID为唯一键。经验是幂等判断必须和业务操作放在同一个本地事务里否则检查完还没执行就宕机恢复后重复执行一样出问题。4. 一次线上OOM的完整排查链路从告警到根因4.1 症状处方图片上传一多服务就批量重启这套系统上线几个月后某天突然收到大量用户反馈“上传处方照片失败”紧接着告警群里开始疯狂弹消息文件服务、问诊服务连续出现“OutOfMemoryError: insufficient memory”部分容器已经自动重启。从监控看JVM堆内存像坐过山车一样直线拉高Full GC之后只短暂降一点然后继续爬升直到堆完全无法分配进程崩溃。当时我第一反应是“是不是内存泄漏”但直觉又觉得不像因为服务重启后能恢复一段时间更像是短时间内被大对象打爆了。4.2 排查过程日志、堆转储、GC日志三线并进当时的排查链路大致是这么走的先打开GC日志确认老年代占用飙高Full GC频繁回收后内存无法回落。然后在服务参数里加-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/logs/dump等下次崩溃时拿堆转储文件。用MAT打开dump文件排序后看到byte[]数组占了堆内存的70%以上并且大量byte数组的长度在2MB到20MB之间。按引用链追踪到出处定位到处方图片上传接口。源码一翻问题立刻清楚了// 这段代码就是事故源头 byte[] fileBytes inputStream.readAllBytes(); String base64 Base64.getEncoder().encodeToString(fileBytes); // 然后base64字符串写入数据库字段一个10MB的处方照片readAllBytes一次申请10MB转成Base64后内存占用又膨胀约33%。如果并发二三十个人上传堆内存瞬间就是几百MB级的消耗再加上SpringMVC解析请求时对MultipartFile的缓冲堆不炸才怪。4.3 根因修复文件入库改成对象存储上传改为流式加限流修复方案分了四步落地第一文件全部改为直传MinIO。前端先从后端拿一个临时上传凭证然后直接PUT文件到MinIO数据库只存文件的bucket名和对象key不再存二进制数据和Base64字符串。第二上传接口改成流式处理不整读进内存。后端限制单文件大小最大5MB超过直接拒绝前端在上传前也做压缩处理。第三生成缩略图。处方图片预览场景很多原图10MB预览太浪费上传成功后用服务端工具生成一份宽边不超过1080px的缩略图列表预览用缩略图点击查看原图时再走对象存储的临时签名URL按需加载。第四上传逻辑独立部署并做线程池隔离。文件上传服务单独拆出来部署Tomcat最大线程数单独设置再配合Sentinel限流峰值流量下最多丢掉部分超大文件不会拖垮核心业务服务。另外一个容易踩的坑是JVM参数只调大堆内存。容器内存4G我们把-Xmx从2G调到3G治标不治本大对象一多照样崩。正确思路是控住大对象的数量和生命周期配合合理的堆设置java -jar app.jar \ -Xms2g -Xmx2g \ -XX:MaxMetaspaceSize512m \ -XX:UseG1GC \ -XX:HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath/data/logs/dump5. 医疗数据的隐私底线脱敏、权限与防篡改5.1 患者数据分级与动态脱敏医疗数据对隐私的要求比普通业务系统高一个量级。我们内部把数据分了四个级别不同级别采用不同的处理和展示策略数据级别典型字段默认展示规则公开信息科室名称、医生职称不脱敏正常展示内部信息医生排班、预约时间仅系统内部可见敏感信息患者姓名、手机号、身份证号默认脱敏授权后可查看明文极敏感信息完整病历、诊断结论、检验报告独立权限申请全链路审计脱敏在实现上不是让每个业务接口自己判断而是通过一个自定义注解加切面统一处理。例如在DTO字段上标注SensitiveMask(type SensitiveType.MOBILE)接口返回时切面自动把手机号处理成“138****1234”。这样业务代码不感知脱敏逻辑新来的同事也不容易漏做导致数据泄露。5.2 权限模型RBAC加数据范围医生不能看全院的病人权限这块我们用的是Spring Security加OAuth2加JWT的组合。JWT有效期设得比较短一般两小时配合Redis做的刷新令牌既保证无状态扩展又能主动吊销有问题的token。纯RBAC模型解决不了“数据范围”的问题。系统里最典型的场景是一个医生只能查看自己接诊的患者科室主任可以查看本科室的患者运营人员看到的是脱敏后的统计数据而超级管理员才有权限查看全量明细。这个在实现上是通过数据权限切面实现根据当前登录用户的角色自动往查询SQL里拼接过滤条件。比如医生查询问诊列表时自动带上“doctor_id 当前登录用户ID”的条件。这个逻辑不放在业务层手工写而是封装在MyBatis的拦截器里统一注入避免有人漏写导致越权。5.3 电子处方防篡改与审计链路电子处方在业务上是具有凭证性质的文书必须防篡改。我们的做法是对处方的关键字段计算哈希值存到独立的校验字段里。读取处方时重新计算哈希不一致就说明数据被改动过系统直接告警并锁定该处方。具体的实现不复杂就是在生成处方时把患者ID、医生ID、药品明细、用法用量、开方时间拼接成固定格式字符串然后做SHA-256把摘要值同时存库。任何字段被改动摘要都对不上。审计链路我们同样没有省开方、审方、改方、退费、导出病例这些关键操作统一通过切面异步记录操作者、操作时间、操作内容、IP和终端指纹消息发到RocketMQ后异步落到审计库。审计日志独立存储应用服务挂了不影响审计写入。6. 开发中反复踩到的Java小坑在医疗系统里都会被放大6.1 Bean属性命名导致JSON字段“变样”的联调事故有一次联调电子就诊卡接口前端拿到的字段名是“eCardNo”而我们的实体类字段写的是private String ECardNo;文档里约定的却是“cardNo”。排查了半天问题出在JavaBean的getter/setter命名规范上。ECardNo这个字段IDEA和Lombok生成的getter是getECardNo()但有些JSON序列化框架会把首字母连续大写的属性名处理成小写开头甚至出现截断。前端按文档取cardNo怎么都拿不到值。教训很直接Java实体字段命名统一用驼峰禁止首字母大写。如果某些外部接口约定必须用特定字段名就在字段上加JsonProperty显式指定不要把命名规则交给序列化框架去猜。6.2 Lombok与编译器版本不匹配的诡异报错团队有人升级JDK后启动项目一直报“you arent using a compiler supported by lombok, so lombok will not work”。排查发现是新版JDK的编译器和当前Lombok版本不兼容。解决办法是升级Lombok版本同时在maven-compiler-plugin里显式配置annotation processor路径让Lombok的注解处理器在编译阶段稳定生效。加上这段配置之后这个报错基本绝迹plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId configuration annotationProcessorPaths path groupIdorg.projectlombok/groupId artifactIdlombok/artifactId version${lombok.version}/version /path /annotationProcessorPaths /configuration /plugin6.3 JAVA_HOME与PATH里JDK不一致启动时最容易被忽略系统里装了好几个JDK版本命令行敲java -version显示的是8echo $JAVA_HOME也指向8但启动脚本里用的其实是另一个路径下的JDK17导致编译和运行环境不一致出现Class版本不兼容的诡异问题。排查方法很简单也容易忽略启动脚本里先检查实际执行的是哪个java把路径打出来。配环境变量这件事建议统一在一个地方管理不要每台机器手改。我们后来在启动脚本里固定写了export JAVA_HOME/usr/local/jdk1.8 export PATH$JAVA_HOME/bin:$PATH这样无论机器上默认JDK是什么版本应用启动用的都是指定版本不会再出现环境不一致引发的“灵异事件”。这六年做医疗Java项目的过程中经历过放号高峰的胆战心惊也经历过OOM事故后的大规模代码重构。Java功底决定你做的系统能跑多稳业务理解决定你的系统有没有人真正在用这两个都躲不掉。如果你也在做同类系统希望这篇文章能帮你少踩几个坑。本文还有配套的精品资源点击获取
返回列表