
简介面向医疗HIS系统开发者的Caché数据库专题资料包定位于帮助后端工程师、系统架构师及医疗信息化从业者掌握InterSystems Caché的核心开发技能。内容围绕高性能多维数据模型、高可用集群配置、安全权限管理与Web服务集成展开针对性解决医疗场景下海量实时数据读写、病历数据结构建模等典型难题。压缩包共145个文件以143份PDF为主另含1份HTML索引页和1张示意图整体约77MB。PDF文档覆盖环境安装配置、数据模型设计、MUMPS语言编程手册、API参考、高可用与容灾策略、安全管理、实例代码及报表分析工具说明目录整理明确可按需查阅。目前已有1031人学习下载。对于正在建设或优化HIS系统的团队而言这套资料既能作为新手入门教程也可充当日常开发中的查询手册尤其适合在系统集成、性能调优与数据安全保障等环节提供参考。1. 医疗HIS为什么绕不开Caché从一个夜班抢救场景说起某医院急诊的夜班抢救系统挂号、开医嘱、计费几乎同时压到同一个病人档案上原来的关系型数据库行锁一加界面直接超时。切换到Caché之后同样的业务逻辑跑在global多维存储上高峰时段没出过一次死锁日志。这个标题里的Caché是医疗HIS领域用了二十多年的经典数据库底座它不用二维表做主存储而是以global多维数组和ObjectScript语言为核心把病人档案、医嘱、计费挂在可动态伸缩的树状节点上。本文要讲的是拿到一套Caché开发文档资料你该先读哪几个核心概念、怎么写第一行ObjectScript、把挂号医嘱费用这些HIS主干业务落到global上以及并发、中文、归档这类最容易翻车的坑怎么提前踩掉。适合正要接手Caché版HIS的开发者、做二次开发的团队以及准备把老系统往新架构迁移的运维同学。2. Caché的核心存储逻辑global多维数组与ObjectScript是怎样支撑HIS业务表的2.1 global是什么为什么HIS要把病人数据装在脱离二维表的存储里打开Caché的终端输入下面两行你会第一次看到HIS数据的真实模样USERSET ^PAT(1001)张三|男|1980-01-01|血型O USERSET ^PAT(1001,VISIT,20240401)发热门诊|李医生|已收费|体温38.5 USERZW ^PAT ^PAT(1001)张三|男|1980-01-01|血型O ^PAT(1001,VISIT,20240401)发热门诊|李医生|已收费|体温38.5^PAT就是一个global。global的名字以尖括号标识实际语法是^PAT后面跟着括号里的下标序列值可以是字符串、数字甚至空节点。它跟关系表最大的区别是关系表要先设计字段再插数据global则完全相反——你先有数据再决定把它挂到哪个下标下面。这个特性对HIS太重要了病人主索引需要扩展过敏史、医保卡号、随访标签时不用ALTER TABLE重建表结构直接在^PAT(patientId)下面新增叶子节点就行。开发文档资料里关于global的章节通常都会用一个图来解释“稀疏多维数组”键可以不连续、值可以任意类型、叶子节点数量可以自由增长这就是医疗档案天然适合的数据形态。我在实际项目里看到的HIS表十有八九能在global上找到原型的影子。比如病人表主索引挂就诊记录药品目录表按ATC编码挂库存节点收费记录表按日期挂当天的流水。字段变更频繁的模块用global直接改需要复杂统计的模块再在类定义上做关系映射。这样做的另一个好处是性能HIS的报表查询经常走全表扫描但Caché的global是稀疏索引按主下标定位一个病人的所有数据往往只要读几次磁盘。所以开发文档里的第一优先阅读章节一定是Globals和Multidimensional Storage不读透这两个后面写业务代码就是凭空猜。2.2 ObjectScript简写HIS后台任务的母语Caché上的业务逻辑最常见的写法是ObjectScript。ClassMethod是它组织代码的基本单位一个挂号的简单方法长这样ClassMethod RegisterPatient(patId As %String, name As %String) As %Status { // 把病人基础信息写入global并带上当前时间 Set ^PAT(patId) name _ | _ $ZDATE($H, 3) Quit $$$OK }这段代码把病人ID作为下标、姓名和日期拼成一个值存进^PAT。$H是Caché的系统时间戳$ZDATE($H,3)把它格式化成YYYY-MM-DD_是字符串连接符。Set是赋值指令$$$OK是系统状态宏表示方法处理成功。在实际HIS里这类方法不会直接暴露给请求端而是被封装成类方法页面端通过SQL存储过程或REST接口去调。开发文档里的ObjectScript命令清单Set、Quit、Do、For、While是出现频率最高的几个。记住一条经验凡是涉及global直接读写的代码用ObjectScript凡是涉及结果集展示的代码用SQL。2.3 Caché与关系型数据库的双向映射SQL只是访问层Caché的体系里表和global不是二选一而是同一份数据的两种视图。你在Studio里定义了一个持久化类Caché会把它投影成一张关系表这张表在SQL里能查但底层存储还是global。也就是说写SQL只是换了种语言在访问global。报表、ETL、BI工具用到SQL就够了指令里的%STARTSWITH是Caché对标准SQL的扩展专门走索引做前缀匹配SELECT Name, DOB, Sex FROM SQLUser.PAT WHERE Name %STARTSWITH 张 AND DOB 1990-01-01%STARTSWITH对应ObjectScript里$EXTRACT的加速版本它要求Name上有索引才能真正发挥性能。开发文档里的SQL Reference会列出这一批扩展操作符%STARTSWITH、%INLIST、%CONTAINS、%PATTERN它们都能利用位图索引和global的稀疏结构加速查询。看不懂这些操作符的开发者容易用标准SQL写一堆LIKE嵌套在数据量到百万级之后性能下滑翻车现场通常就在这里。这一章讲清楚了一个基本判断Caché不是要把关系数据库替换掉而是把物理存储层换成更接近医疗数据访问模式的global。开发时先想清楚数据怎么挂、怎么读再决定用ObjectScript还是SQL。文档资料里关于Mapping和Projection的部分就是在讲这个对应关系值得在动手写代码前完整过一遍。另外刚开始接触Caché的人会在文档里看到“例程Routine”和“类Class”两种代码组织方式。老一代HIS大量用MAC例程新项目基本是类Studio里新建代码文件时选ClassMethod还是Routine会影响后续维护。建议新代码一律写类类可以映射成表、可被Xdata挂REST服务、还能被对象方式调用旧例程能跑就别动等到要改功能时再顺手迁移到类方法。3. 本地搭一套Caché开发环境从启动实例到跑通第一个global3.1 开发环境的三种来源与选型原则想上手Caché第一步不是写代码而是先搞到一个能练手的实例。常见做法有三种一是找公司信息科或DBA要一台开发测试服务器申请一个独立命名空间这种方式离生产数据最近适合做实际模块改造二是在自己电脑上装厂商发布的最新开发者版装完就是一个干净的学习环境适合练语法和建表三是直接连接项目组已有的开发库直接在存量数据上验证自己的global设计但风险是误写污染数据。选型原则很简单练手用本地单机版调试业务用开发服务器永远别拿生产实例当实验田。Caché的实例本身支持多命名空间一个实例里可以同时存在HS、DEMO、TEST等多个命名空间每个命名空间的数据、代码、配置互相隔离。开发文档里的Configuration和Namespace章节会把实例和命名空间的关系拆得很清楚你把命名空间当成独立的数据库来理解就行。3.2 连接到管理门户与第一行终端命令Caché默认带一个Web管理门户浏览器打开本机或服务器的管理端口就能看到登录页常见的端口配置是52773或57772具体以安装时的设置为准。登录之后能看到System Explorer、System Operation、System Administration几个大分类开发阶段最常用的是System Explorer里的Globals和SQL。先打开终端Terminal切到用户命名空间# 在安装目录下启动会话不同发布版前缀略有差异 cd /usr/cachesys/ sudo ./csession CACHE %SYS进入后默认停在%SYS系统命名空间这里只管系统配置不适合放业务数据。切换到用户命名空间用ZN命令%SYSZN USER USER提示符变成USER就表示已经切进用户命名空间。此时你可以执行第一行global写入命令USERSET ^APP.DEMO(HELLO)Caché ok USERWRITE ^APP.DEMO(HELLO) Caché ok这段操作的逻辑SET往global节点写值WRITE把节点值读出来显示。命令本身不复杂但它验证了整个链条——网络连接、实例认证、命名空间权限、global读写都正常。大多数环境问题在这一步就能暴露连不上多半是端口或认证配置连上了写不进去多半是命名空间映射到了只读库。3.3 在Studio里创建你的第一个命名空间终端只适合验证真正开发都在Studio里。打开Studio连接同一个实例文件类型里选择新建一个类Class名字按项目规范填比如APP.DEMO.Patient。Studio会自动打开一个类的编辑模板里面已经有Class、Property、Method的骨架。建议第一次动手时把之前终端里的global读取逻辑写成一个类方法顺便验证Studio和实例的编译链路Class APP.DEMO.Patient Extends %RegisteredObject { ClassMethod DumpDemo() As %String { // 读取之前终端写入的global节点 Set value $GET(^APP.DEMO(HELLO)) Quit value _ value } }保存后点Compile再在终端里调用USERWRITE ##class(APP.DEMO.Patient).DumpDemo() valueCaché ok这里的关键不是业务逻辑而是让你走通“Studio写代码→编译→终端调用”的完整开发闭环。$GET函数比直接取global多一层保护节点不存在时返回空字符串而不是报错这是HIS代码里必须养成的习惯因为很多病人档案节点的叶子不存在是常态不是异常。3.4 建一个自己的global并导出备份开发过程中会建一堆global全部手工在终端写不现实。真正的做法是在Studio里用Globals编辑器或直接在终端用循环生成。比如模拟生成100个病人挂载点USERFOR i 1:1:100 { SET ^APP.DEMO(PAT, i) demo-patient- _ i }FOR循环是ObjectScript里最简单的批量控制结构起点、步长、终点的写法是起点:增量:终点。建完之后在管理门户里进入Globals页面能看到^APP.DEMO下面挂着一个100节点的树。这一步是为了检验你对“稀疏树”的理解如果你只SET了偶数下标树上就会出现空洞但访问完全不报错。很多关系型数据库出身的人第一次看到这种空洞会慌其实这正是预期行为。global建好之后记得用管理门户导出成.net文本文件路径任意但扩展名建议用.gbl。导出的价值在于后续改动global结构前先导出基线跑挂了一键还原不用靠手工敲命令回滚。开发文档里对应章节在Global Export和Import工业化流程里这两步必须脚本化。4. 用Caché写HIS核心功能挂号、医嘱与费用结算的最小实现4.1 挂号号段与并发安全$INCREMENT才是保命函数挂号是HIS里并发压力最大的动作之一多个窗口同时取号号段绝不能重复。最稳妥的写法是用$INCREMENT函数对全局计数器做原子自增ClassMethod Register(patName As %String, doctor As %String) As %String { // 从全局计数器中取一个不重复的号 Set id $INCREMENT(^APP.SEQ(PAT)) // 开启事务保证挂号信息要么全部写入要么全部不写 TSTART Set ^APP.DATA(PAT, id, NAME) patName Set ^APP.DATA(PAT, id, DOCTOR) doctor Set ^APP.DATA(PAT, id, STATUS) ACTIVE TCOMMIT Quit OK: _ id }$INCREMENT是原子操作多个进程同时调用也不会取到重复号这是HIS互斥号段的基础。这里有个关键选择为什么不用先读再写的方式先读再写是传统的“读-改-写”三步两个进程同时读到连续号就会撞号必须配合锁才安全$INCREMENT一步到位既避免了锁等待也让代码更短。TSTART和TCOMMIT是事务边界把四个SET操作包进同一个事务中途任何一步失败都会整体回滚不会出现有名字没医生、或者有信息没状态的半截数据。4.2 医嘱流转Lock锁的加锁、等待与释放医嘱的状态流转比挂号复杂医生提交、护士执行、药房发药这三个角色可能同时操作同一条医嘱。如果两个进程同时把同一条医嘱改成不同状态最后写入的一方会静默覆盖另一方。解决方式是给医嘱节点加锁// 尝试加锁最多等待5秒 Lock ^APP.DATA(ORD, ordId):5 If $Test { // $Test0表示5秒内没拿到锁直接放弃操作 Quit BUSY } // 拿到锁之后再做状态更新 Set ^APP.DATA(ORD, ordId, STATUS) EXECUTED // 释放锁动作要快锁持有越久并发越差 Lock -^APP.DATA(ORD, ordId)Lock命令从开始、-结束带:5表示等锁超时时间单位是秒。$Test变量是加锁的结果标志$Test为1表示拿到锁0表示超时。医嘱状态更新对实时性要求高等锁超过5秒基本可以判定另一个终端卡住了这时候返回BUSY让用户重试比无限等下去好得多。锁的粒度也有讲究锁curd细到单条医嘱节点并发高锁到整个病人档案就太粗会拖慢同病区的所有操作。实际项目里医嘱锁通常锁到条码单号这个维度挂单、改单、退单一律经过同一把锁。4.3 费用结算SQL类查询与ObjectScript的分工费用结算模块既有查询需求又有强一致性的写需求。写报表、看当日流水、按科室汇总用SQL最划算而扣费、退费和记账必须走事务加锁。开发文档里推荐的融合方案是把SQL查询封装成类查询再由ObjectScript在事务里调用Set tStmt ##class(%SQL.Statement).%New() // 假设SQLUser.PAT下已有按病人查费用的类查询GetBill Do tStmt.%PrepareClassQuery(SQLUser.PAT, GetBill) Set tRes tStmt.%Execute(patId) // 遍历结果集累加当日费用 Set total 0 While tRes.%Next() { Set total total tRes.%Get(AMOUNT) } Write 当日费用合计:, total%SQL.Statement是Caché面向对象的SQL调用入口%PrepareClassQuery把类查询预处理成可执行对象%Execute传入参数执行返回的结果集用%Next逐行读取。GetBill类查询本质就是一个SELECT语句但通过类定义被绑定在业务对象上SQL负责把大数据量计算下推到存储层ObjectScript负责事务和业务校验。如果纯用SQL做费用更新会遇到事务边界和锁拿不到的问题如果纯用ObjectScript操作global做统计又绕不开逐节点遍历的性能低谷。各取所长是这条方案能长期跑稳的根源。4.4 开发文档里必须读懂的五个技术名词在Caché开发文档里反复出现的几个名词弄懂它们等于拿到了读资料的钥匙名词一句话解释对应开发场景命名空间Namespace独立的代码与数据环境边界开发库、测试库、生产库隔离例程Routine老式的代码组织单位后缀MAC历史遗留HIS模块维护类Class新式代码组织单位可映射成关系表业务方法、REST服务、持久对象持久类Persistent类实例自动存入数据库病人主索引、医嘱记录、结算流水投影Projection把类映射成SQL表或REST接口报表SQL、外部系统对接这五个词里最容易理解错的是“投影”。投影不是数据复制而是同一份数据的不同访问方式类方法改了globalSQL查询立刻能查到新数据因为底层是同一个物理节点。文档资料里单独有一章讲Classes里面专门解释了Persistent类的存储如何与global对应建议细读一遍。读完再回头看本文的挂号代码你会发现方法里写global的每一步其实都在往持久类的存储区域里面塞数据只是绕开了类对象这个中间层。5. Caché在HIS开发里的避坑指南并发覆盖、中文乱码与归档5.1 并发更新把另一台终端的修改覆盖了现象两个护士站同时修改同一条医嘱备注后保存的一方把先保存的内容完全盖掉既无提示也无报错事后查不到是谁干的。原因代码直接Set global节点没有上锁也没走封装方法。$INCREMENT只保证了取号不重复不保护后续的改写在不受干扰。解决所有写操作收敛到类方法内部方法开头对操作节点加锁写完后立即解锁同时把操作人、操作时间、操作内容写进同节点的AUDIT分支出问题时有据可查。如果嫌加锁麻烦至少要保证“读-改-写”全程在一个事务里配合事务级别的隔离保证。5.2 写入的中文在终端里正常报表里全是问号现象管理门户里看global数据中文正常用SQL工具拉出来的数据全是问号或乱码第三方报表对接后无法阅读。原因Caché实例的NLSNational Language Support配置和客户端的字符集不一致。很多老实例默认配置是Latin1或UTF-8但业务库实际写入的是GBK或GB2312SQL客户端连接时又按系统默认字符集解码两头一错位就乱。解决先看实例的NLS配置把默认字符集统一改成UTF-8连接串里显式指定charset参数历史脏数据用转码函数批量清洗一次。这个坑最隐蔽的地方在于ObjectScript终端内部统一用Unicode编码数据显示正常但SQL访问层和外部接口是两条不同的解码路径只要有人绕过统一配置连接乱码就会突然出现。5.3 global命名和系统保留前缀冲突现象新建的global在开发环境正常搬到测试库后某些节点偶尔被系统任务清掉数据凭空消失。原因global的顶层命名没有遵守规范命中了系统维护工具的清理范围。Caché里以%开头的global是系统保留空间以^TMP开头的是进程临时数据以^CacheTemp开头的会随实例重启清理如果业务代码把临时数据放进了要长期保留的global里备份和清理策略会直接误杀。解决业务global一律以专属应用前缀开头比如^APP.、^HIS.、^HOSP.下级再按模块划分形成^HIS.PAT、^HIS.ORD这样的清晰树。写代码前在管理门户里查一下global列表确认这个顶层名字没有被系统任务占用。5.4 事务日志和备份文件把磁盘撑满现象磁盘空间告警实例日志目录下积累了大量journal文件备份目标盘的容量也持续上涨最终实例进入只读保护模式HIS前台全部卡死。原因Journal是Caché的事务日志默认持续记录所有写操作只在正常关闭或备份完成后自动清理备份策略如果只做全量不做增量备份文件日积月累会先于业务数据把磁盘占满。解决把Journal的自动切换和清理周期配到运维规范里每天一次全量备份后执行日志截断备份保留周期按院方存储容量定落后备份文件定期归档到独立存储。这个问题的典型特征是“慢慢涨”从磁盘告警到实例锁死通常有两三天时间但HIS现场常常没有专职DBA等到锁死才发现已经晚了。5.5 开发环境和生产环境的global映射不一致现象同一段代码在测试库跑得好好的发布到生产库后要么找不到数据要么写到了错误的命名空间。原因global映射在开发机器和生产机器上的配置不同。开发时默认映射到USER或HSL命名空间生产上重新做了映射业务global跑到了另一个库文件里而类代码是按逻辑命名空间编译的物理路径一变行为就变。解决发布前用管理门户比对两份环境的Namespace和Global Mapping配置把映射差异整理成变更单更稳妥的做法是部署脚本里固定写清楚映射关系每次发版自动执行一次映射校验而不是靠人肉检查。养成这个习惯之后绝大多数的“测试好、生产挂”都能避免。6. 进阶把Caché开发文档变成你自己的问题追查手册开发文档资料的最终价值不是放在书架里当字典而是变成一张你能快速定位问题的认知地图。我自己的习惯是这样拿到文档后先翻目录把Concept、Language、SQL、System Admin四大部分在脑子里留下位置索引然后每踩一个坑就往对应章节加一条备注。比如global写入报错就去Concepts里查Globals那一章$INCREMENT取号超时去Language里的$INCREMENT函数页SQL报表慢去SQL Reference里看索引和查询计划分析器。排查HIS线上问题时我一般会按三层来查第一层看进程管理页面确认是否有长事务卡住了锁第二层用管理门户的SQL语句分析工具看慢查询是不是没走索引第三层直接写一段ObjectScript遍历可疑的global节点把数据状态拉出来核对。Caché有个方便的地方是几乎不用重启改了类代码编译生效改了global映射立即套用清了死锁进程自动释放。这给排查争取了很大空间但也意味着运维动作可以很激进改之前务必留好导出备份。另外养成随时随地导出的习惯。动病房数据前导出涉及global改类方法前导出项目XML升版本前导出整个命名空间。Caché的导出文件是纯文本丢进代码仓库做diff非常方便比记忆粘贴靠谱。等到真正出事的时候你能向前滚动看历史版本绝大多数问题都能定位在最近一次改动里。希望你接下来的Caché开发比我的翻车次数少一点如果真遇到了希望这篇笔记里的方案能帮你早一点收工。本文还有配套的精品资源点击获取