更新记录(仅首版,2026年07月29日 发布)
- 首次发布
引用本文(DOI(已登记存档): 10.5281/zenodo.22175286)
以下 DOI 指向先前登记的存档,内容可能与当前正文不同。引用当前正文时,请使用本页网址。
Go Komura(2026)。《MSMQ 还能用多久——“连弃用都算不上”的遗留队列迁移判断》。小村软件有限公司。 https://comcomponent.com/zh-CN/blog/msmq-migration-decision-guide/
- DOI(已登记存档)
- 10.5281/zenodo.22175286
- DOI(上次登记版本)
- 10.5281/zenodo.22175287
“听说 MSMQ 已经被废止了。现在还在运行的系统,是不是也必须马上迁移?”——在既有系统的维护中,首先需要把这一点区分清楚。
MSMQ(Microsoft Message Queuing)作为操作系统功能是否存续,与从 .NET 使用它的 API 是否被支持,是两回事。在本文的基准时点 2026 年 7 月,MSMQ 并未列入官方的弃用一览,仍作为 Windows 的可选功能保留着。另一方面,标准库 System.Messaging 是 .NET Framework 专用的,并未移植到 .NET(Core 及以后)。123
也就是说,问题不在于“明天 MSMQ 就会停掉”,而在于“把应用迁到 .NET 时,队列的实现会成为迁移的障碍”。
本文面向负责维护和迁移 MSMQ 系统的开发者,以及决定方针的信息系统部门。先确认事实关系与继续使用的条件,然后进入盘点、迁移目标选型、表队列的实现和切换步骤。
从 VB6 或 .NET Framework 迁移的整体做法,请参考《VB6 到 .NET 的迁移实务》《.NET Framework→.NET 迁移前检查清单》。本文只聚焦队列这一部分。
1. 先说结论:把操作系统的存续与应用的迁移分开看
如果要把应用迁到 .NET,原则上队列也要一起迁移。如果短期内仍以 .NET Framework 运维,那么在确认操作系统的支持期限与运维条件之后,可以选择继续使用。新项目不建议采用 MSMQ。4
从你现在要决定的事情读起
| 现在要决定的事 | 判断的出发点 | 说明 |
|---|---|---|
| “已被废止”的说法是真的吗 | 把操作系统功能与托管 API 的状况分开 | 第 1 章:事实关系 |
| 短期内能否就这样继续用 | 除操作系统的支持期限外,还要确认能否配齐监控、恢复与交接 | 第 2 章:继续使用的条件 |
| 想随 .NET 迁移一起替换掉 | 盘点依赖、消息格式、DTC 与离线耐受性 | 第 3 章:盘点、第 4 章:迁移目标 |
| 能不能用 P/Invoke 或 CoreWCF 保留下来 | 把“能调用得通”和“扛得住维护负担”分开看 | 1.3 节:托管 API 与 P/Invoke、1.4 节:CoreWCF |
| 选 RabbitMQ 还是 Azure Service Bus | 在这二选一之前,先考虑同一个业务数据库上的表队列够不够用 | 第 4 章:按需求选型、第 5 章:表队列 |
| 想把取出用的 SQL 搬到自己的数据库上 | 确认隔离级别、快照设置和锁提示 | 5.3 节:SQL 与数据库设置 |
| 想保证队列的处理顺序 | 把取出顺序与写入业务数据的顺序分开看 | 5.4 节:并行处理与顺序 |
| 想把工作进程投入实际运维 | 确认事务边界,以及回收、重试与清理 | 5.5 节:工作进程、5.6 节:运维 |
| 运行中的系统该怎么切换 | 确定消息格式、接收端、去重措施和残留消息的处理 | 第 6 章:切换步骤 |
如果要通读全文,顺序是:确认存续 → 继续使用的条件 → 盘点 → 迁移目标 → 实现 → 切换。回顾判断时,请参考第 7 章的总结。
1.1. 先确认在谈哪一层
“MSMQ 还能不能用”的答案,按层次不同而不同。状态的基准时点与开头相同,都是 2026 年 7 月。
| 层次 | 当前状态 | 实务上的意义 |
|---|---|---|
| 操作系统功能(Windows 的可选功能) | 存续。同捆在现行的 Windows 客户端/Server 中,也没有发布弃用声明12 | 启用后现在依然能运行。在操作系统的支持期限内可以继续使用 |
Win32 原生 API(MQSendMessage 等) |
有文档,可以使用5 | 从 .NET 用 P/Invoke 调用的路子还在。但格式化器与事务协作要自己写、自己维护 |
| .NET Framework 的 System.Messaging | 可以使用。但覆盖范围只到 .NET Framework 1.1~4.8.13 | 既有系统就跑在这里。再往前没有后续 |
| .NET(Core 及以后)的官方托管 API | 不存在。Windows 兼容性包中也不包含6 | 把应用升到 .NET 的那一刻,队列部分就必须重做 |
| WCF 的 MSMQ 绑定 → CoreWCF.MSMQ | 存在社区主导的移植。CoreWCF 本身有支持策略,但 MSMQ 实现依赖 System.Messaging 的社区移植7 | 可用于延续经由队列被调用的 WCF 服务(接收端)的寿命,但既不是发送端的替代,也不是通用的队列 API |
| 新项目采用 | 不建议 | 因为没有官方的未来路径 |
1.2. 没有被弃用的是操作系统功能
Windows 客户端的“Deprecated features”里列着 NTLM、VBScript、WordPad 等,但没有 MSMQ 这一项。Windows Server 的“Features Removed or No Longer Developed”也一样,包括 Windows Server 2025 的标签页在内都没有。12
弃用(deprecated)是一个官方阶段,表示已结束积极开发、将来有可能被删除,它与删除(removed)不同。就 MSMQ 而言,连这个弃用声明都还没有发布——这就是本文所确认到的状况。
1.3. 妨碍向 .NET 迁移的是托管 API
System.Messaging.MessageQueue 的参考文档覆盖的是 .NET Framework 1.1~4.8.1,没有面向 .NET(Core 及以后)的版本。Windows 兼容性包(Microsoft.Windows.Compatibility)提供注册表、WMI、Windows 服务、EventLog 等约两万个 API,但不包含 System.Messaging。36
要用 P/Invoke 保留,就得连包装层的维护一起扛下来
并不是连原生 API 也没有了。MQSendMessage、MQReceiveMessage 等 Win32 API,从 .NET 用 P/Invoke 调用本身是可行的。但原先由 System.Messaging 承担的格式化器与事务协作,都要自己写成包装层并持续维护。这不是迁移的正选,而是无论如何都要保留 MSMQ 时的续命手段。5
1.4. 把 CoreWCF 当作“WCF 接收端”的路径来判断
.NET Framework 的 WCF 有使用 MSMQ 的绑定。把它放到 .NET 上托管的路径,就是 CoreWCF 的 MSMQ 传输(CoreWCF.MSMQ)。7
但它的用途是经由队列被调用的 WCF 服务的接收端。它既不是能替代 System.Messaging 的通用队列 API,也不是发送端客户端的替代品。
CoreWCF 本身有 Microsoft 的支持策略。另一方面,MSMQ 传输的实现依赖 .NET Framework 版 System.Messaging 的社区移植,不能认为这部分依赖也享有同样的保证。采用前要确认所用版本是否属于支持对象、依赖部分由谁来维护。7
由此可见,“MSMQ 已被废止”和“不能原样迁到 .NET”不是一个意思。选择 P/Invoke 或 CoreWCF,等于把队列的替换往后推,代价是扛下这条路径的维护负担。
图中实线表示始终成立的关系,虚线表示带条件的关系(成立条件写在详情页各关系的说明中)。关系的完整列表(共 28 条,附依据与确信度)以及主要概念的定义,汇总在知识地图详情页(日文)。数据:JSON-LD / Turtle
2. 是否继续使用:由 .NET 迁移计划和运维体制来决定
2.1. 为什么仅凭“还在运行”无法做出判断
.NET Framework 4.8 作为 Windows 的组件,按所安装的操作系统的生命周期获得支持。因此,在支持期限内继续运行的计划是可以做出来的。4
要确认的不只是能不能运行。如果因为依赖 MSMQ 而把整个应用留在 .NET Framework 上,下面这些维护与迁移上的负担也会一并留下。
| 负担 | 对迁移判断的影响 |
|---|---|
| 维护人员的确保与交接 | 能讲清 System.Messaging 与 DTC 的技术人员越来越少,重新调查配置的成本上升 |
| 运行时与库的限制 | 有些新的 C# 语法只要更新编译器就能用,但运行时的性能改进和新的标准库用不上。把 .NET Framework 排除在支持范围之外的包也在增多 |
| 对 DTC 的依赖 | 如果保留“把队列取出与数据库更新一起确定”的部分,就算周边都 .NET 化了,最难的一关也会留到最后 |
| 旧的序列化格式 | BinaryFormatter 的实现已在 .NET 9 中从运行时删除,迁移时必须连格式一起处理8 |
维护人员流失的问题,在《没有源代码也没有文档的系统的维护》中也有讨论。MSMQ 的咨询之所以多半不是从故障处理而是从迁移工时估算开始,正是因为真正的问题不在于能不能运行,而在于这类依赖与交接。
“操作系统还在支持期内就能运行”和“越等越容易迁移”是两回事。即使短期内不迁移,也要先把难点盘点出来。
2.2. 短期内不迁移,就要满足 4 个最低条件
没有把应用升到 .NET 的计划,从投入产出比看短期内也不打算改动——这种所谓的“搁置”在有条件的前提下是合理的。但前提是下面 4 项全部满足。
| 检查 | 最低条件 | 具体要做的事 | 不满足会发生什么 |
|---|---|---|---|
| □ | 在装机与恢复步骤中写明 MSMQ | MSMQ 是 Windows 的可选功能。把“启用或关闭 Windows 功能”或用 DISM/PowerShell 启用的步骤写进环境搭建手册 | 更换 PC 与服务器时忘了启用,迁移当天原因不明地跑不起来 |
| □ | 监控队列长度 | 对积压条数设置阈值监控与告警。无法送达队列和日志队列也要纳入对象 | 接收端停了也不会报错,消息只会一直堆积,业务在无人察觉中停摆 |
| □ | 把配置写成文档 | 记录队列路径、权限、是否为事务队列、格式化器、日志队列设置(3.2 节的盘点表可以直接拿来用) | 将来的迁移估算又要从调查开始重来,精度和工时都会变差 |
| □ | 每年复查一次判断 | 每年确认“是否被列入弃用清单”“操作系统更新后行为有没有变化” | 等弃用公告出来才手忙脚乱地行动 |
其中年度复查,是为了不漏掉弃用公告和操作系统更新影响的步骤。为应对人员调动,不只留下配置,还要留下恢复步骤。这些条件没有补齐就维持现状,风险是将来既发现不了系统已经停了,也查不出原因。
3. 盘点:记录下你把什么交给了 MSMQ
3.1. 掌握功能与术语
在 MSMQ 中,发送端把消息写入队列,同一台机器或另一台机器上的应用在自己方便的时机取出。需要迁移目标接手的性质主要有下面 3 个。
接收方停了也能先存下来:store-and-forward
即使接收方停机,消息也会先在本地累积,等恢复后再送达。这在不稳定的站点间线路上很有用,但它并不无条件地保证跨重启的持久性。
把队列与数据库更新一起确定:事务
队列操作可以纳入事务;用上 MS DTC(分布式事务协调器)之后,可以把从队列取出和数据库更新放进一个分布式事务中一起确定。
不引入额外中间件就能用:随操作系统同捆
因为不需要额外引入中间件,2000 年代的订单收发联动、报表的异步处理、生产现场的工序间联动等场景都广泛使用了它。这 3 点的组合,正是它至今仍然留存的原因。
用术语区分累积、持久化与事务
判断表中用到的术语,也在这里统一。9
| 术语 | 含义,以及需要确认它的理由 |
|---|---|
| 私有队列 / 公共队列 | 私有队列只注册在本地计算机上,不发布到 Active Directory。用 .\private$\队列名 这样的形式指定。公共队列注册到目录服务中,可以在域内检索到。中小规模业务系统中多为私有队列 |
| express 消息 | 默认的发送模式。传输途中和送达之后都放在内存里,因此速度快,但所在计算机或 MSMQ 服务一停就会丢失 |
| recoverable(可恢复)消息 | 在发送端、中继节点和目的队列上都保存到磁盘,可以跨重启保留下来。需要在发送端显式指定 |
| 事务队列 | 只处理事务消息。该属性在创建队列时就已确定,之后无法更改。事务消息会持久化到磁盘 |
| DTC | 调停跨多个资源(如 MSMQ 与数据库)的事务的 Windows 服务。队列与数据库的一致性该如何替代,是迁移时的争论焦点 |
| 日志队列 / 无法送达队列 | MSMQ 生成的系统队列。前者保留已发送、已取出消息的副本,后者保留未能送达的消息。要监控放任不管带来的容量增长 |
“接收端停了也能累积”和“MSMQ 或机器重启后消息仍在”是两回事。要分开调查:只用到了前者,还是也需要由 recoverable 消息或事务消息实现的后者。
3.2. 从代码、配置和运维中找出依赖
不要只看库引用就结束盘点
先找 System.Messaging 的引用和 MessageQueue 的创建位置。但仅凭那里没有引用,还不能断定“没有使用 MSMQ”。要按调用路径分别确认。
| 调用路径 | 在代码和配置中要找的东西 |
|---|---|
| .NET Framework 的库 | System.Messaging 的引用、MessageQueue 的创建位置 |
| WCF 的配置 | netMsmqBinding / msmqIntegrationBinding |
| 原生 API 与 COM | MQSendMessage 等 MQ* 原生函数、经由 COM 的调用 |
逐个队列留下迁移判断的依据
用下面的表,逐个队列记录确认结果。目的不是确认完就算了,而是把它作为迁移方针和立项材料的依据留存下来。
| 观察点 | 看哪里 | 记录什么 | 对判断的作用 |
|---|---|---|---|
| (a)队列路径 | 传给 MessageQueue 的路径字符串、配置文件中的连接目标、FormatName: 指定 |
本地还是远程、私有还是公共、对端机器名 | 如果是远程或跨站点,就据此判断是否需要 store-and-forward(4.2 节的“离线耐受性”一行)。如果本地就能闭环,则落到 4.2 节的第 1 行 |
| (b)事务与 recoverable | 队列创建位置(是不是事务队列)、发送时是否指定了 recoverable、是否使用了 DTC | 是不是事务队列、有没有指定 recoverable、是否参与 DTC | 用了 DTC,迁移目标基本就定为表队列。两者都没有时,要写明现行系统是按“重启就会丢消息”的前提在运维的 |
| (c)格式化器 | XmlMessageFormatter / BinaryMessageFormatter / ActiveXMessageFormatter 的指定位置 |
使用的格式化器,以及消息正文的类型 | 属于 BinaryFormatter 系列就必须换成 JSON(6.1 节)。这会成为迁移工时的主要来源8 |
| (d)日志队列与无法送达队列 | 队列属性(是否启用日志队列)、系统队列里的内容、运维手册 | 是否使用日志队列、是否真的在看无法送达队列 | “失败消息怎么处理”会成为迁移目标的设计需求(用表队列的话就是转存表) |
| (e)谁在创建和删除 | 安装程序、部署脚本、应用启动时的 Create 调用 |
创建主体,以及由谁来设置权限 | 迁移时“新队列由谁来准备”就是它的直接对应。选择搁置时,要把它转写进装机步骤(第 2 章) |
4. 迁移目标:不按后继产品名,而按需要的性质来选
4.1. 用 4 个问题梳理需求
根据盘点结果,确认下面 4 点。
- 接收端做的是不是更新自家业务数据库的处理。
- 事务队列与数据库更新,是不是用 DTC 一起确定的。
- 发送端与接收端是否在不同机器、不同站点。是否真的用到了 store-and-forward。
- 消息量有多大。如果是多数业务系统那种一天几千到几万条的规模,仅凭候选方案的处理能力很难成为决定因素。
4.2. 按性质给出的首选
如果是更新同一系统业务数据库的异步任务,先考虑表队列。只有当出现向多个系统分发、路由等表队列满足不了的需求时,才选择消息代理。
| 用到的性质 | 首选 | 理由与注意事项 |
|---|---|---|
| 同一系统内的异步处理(接收端更新数据库) | 数据库的表队列 | 可以把“取出消息”和“更新业务数据”放进同一个本地事务中确定,不再需要 DTC。既有的备份、监控和运维流程可以直接沿用。另外,发送端也可以把“更新业务数据与插入消息行”放进同一事务,这就是所谓的 Outbox 模式 |
| 用 DTC 让队列与数据库更新保持原子性 | 数据库的表队列 | 把分布式事务换成本地事务才是这次迁移的本质。云上的队列服务无法参与 DTC,只要还有这项需求,走消息代理就是绕远路 |
| 多系统、多语言之间的松耦合联动 | RabbitMQ(本地部署) / Azure Service Bus(可放到云上) | 分发的扇出与路由是消息代理的强项。选 RabbitMQ 要把自行运维的负担(冗余、升级)估算进去 |
| 站点间的离线耐受性(store-and-forward) | Azure Service Bus 等+重发设计,或者站点侧表队列+同步 | 能透明替代 MSMQ 那种“先存在发送端本地、之后再送达”的机制并不多。要改成由应用显式承担在发送端暂存这一职责的设计 |
| 同一进程内的生产者/消费者 | .NET 的 Channels 等内存队列 | 这类情况本来就不需要跨进程队列。在进程内闭环即可,需要持久性再转向表队列 |
| 只当作 Windows 服务之间的进程间通信来用 | 命名管道等 IPC | 但能这样替换的只有双方始终同时运行的情况。接收端停止期间,MSMQ(即使是 express 消息)仍会接收发送并在之后送达,而管道会立即失败。如果依赖停机期间的累积,就要在应用侧实现重发与缓冲,或者保留队列。怎么选见《Windows 的 IPC 判断表》 |
为什么先考虑表队列
这里说“先考虑表队列”,并不是因为想在任何用途上都推荐数据库。而是因为在 MSMQ 时代的业务系统中常见的更新同一个数据库的异步处理里,用表队列既能简单地保持一致性,又能利用既有的备份、监控与运维流程。
适合消息代理的需求,以及需要额外设计的部分
RabbitMQ 和 Azure Service Bus 适合多系统、多语言的松耦合分发、扇出和路由。需要高吞吐量时也可以纳入考虑。但由于队列与业务数据库成了不同的资源,不能把 MSMQ+DTC 的原子性原样搬过去,而要在应用侧设计幂等性与重发。新中间件的监控、冗余、补丁和人员培训也要算进估算里。
即使迁移,也不代表停机期间的累积可以直接砍掉
请确认离线耐受性和接收端停机期间的累积,是不是可以去掉的需求。选了云上的队列,也不会自动附带发送端本地的累积;换成命名管道,停机期间的待接收也不会以同样的形式保留下来。需要的话,就在应用侧加上重发与缓冲,或者把队列保留下来。
5. 表队列:把队列操作与业务更新放在同一个数据库中确定
本章先确认把两者放进同一个数据库的意义。在此基础上,依次来看取出 SQL 的前提、顺序的保证范围、工作进程的事务、实际运维中要补上的处理。
5.1. 为什么可以不再需要 DTC
在 MSMQ 中,队列与数据库是不同的资源。要把“已从队列取出”和“已更新业务数据”一起确定,就需要由 DTC 支撑的分布式事务。
把队列改成与业务数据同一个数据库中的表之后,两者都变成对该数据库的更新。取出、业务更新和完成记录可以用一个本地事务确定。把分布式事务换成本地事务,正是这次迁移的核心。
不只接收端,发送端也能同时确定
在发送端,也可以把业务数据的更新和消息行的 INSERT 放进同一个事务。这就是 Outbox 模式,可以防止“只更新了业务数据,消息却没有发出去”这种不一致。
下面是 SQL Server 上的最小形式。C# 部分是用来表示事务边界的伪代码,不是可以直接部署的完整实现。
5.2. 保存任务的表
CREATE TABLE dbo.JobQueue (
Id BIGINT IDENTITY(1,1) PRIMARY KEY,
Payload NVARCHAR(MAX) NOT NULL, -- 正文。用 JSON 保存
Status TINYINT NOT NULL DEFAULT 0, -- 0:未处理 1:处理中 2:已完成
EnqueuedAt DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
StartedAt DATETIME2 NULL, -- 用于回收停在“处理中”的行
RetryCount INT NOT NULL DEFAULT 0
);
CREATE INDEX IX_JobQueue_Status ON dbo.JobQueue (Status, Id);
Payload 中存放 JSON 正文,用 Status 区分未处理、处理中和已完成。StartedAt 与 RetryCount 用于回收和重试的运维。
5.3. 取出一条,以及数据库设置的确认
用一条 SQL 完成取出行的更新与正文的获取
取出时,一边把一条更新为“处理中”,一边用 OUTPUT 接收正文。借助 READPAST,可以跳过被其他工作进程行锁住的候选而不必等待,从而用多个进程并行处理。10
WITH next_job AS (
SELECT TOP (1) *
-- READCOMMITTEDLOCK 在 READ_COMMITTED_SNAPSHOT 为 ON 的数据库上是必需的(后述)。
-- 在 OFF 的数据库上它与默认行为相同,所以一直带着就能同时兼顾两种情况
FROM dbo.JobQueue WITH (READPAST, UPDLOCK, READCOMMITTEDLOCK)
WHERE Status = 0
ORDER BY Id -- 按投入顺序取出。这是它成其为“队列”的条件
)
UPDATE next_job
SET Status = 1, StartedAt = SYSUTCDATETIME()
OUTPUT inserted.Id, inserted.Payload;
先确认快照设置与隔离级别
读这段 SQL 时,要确认 READ_COMMITTED_SNAPSHOT 和会话的隔离级别。
官方文档指出,当数据库的 READ_COMMITTED_SNAPSHOT 为 ON,且会话处于 READ COMMITTED、或者同时使用了 READCOMMITTED 提示时,就不能直接指定 READPAST。处理办法是:如果有 READCOMMITTED 提示就去掉它,并加入 READCOMMITTEDLOCK。10
默认的隔离级别就是 READ COMMITTED,因此不做任何处理就把它搬到启用了快照的数据库上,结果不是单纯被阻塞,而是SQL 语句直接报错。Azure SQL Database 默认为 ON。本地部署的数据库也可能为了缓解读取阻塞而启用了它,所以要先查清楚。
SELECT is_read_committed_snapshot_on FROM sys.databases WHERE name = DB_NAME();
上面的取出 SQL 加了 READCOMMITTEDLOCK,无论快照设置如何都使用基于锁的读取。在 OFF 的数据库上,它与默认行为相同。10
不一并写上 ROWLOCK 的理由,以及 READPAST 的局限
另一方面,与常见的 WITH (READPAST, UPDLOCK, ROWLOCK) 不同,这里没有一并写上 ROWLOCK。因为ROWLOCK 与 READCOMMITTEDLOCK 属于同一组粒度提示,同一张表上不能同时指定两者。10
READPAST 能跳过的只有行锁,跳不过页锁。像本例这样用 TOP (1) 的索引查找取一行的范围内,锁升级实际上不成问题;但如果想显式写上 ROWLOCK,就要在确认 READ_COMMITTED_SNAPSHOT 为 OFF 之后,把 READCOMMITTEDLOCK 去掉。
5.4. 取出顺序与写入业务数据的顺序是两回事
要给取出的候选指定排序
省掉 ORDER BY 写成 UPDATE TOP (1),选中哪一行是不确定的。UPDATE 的 TOP 不会对目标行排序,仅仅存在 (Status, Id) 索引也不构成顺序保证。为了避免旧任务一直被往后排,示例中指定了 ORDER BY Id。11
并行处理时先取出的未必先生效
不过,并行处理的完成顺序是对不齐的。工作进程 A 在处理任务 1 期间,工作进程 B 会用 READPAST 跳过那一行,把任务 2 先提交。
| 能对齐的 | 对不齐的 |
|---|---|
哪一行先被取出(ORDER BY Id) |
哪一行先写入业务数据 |
如果像对同一个订单号的更新那样,写入顺序本身有意义,就要在下面两种做法中选一种。
| 保证顺序的方法 | 得到的性质与付出的代价 |
|---|---|
| 只留一个消费者 | 舍弃并行度换取顺序。只要还能满足所需的吞吐量,这是最简单的做法 |
| 按键拆分 | 按订单号等把工作进程固定下来,或者加上“同一个键正在处理中就不取出”的条件。保证的是同一个键内部的顺序,跨键的顺序不保证 |
两种都不选,就要在设计中写明并行处理不保证整体的 FIFO。在 MSMQ 上并行接收也会出现同样的问题。重要的是,不要带着“既然是队列就一定按顺序”的理解去做迁移。
5.5. 工作进程的事务边界
接收端要对齐的,是取出、业务数据更新和完成记录这三者的事务。下面的伪代码把这三个处理交给同一个连接和事务,最后一起确定。
while (!stoppingToken.IsCancellationRequested)
{
using var tx = connection.BeginTransaction();
var job = DequeueOne(connection, tx); // 上面的 UPDATE ... OUTPUT
if (job is null)
{
tx.Commit();
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken); // 为空时只等待一个轮询间隔
continue;
}
ApplyBusinessData(connection, tx, job); // ← 更新业务数据(真正想做的工作)
MarkDone(connection, tx, job.Id); // Status = 2
tx.Commit(); // “取出”与“业务更新”在这一行同时确定
}
要点在于:DequeueOne、ApplyBusinessData、MarkDone 使用同一个连接和事务,最后的 Commit 把取出与业务更新一起确定。换成消息代理时,这种同库的性质就不存在了,需要另行设计一致性方案。
5.6. 实际运维中要补上的回收、重试与清理
在最小示例之外,还要设计下面这些运维处理。
| 运维项 | 处理内容 |
|---|---|
| 回收停在“处理中”的行 | 把 Status = 1 且 StartedAt 早于一定时间的行改回 Status = 0 的恢复处理 |
| 重试上限与转存 | 把 RetryCount 超过上限的行移到别的表,或改成 Status = 9 之类。这相当于 MSMQ 的无法送达队列 |
| 清理已完成的行 | 定期删除或归档 Status = 2 的行,防止表和索引膨胀 |
在中小规模的业务系统中,用表加轮询、或者加通知的形式,往往就够用了。在增加一套队列产品之前,先确认这种实现能否满足所需的性质与运维要求。
6. 切换步骤:先把格式、行为和接收端对齐
切换时,先把消息格式以及输入与结果的测试对齐。然后再选择是用桥接连通新旧,还是把旧队列排空后切换。
6.1. 确定新旧能够共存的消息格式
迁移后默认使用 JSON。不要带过去的是 BinaryMessageFormatter 这类BinaryFormatter 系列的序列化。BinaryFormatter 的实现已在 .NET 9 中从运行时删除,从安全角度也不推荐。8
另一方面,并不是所有二进制格式都不行。像 Protocol Buffers、MessagePack 这样规范由独立方维护的格式,把它原样带到新系统本身没有问题。先在 3.2 节的盘点中确认格式化器与正文的类型。
6.2. 改写之前,先用测试把输入与结果固定下来
队列处理是容易出现时序相关缺陷的部分。先准备好“给出这条输入消息,就会得到这个结果”的特性化测试,让替换之后也能机械地确认结果一致。也请参考《用特性化测试固定行为之后再重构》。
6.3. 先从接收端做起,桥接要按会丢失、会重复来设计
先让接收端支持新队列
先准备好新队列,让接收端支持它,然后再切换发送端。
中间加一个把消息从旧 MSMQ 搬到新队列的小桥接程序,就不必一次性切换所有发送端。桥接程序本身留在 .NET Framework 上也没关系。
按有无事务区分交接步骤
但是,如果在从 MSMQ 取出之后、写入新队列之前停机,就会发生丢失或重复投递。要根据迁移源队列的种类,按下面的方式设计。
| 迁移源 | 最低限度必需的交接方式 |
|---|---|
| 事务队列 | 把 MSMQ 侧改为事务接收,写入失败时可以回退。新队列一侧按消息 ID 去重,做幂等处理 |
| 非事务队列 | 用 Peek 读取 → 幂等地写入新队列 → 确认写入成功后再从旧队列中移除。移除之前停机造成的重复,由新队列一侧吸收 |
队列的事务属性在创建时就已确定,之后无法更改。请注意,不能把事务接收的步骤原样用在非事务队列上。
6.4. 桥接不划算,就排空再切换
如果为桥接所做的丢失与重复对策与系统规模不相称,就不要勉强让新旧并存,而是转向计划停机把旧队列排空之后再切换的做法。
留着消息就切换,会造成重复处理和丢失。把队列排空再切换,结果往往是最安全也最快的做法——这是本文基于实务给出的判断。
7. 总结
在本文的基准时点 2026 年 7 月,MSMQ 尚未被官方列为弃用。作为操作系统功能留存下来,和容易迁移到 .NET,是两回事。System.Messaging 封闭在 .NET Framework 之内,要把应用升到 .NET,原则上队列也要一并重新审视。123
如果短期内继续使用,就要确认操作系统的支持期限,并准备好搭建与恢复步骤、队列监控、配置记录以及每年一次的判断复查。搁置的最大风险不只在技术,还在于将来没有人能碰它。
如果要迁移,先盘点依赖与消息格式。对于更新同一个业务数据库的异步处理,表队列是首选。它可以把原先由 MSMQ+DTC 承担的一致性,换成同一数据库上的本地事务。出现向多个系统分发、路由等表队列满足不了的需求时,再考虑 RabbitMQ 或 Azure Service Bus。
在此基础上,再确认 SQL 的隔离级别与提示、并行时的写入顺序,以及桥接的丢失与重复对策。重要的是,不要因为“听说被废止了”就着急,而要先掌握自己的系统究竟依赖什么,再决定维持还是迁移。
相关文章
- VB6 应用能用到什么时候 —— 运行时支持现状与务实的 .NET 迁移做法
- .NET Framework→.NET 迁移前检查清单
- 用特性化测试(Characterization Test)固定行为之后再重构
- Windows 的进程间通信(IPC)判断表
- 认为按 Send 的单位就能 Receive 的误解 —— 按字节流处理的接收端设计
- 接手了没有源代码也没有文档的系统,该怎么承接下来
- 业务应用的数据库架构迁移与版本管理
相关咨询领域
小村软件有限责任公司承接包括 MSMQ 在内的遗留架构盘点与迁移规划、从 .NET Framework 到 .NET 的迁移,以及包含队列处理在内的业务系统改造。
参考链接
-
Microsoft Learn, Deprecated features in the Windows client。Windows 客户端上已结束积极开发(即被弃用)功能的官方一览。关于截至 2026 年 7 月的一览中列有 NTLM、VBScript、WordPad 等,而没有列出 MSMQ(Microsoft Message Queuing);以及弃用(deprecated)是“未在积极开发,将来的更新中可能被删除”的阶段,与删除(removed)有所区别这两点。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Features Removed or No Longer Developed in Windows Server。Windows Server 中已删除功能与停止开发(弃用)功能的官方一览。关于包括 Windows Server 2025 标签页在内、截至 2026 年 7 月的一览中没有列出 MSMQ;以及被弃用的组件仍会继续同捆在 Windows Server 中,按产品生命周期为生产环境提供支持,并持续获得安全更新与质量更新这两点。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, MessageQueue Class (System.Messaging)。关于 System.Messaging.MessageQueue 类的参考文档所覆盖的版本是 .NET Framework 1.1 到 4.8.1,不存在面向 .NET(Core 及以后)的版本这一点。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, .NET Framework official support policy。关于 .NET Framework 4.8 被定义为 Windows 操作系统的组件,按所安装的父产品(操作系统)的生命周期策略获得支持这一点。 ↩ ↩2
-
Microsoft Learn(存档), Message Queuing Functions以及MQSendMessage、MQReceiveMessage。关于 MSMQ 的 Win32 原生 API(MQCreateQueue、MQSendMessage、MQReceiveMessage 等)面向 C/C++ 应用程序做了文档化,可以不经过托管 API 就完成队列的创建、发送与接收这一点。 ↩ ↩2
-
Microsoft Learn, Use the Windows Compatibility Pack to port code to .NET。关于 Windows 兼容性包(Microsoft.Windows.Compatibility 包)为在迁移到 .NET 时弥补对 .NET Framework 专用 API 的依赖而提供约两万个 API;以及其技术领域一览(CodeDom、配置、Directory Services、Drawing、ODBC、ACL、WCF、注册表、WMI、性能计数器、Windows 服务、EventLog 等)中不包含消息传递(System.Messaging)这两点。 ↩ ↩2
-
CoreWCF project, CoreWCF.MSMQ (NuGet) 以及 CoreWCF 1.4.0 Preview release、Microsoft, CoreWCF Support Policy。关于 CoreWCF 是把 WCF 服务端移植到 .NET 的社区主导项目,微软为它提供了官方的支持策略;作为队列类传输的一环,已经发布了对应 MSMQ 的包(CoreWCF.MSMQ);以及该 MSMQ 实现依赖 .NET Framework 版 System.Messaging 库的社区移植这几点。 ↩ ↩2 ↩3
-
Microsoft Learn, BinaryFormatter migration guide。关于 BinaryFormatter 出于安全原因被分阶段废止,.NET 9 以后其实现已从运行时删除、默认无法使用;以及官方把 JSON(System.Text.Json)等更安全的序列化格式作为迁移目标加以引导这两点。 ↩ ↩2 ↩3
-
Microsoft Learn(存档), Express and Recoverable Messaging、System-Generated Queues、Message Queuing (MSMQ)。关于 express 消息在传输途中和送达之后都保存在 RAM 上,会因消息所在计算机停止或 MSMQ 服务停止而丢失;recoverable 消息会在发送端以及中继经过的每台计算机上写入磁盘,在目的队列中也保存在磁盘上;公共队列会注册到目录服务中,而私有队列只注册在本地计算机上、不会发布到目录服务;以及保留从队列中取出的消息和已发送消息副本的日志队列,与保留未能送达消息的无法送达(死信)队列,都是 MSMQ 生成的系统队列这几点。 ↩
-
Microsoft Learn, 表提示 (Transact-SQL)。关于
READPAST是跳过其他事务锁住的行而不去读取的提示,能跳过的只有行级锁、跳不过页级锁,并且只能在READ COMMITTED或REPEATABLE READ隔离级别下指定这一点。尤其是关于下面这段描述:“当READ_COMMITTED_SNAPSHOT数据库选项设置为ON,且 (a) 会话的事务隔离级别为READ COMMITTED,或 (b) 查询中同时指定了READCOMMITTED表提示,两者中任一为真时,不能指定READPAST表提示。要在这些情况下指定READPAST提示,需要先删除(若存在)READCOMMITTED表提示,并在查询中加入READCOMMITTEDLOCK表提示”。READCOMMITTEDLOCK是无论READ_COMMITTED_SNAPSHOT如何设置都以加锁方式执行READ COMMITTED的提示,以及对同一张表不能指定两个以上的粒度提示(PAGLOCK/NOLOCK/READCOMMITTEDLOCK/ROWLOCK/TABLOCK/TABLOCKX),也请参见同一页面。数据库一侧的设置可以通过 sys.databases 的is_read_committed_snapshot_on确认。 ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, TOP (Transact-SQL)。关于在 INSERT、UPDATE、MERGE、DELETE 中使用 TOP 时,被引用的行并不会按某种顺序排列;以及想要确定顺序时,应使用带有 TOP 与 ORDER BY 的子查询这两点。 ↩
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
VB6 应用能用到什么时候 ── 运行时支持现状与务实的 .NET 迁移做法
VB6 应用程序到底能用到什么时候?本文整理 VB6 运行时的支持政策(Windows 11 也在支持范围内)与 IDE 支持早已终止这一不对称现状,并以实务指南的形式说明全面重写、自动转换、分阶段迁移的判断表、迁移前的资产盘点、VB6 与 .NET 的不兼容之处,以及 C...
参数为什么会坏掉 ── Windows 命令行参数的规则
在 Windows 上并不存在参数的数组,传给 CreateProcess 的是一条字符串,切分由接收方完成。本文讲解 CommandLineToArgvW・CRT・.NET 的切分规则,以及 .NET 的 ArgumentList 与 C++ 中正确的组装方法。
多线程实战最佳实践 .NET 篇——增加线程之前必须先定好的事
面向 .NET/C# 梳理防止“线程一开就偶尔崩溃、偶尔卡死”的设计做法,涵盖不自己创建线程而依托 Task、减少共享可变状态、加锁的纪律、用 CancellationToken 设计停止流程,直到 UI 线程的处理方式。
在 C# 与 PowerShell 中使用 WMI/CIM——硬件信息获取、进程监控与远程查询实务指南
获取电脑序列号、监控磁盘剩余空间、检测进程启动,这些需求的经典答案就是 WMI/CIM。本文讲解 Get-CimInstance 等 CIM cmdlet 的用法与从旧版 Get-WmiObject 的迁移、C# 中 System.Management 与 CIM API ...
Windows I/O 底层原理(第 4 篇)——缓存管理器:你的 WriteFile 何时才真正写入磁盘
本文是图解讲解 Windows 缓存管理器的系列第 4 篇。梳理以文件映射方式实现的缓存、预读与延迟写入、FlushFileBuffers 与 FILE_FLAG_NO_BUFFERING 的取舍,以及断电导致数据丢失的条件。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
ActiveX 迁移
整理保留、包装或替换 COM / ActiveX / OCX 资产的阶段性判断的主题页面。
与本主题相关的服务
本文与以下服务页面相关联,欢迎从最接近的入口查看。
Windows 应用程序开发
支持包含常驻处理、设备联动、运行日志与可维护结构的 Windows 桌面应用程序。
Windows 软件维护 & 现代化
支持既有 Windows 软件的阶段性升级、功能追加、64 位就绪以及可维护性的重构。
常见问题
汇总了咨询这一主题时常见的问题。
- MSMQ 已经被废止了吗?
- 没有。截至 2026 年 7 月,MSMQ 既没有出现在 Windows 客户端的弃用功能一览中,也没有出现在 Windows Server 的“已删除功能与停止开发功能”一览中。它仍作为 Windows 的可选功能同捆在现行操作系统中。“已被废止”这种说法之所以流传开来,是因为与 .NET 一侧的状况混淆了。处理 MSMQ 的标准类库 System.Messaging 只存在于 .NET Framework,并未移植到 .NET(Core 及以后)。也就是说,准确的状态是“作为操作系统功能仍然存续,但没有从现代 .NET 使用它的官方手段”。
- 有没有办法从 .NET 8 或 .NET 10 使用 MSMQ?
- 没有官方的托管 API。System.Messaging 是面向 .NET Framework 4.8.1 及之前版本的 API,Windows 兼容性包(Microsoft.Windows.Compatibility)中也不包含它。用 P/Invoke 调用 MSMQ 的原生 Win32 API(如 MQSendMessage)在技术上可行,但这意味着要自己编写并维护包含格式化器与事务协作在内的包装层。社区方面,CoreWCF 项目发布了用于 MSMQ 传输的包(CoreWCF.MSMQ),但它是为了在现代 .NET 上托管经由队列被调用的 WCF 服务(WCF 服务端的移植),既不是能替代 System.Messaging 的通用队列 API,也不是发送端客户端的替代品。它的实现同样依赖 .NET Framework 版 System.Messaging 的社区移植。它可以作为验证或临时续命的选项。CoreWCF 本身有微软官方的支持策略,但这并不意味着这个 MSMQ 实现所依赖的 System.Messaging 社区移植也享有同样的保证,因此要把它作为业务系统的前提,就应先确认所用版本是否属于支持策略的对象、以及依赖部分如何处理,再做判断。从实务角度看,正道是在把应用升到 .NET 的同时一并迁移队列。
- 迁移目标选 RabbitMQ 还是 Azure Service Bus 更好?
- 在这二选一之前,请先考虑把数据库的表当作队列使用的方案。使用 MSMQ 的中小规模业务系统,多数情况下队列的对端处理就是更新自家的数据库。这种情况下,表队列可以在与业务数据相同的本地事务中确定“取出消息”与“更新业务数据”,用更简单的机制替代原本由 MSMQ+分布式事务(DTC)实现的一致性。只有在表队列满足不了的需求上,例如需要多系统之间的松耦合分发或高吞吐量时,才去考虑:本地部署需求强就选 RabbitMQ,可以放到云上就选 Azure Service Bus——按这个顺序推进不容易出问题。
- 短期内保持 .NET Framework 不动、把它搁置起来,这个判断可行吗?
- 有条件地可行。.NET Framework 4.8 作为 Windows 的组件,按操作系统的生命周期获得支持,MSMQ 本身也没有被弃用,因此可以预期它会“继续运行”。不过,如果选择搁置,至少要做到下面 4 点:在装机步骤中写明 MSMQ 功能的启用方法、加上队列长度与日志队列的监控、把消息格式与连接配置写成文档、为人员调动做好准备并留下恢复步骤。搁置真正的风险不在技术,而在于“变得没有人能碰它”。