- 后端
- 工作流自动化
- 流程编排
【免费下载链接】workflow-core
Lightweight workflow engine for .NET Standard
本指南围绕 Workflow Core 官方提供程序WorkflowCore.LockProviders.SqlServer展开,讲解如何利用 SQL Server 内置的sp_getapplock/sp_releaseapplock存储过程实现分布式锁管理器(DLM,Distributed Lock Manager),从而支撑多节点集群并发处理工作流。读完本文,你将掌握该提供程序的安装、配置、底层锁机制、资源命名与返回码语义,并能在自己的 ASP.NET Core / 控制台应用中组合队列提供程序搭建真正可水平扩展的 Workflow Core 集群。
为什么需要分布式锁:从单节点到集群
Workflow Core 默认在单节点模式下运行。从 WorkflowOptions 的源码可以看到,其内置的锁与队列均为进程内实现:LockFactory默认构造SingleNodeLockProvider,QueueFactory默认构造SingleNodeQueueProvider。SingleNodeLockProvider 内部只是一个受lock保护的HashSet<string>,锁只在当前进程内有效。
这意味着:当多个节点同时处理队列中的同一个工作流时,内存锁无法跨进程互斥,两个节点可能同时取出并执行同一个工作流实例,导致状态竞争、重复执行与数据损坏。而 multi-node-clusters 文档 明确指出:要运行多节点集群,必须同时配置外部队列提供程序与分布式锁管理器来协调各节点。
WorkflowCore.LockProviders.SqlServer正是官方提供的分布式锁管理器实现:它以 SQL Server 为锁的权威存储,通过应用锁(Application Lock)实现跨节点互斥,让多个节点可以安全地共同消费同一个工作流队列。
安装提供程序
通过 NuGet 安装官方包:
PM> Install-Package WorkflowCore.LockProviders.SqlServer该包面向netstandard2.0(见 WorkflowCore.LockProviders.SqlServer.csproj),因此可被 .NET Standard 2.0 及以上版本的宿主项目引用。它依赖Microsoft.Data.SqlClient(当前仓库锁定版本为 5.1.5),并通过项目引用依赖核心库WorkflowCore,无需额外手工配置。
快速配置:一行代码接入
在构建服务提供程序(通常为依赖注入容器)时,调用UseSqlServerLocking扩展方法即可替换默认的锁实现:
services.AddWorkflow(x => x.UseSqlServerLocking("connection string"));这里的"connection string"即指向你 SQL Server 实例的 ADO.NET 连接字符串。扩展方法定义在 ServiceCollectionExtensions.cs,其实现非常简洁:
public static WorkflowOptions UseSqlServerLocking(this WorkflowOptions options, string connectionString) { options.UseDistributedLockManager(sp => new SqlLockProvider(connectionString, sp.GetService<ILoggerFactory>())); return options; }它通过 WorkflowOptions.UseDistributedLockManager 将LockFactory替换为SqlLockProvider;随后在 ServiceCollectionExtensions.AddWorkflow 中,该工厂被注册为IDistributedLockProvider单例。所有后台任务与控制器都会从容器解析这个单例并使用它。
配合持久化提供程序(例如 WorkflowCore.Persistence.SqlServer)和队列提供程序(例如 WorkflowCore.QueueProviders.SqlServer 或 RabbitMQ)一起注册,即可形成完整的集群化配置:
services.AddWorkflow(x => { x.UseSqlServerLocking("Server=...;Database=...;User Id=...;Password=...;"); x.UseSqlServerPersistence("Server=...;Database=...;User Id=...;Password=...;"); // x.UseQueueProvider(...); // 外部队列,如 SQL Server / RabbitMQ / Redis 队列 });底层原理:SqlLockProvider 如何工作
锁接口契约
所有分布式锁提供程序都实现 IDistributedLockProvider 接口,其核心方法为:
Task<bool> AcquireLock(string Id, CancellationToken cancellationToken):尝试获取指定资源 ID 的锁,成功返回true;Task ReleaseLock(string Id):释放指定资源 ID 的锁;Task Start()/Task Stop():生命周期钩子,由WorkflowHost在启动/停止时调用(见 WorkflowHost.cs 与#L139)。SqlLockProvider对这两个方法直接返回已完成的任务,无需额外初始化。
sp_getapplock 的参数语义
SqlLockProvider.cs 是核心实现,它把每个工作流资源映射为一条 SQL Server 应用锁。获取锁时执行sp_getapplock存储过程,关键参数如下:
| 参数 | 取值 | 说明 |
|---|---|---|
@Resource | "wfc:{Id}" | 锁资源名称,以固定前缀wfc(Workflow Core 缩写)加冒号再拼接资源 ID,避免与其他应用的应用锁冲突 |
@LockOwner | "Session" | 锁归属范围为当前数据库会话;连接断开时锁自动释放,防止节点崩溃后锁永久残留 |
@LockMode | "Exclusive" | 排他锁,同一时刻只能有一个节点持有 |
@LockTimeout | 0 | 获取锁的等待超时(毫秒)。为0表示不等待,立即返回获取结果 |
构造函数中还通过SqlConnectionStringBuilder对连接字符串做了两处增强(见 SqlLockProvider.cs):强制开启连接池Pooling = true,并把ApplicationName设置为"Workflow Core Lock Manager",便于在 SQL Server 侧诊断连接来源。
返回码语义:谁抢到了锁?
sp_getapplock通过RetVal返回码告知结果,SqlLockProvider逐一处理(对应 SqlLockProvider.cs):
| 返回码 | 含义 | 提供程序行为 |
|---|---|---|
>= 0 | 锁获取成功 | 记录连接并返回true(表示本次会话持锁成功) |
-1 | 请求超时(本次因LockTimeout = 0通常表现为立即失败) | 记录 Debug 日志,返回false |
-2 | 请求被取消 | 记录 Debug 日志,返回false |
-3 | 被选为死锁牺牲者 | 记录 Debug 日志,返回false |
-999 | 参数错误或其他内部错误 | 记录 Error 日志,返回false |
只有返回码非负时才认为持锁成功,否则关闭连接并放弃本次处理。
锁的生命周期:从持锁到释放
- 持锁期间:成功的连接被保存在进程内字典
_locks[Id]中,后续释放锁时直接复用同一连接调用sp_releaseapplock(同样以Session为@LockOwner)。若sp_releaseapplock返回码为负,则记录错误日志。 - 进程内互斥:
AcquireLock与ReleaseLock外围均有AutoResetEvent _mutex保护,保证同一节点内对锁字典与连接集合的并发访问是线程安全的。 - 容错设计:由于锁归属于数据库会话,即使节点进程崩溃、连接断开,SQL Server 也会自动释放该锁,不会出现"锁永远无法释放"的僵局;同时每次获取锁都新建独立连接,天然避免了会话复用带来的误释放风险。
分布式锁在 Workflow Core 中的实际应用场景
从源码结构看,IDistributedLockProvider被注入到多个核心服务中,锁的粒度与资源 ID 设计如下:
- WorkflowConsumer:工作流队列消费者。出队后先
AcquireLock(itemId)(即工作流实例 ID)再执行;若获取失败则直接跳过本次,说明该实例正被其他节点处理。执行完成后在finally中释放锁并清理灰名单。 - EventConsumer:事件队列消费者。事件出队时对
"evt:{itemId}"加锁,唤醒订阅时对订阅所属的sub.WorkflowId加锁,防止多个节点同时投递同一事件。 - RunnablePoller:轮询器通过
"poll runnables"、"unprocessed events"、"poll-commands"等全局资源 ID 加锁,保证同一时刻只有一个节点在轮询持久化存储并派发可执行实例。 - WorkflowController:启动、挂起、恢复、终止工作流等管理操作均以
workflowId加锁,避免管理操作与消费者并发修改同一实例。 - ActivityController:活动订阅以
"sub:{subscription.Id}"加锁,协调分布式活动令牌的领取。 - SyncWorkflowRunner:同步执行器同样基于锁保证同步执行的互斥性。
这些场景印证了分布式锁的两种粒度:实例级(按 workflowId / eventKey 精确互斥同一资源)与全局级(固定资源 ID 实现"主节点选举"式互斥),二者共同保证集群中的并发安全。
集群部署与配置注意事项
- 必须搭配外部队列提供程序:锁只解决"谁先执行"的互斥问题,不解决"工作项如何在节点间分发"的问题。集群化需要同时配置外部队列(如仓库内的 WorkflowCore.QueueProviders.SqlServer、WorkflowCore.QueueProviders.RabbitMQ 等),详见 multi-node-clusters 文档。
- SQL Server 连接要求:提供程序需要能够连接到你指定的 SQL Server 实例并具备执行
sp_getapplock/sp_releaseapplock的权限(public数据库角色默认即可调用这两类系统存储过程)。它不要求额外的表或初始化脚本,也不依赖 Workflow Core 自身的持久化表。 - 锁超时行为:由于
@LockTimeout = 0,获取锁采用"立即失败"而非阻塞等待的策略;消费者获取锁失败后会跳过本次轮询、等待下一个轮询周期重试。这与 WorkflowOptions.PollInterval(默认 10 秒)共同决定了集群的调度节奏。 - 资源前缀隔离:所有锁资源都以
wfc:为前缀,多个 Workflow Core 集群若共享同一个 SQL Server 实例,可通过前缀天然隔离;若需进一步区分,可考虑使用不同数据库。
总结
WorkflowCore.LockProviders.SqlServer是一个零依赖外部中间件(如 Redis、ZooKeeper)的分布式锁方案——只要你的基础设施中已有 SQL Server,就可以用一行UseSqlServerLocking(connectionString)将 Workflow Core 从单节点升级为多节点集群。其底层借助 SQL Server 成熟的应用锁机制(sp_getapplock/sp_releaseapplock,会话级锁所有权)实现了跨节点的互斥与崩溃自动释放,配合外部队列提供程序即可支撑工作流引擎的水平扩展。
- 后端
- 工作流自动化
- 流程编排
【免费下载链接】workflow-core
Lightweight workflow engine for .NET Standard
相关推荐
ASP.NET Core 缓存库 SqlServer 测试指南:基于真实 SQL Server 的分布式缓存功能测试
ASP.NET Core 缓存库 SqlServer 测试指南:基于真实 SQL Server 的分布式缓存功能测试 导读 在 ASP.NET Core 的 s
后端Web框架dotnet-sql-cache 工具实战指南:为 ASP.NET Core SQL Server 分布式缓存创建表与索引
dotnet sql cache 工具实战指南:为 ASP.NET Core SQL Server 分布式缓存创建表与索引 导读 dotnet sql cach
后端Web框架基于 PostgreSQL 咨询锁的分布式锁实现:Medusa `@medusajs/locking-postgres` 模块提供者全解析
基于 PostgreSQL 咨询锁的分布式锁实现:Medusa @medusajs/locking postgres 模块提供者全解析 本篇文章围绕 Medus
后端电商前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考