ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C#工作流引擎实战:从手动审批到10万+TPS的性能优化

C#工作流引擎实战:从手动审批到10万+TPS的性能优化 去年年初系统里堆了一百多个审批流程从采购申请、合同会签到假单、报销、用章申请全是走人事手动流转的老路。业务高峰期每个流程平均要两三天才能跑完一圈光催办的消息一天就得几十条。后来实在扛不住我带着团队用C#从零自研了一套轻量级工作流引擎把流程定义、状态流转、审批节点、超时提醒全部接上自动化链路单机实测TPS从原来的人工处理水平直接冲到10万。这篇文章不吹数字而是想把当时从架构设计、核心代码到性能调优的完整路径拆出来给正在做类似系统、或者正被审批流程折磨的兄弟们一份能直接参考的实战经验。1. 项目拆解与整体架构设计1.1 先把标题里的水分挤干10万TPS到底意味着什么聊性能指标之前先把它说透。TPSTransactions Per Second每秒事务数指的是一秒内系统能完成多少次完整的业务操作。工作流引擎里的一个事务通常是从一个节点流转到下一个节点包括状态变更、节点动作执行、日志写入、持久化提交这整套操作。10万TPS不是随便一台机器就能跑出来的。这个数字要成立需要有四个前提压测场景是纯内存执行、不落盘的极简单流程节点动作只是状态切换不涉及外部系统调用数据库批量写入而不是每条流程一次事务提交后端服务本身扛住了并发没有明显的锁竞争或GC瓶颈。我们当时压测用的是一台8核16G的云主机跑的是定义好的三节点直连流程数据先打在内存缓冲队列里异步批量写库最终得到单机TPS破十万的结果。你要真拿这个数字去对标SAP、Flowable这些重型流程引擎的完整审批场景那不公平大家衡量口径不一样。我提这个不是泼冷水而是想说清楚一个道理性能优化是系统工程指标的成立范围和口径同样重要。你只有在知道自己到底测的是什么、优化的是什么之后才可能把指标做上去。1.2 为什么选C#而不是Java或Go来写工作流引擎这个选择当初在团队里还争论过一阵子。Java有Flowable、Activiti这些成熟的开源工作流引擎Go也有Conductor、Temporal这类分布式调度框架。为什么我们最终还是决定用C#从零实现最核心的原因是团队技术栈和现有系统高度统一。我们现有的业务后端都是.NET Core如果引入一套Java生态的流程引擎就意味着要多维护一套独立的微服务、一套CI/CD流水线还有跨语言对接的序列化协议和异常排查成本。为了一个流程引擎把整个研发体系的复杂度抬上去不划算。第二个原因是C#本身在这一领域的表达能力确实强。委托、事件、async/await、表达式树这些特性非常适合描述状态迁移、节点触发条件和异步动作。尤其async/await在处理超时提醒、外部回调、并行分支的时候写起来非常清爽。第三个原因是性能可控。.NET Core以后JIT和GC都有大幅优化配合结构体和Span这类底层能力单机的吞吐压到极致并不输给Go的Goroutine模型。更何况工作流引擎的瓶颈一般不在语言本身而在持久化和锁设计上。1.3 从“100个手动审批”到自动化需求抽象是关键手动审批流程之所以慢不是因为审批的人慢而是因为流程没有结构化。每个流转环节在哪个节点、多久没处理、超时该提醒谁、转给谁全靠人脑去记。你要自动化首先就得把所有流程抽象成机器能理解的结构。我们花了两周时间盘点了一百多个现存审批流程最后总结出四个共性概念流程定义一张流程长什么样有哪些节点、什么顺序、什么条件分支流程实例一条具体的业务单据走到哪了、状态如何、经历过哪些节点节点审批、会签、抄送、自动动作等最小执行单元事件触发节点流转的外部信号比如提交、同意、驳回、超时。把业务需求抽象成这四个概念以后你就会发现无论是采购申请还是用章申请本质上都是同一套流转模型。所谓自动化革命说穿了就是把“人眼判断下一步”变成“引擎根据定义和上下文自动判断下一步”。2. 核心引擎模块的代码深度解析2.1 流程定义模型用代码把流程画出来工作流引擎的基石是流程定义。不能把流程写死在业务代码里否则每次改审批路径都要重新发版。所以第一步就是把流程定义设计成可持久化、可解析的数据模型。下面是我们当时的核心定义类代码量不大但撑起了整个引擎的节点导航能力public class WorkflowDefinition { public string WorkflowId { get; set; } public string Version { get; set; } public string Name { get; set; } public Dictionarystring, WorkflowNode Nodes { get; set; } public WorkflowNode GetNode(string nodeId) { return Nodes.GetValueOrDefault(nodeId); } public WorkflowNode EnterNode Nodes[start]; public void Validate() { if (!Nodes.ContainsKey(start)) throw new InvalidOperationException(流程必须包含 start 节点); if (!Nodes.Any(n n.Value.Type NodeType.End)) throw new InvalidOperationException(流程必须包含至少一个 End 节点); } } public class WorkflowNode { public string NodeId { get; set; } public NodeType Type { get; set; } public string NextNodeId { get; set; } public FuncWorkflowContext, bool Condition { get; set; } public ActionWorkflowContext Action { get; set; } public TimeSpan? Timeout { get; set; } } public enum NodeType { Start, Approve, Condition, Fork, Join, End }有几个设计细节说一下。节点用字典存储而不是列表是为了O(1)的节点查找。Condition是一个FuncWorkflowContext, bool委托把条件判断从硬编码的高阶if-else里解放出来。比如在审批流里“金额大于五万走总经理审批否则部门经理审批”就是两个节点挂上不同的委托。Timeout字段也很关键它内置了超时定义。因为手动审批的老大难问题之一就是审批单在某个节点睡觉没人管。有了超时定义引擎就可以在超时后自动触发提醒或回调。流程定义本身是纯内存对象树至于如何从数据库或JSON反序列化出来我们在后续持久化模块里会处理。这样做的好处非常直接定义和运行分离改流程定义不影响正在运行的流程实例。2.2 状态机引擎从状态到状态的流转核心流程引擎的核心驱动力就是状态机。每一个流程实例本质上就是一个状态节点就是触发状态迁移的事件。这里我们摒弃了传统的状态机框架因为工作流的状态迁移不仅要考虑当前状态还要考虑流程的上下文数据比如金额、审批人、附件。看这个核心的流转方法public WorkflowNode Step(WorkflowContext ctx, string currentNodeId) { if (ctx null) throw new ArgumentNullException(nameof(ctx)); var definition _cache.GetDefinition(ctx.WorkflowId, ctx.Version); var node definition.GetNode(currentNodeId); if (node null) throw new InvalidOperationException($节点 {currentNodeId} 不存在); // 执行节点的动作比如状态更新、通知发送 node.Action?.Invoke(ctx); // 到达终点的判断 if (node.Type NodeType.End) { ctx.Instance.Status InstanceStatus.Completed; return null; } WorkflowNode nextNode; // 如果节点自带条件走条件路由 if (node.Type NodeType.Condition) { var matched definition.Nodes.Values .Where(n n.Condition ! null) .FirstOrDefault(n n.Condition(ctx)); if (matched null) throw new InvalidOperationException($节点 {currentNodeId} 没有匹配的条件分支); nextNode matched; } else { nextNode definition.GetNode(node.NextNodeId); } // 节点迁移 ctx.Instance.MoveTo(nextNode.NodeId); return nextNode; }这段代码的关键点不只是状态迁移还有它把“执行动作”和“迁移”拆开了。现实中审批流的动作五花八门同意之后要更新订单状态、要通知财务、要写审计日志这些都是动作不是流程控制逻辑。把动作挂在节点上引擎只负责迁移业务代码只关心动作两者解耦。再仔细看条件路由那一段我们没有用复杂的规则引擎而是简单地遍历所有带Condition的节点找到第一个返回true的节点。这在大多数企业审批流场景里够用了。如果有一天条件多了、复杂了再考虑引入规则引擎也不迟但起步阶段务必保持简单。2.3 持久化与并发一致性绝对不能丢状态的底层保障工作流引擎和普通业务接口最大的区别在于它是长生命周期业务。一个审批实例可能存活几天甚至几周。这期间服务可能重启、断电、升级流程状态必须能够无损恢复。所以持久化不是辅助功能而是核心功能。我们的持久化结构主要三张表表名说明关键字段WorkflowInstance流程实例主表InstanceId、WorkflowId、Version、CurrentNodeId、Status、PayloadJsonWorkflowHistory节点流转历史Id、InstanceId、FromNode、ToNode、Operator、Action、TimestampWorkflowTimer超时与延时任务表Id、InstanceId、DueTime、CallbackType、Status初始化实例时一次性写入实例主表流转时先写历史、再更新主表。这两个操作放在同一个数据库事务里保证不会出现“历史记录说走了主表还在原地”的数据不一致。并发这块直接上一个硬结论同一流程实例的并发流转必须用行锁或乐观锁拦住。我们采用乐观锁方案在WorkflowInstance表上加了RowVersion字段每次更新时校验版本号var rows db.WorkflowInstances .Where(w w.InstanceId instanceId w.RowVersion expectedVersion) .ExecuteUpdate(w w .SetProperty(a a.CurrentNodeId, nextNodeId) .SetProperty(a a.RowVersion, a a.RowVersion 1)); if (rows 0) throw new ConcurrencyException($流程实例 {instanceId} 已被其他线程修改);这里ExecuteUpdate是EF Core 7的原子写操作。它直接在数据库层完成条件更新避免了先查后改的竞态窗口。为什么不用数据库悲观锁因为工作流引擎里大部分实例都是空闲的等待审批人处理悲观锁会让这条数据在整个处理周期内被锁住拖垮数据库并发能力。2.4 引擎协调器驱动整个流转的“心脏”有了定义、有了状态机、有了持久化还差一个把所有部件串起来的东西。我们内部叫它引擎协调器职责很简单拿一个待执行的流程实例驱动它不断执行直到遇到阻塞节点或者流程结束。public sealed class WorkflowEngine { private readonly IDefinitionCache _cache; private readonly IWorkflowRepository _repository; private readonly INotificationService _notifier; public async TaskWorkflowResult ExecuteAsync( string instanceId, string currentNodeId, WorkflowInput input) { // 1. 加载实例 var instance await _repository.GetInstanceAsync(instanceId); // 2. 创建上下文 var ctx new WorkflowContext(instance, input); // 3. 循环推进直到碰到需要人工介入的节点或结束节点 var node _cache.GetDefinition(instance.WorkflowId, instance.Version) .GetNode(currentNodeId); var visitedNodes new Liststring(); while (node ! null node.Type ! NodeType.Approve) { node Step(ctx, node.NodeId); visitedNodes.Add(node?.NodeId ?? string.Empty); // 防止死循环连续执行超过 100 个自动节点时熔断 if (visitedNodes.Count 100) throw new InvalidOperationException(流程出现循环或自动节点过多); await _repository.SaveHistoryAsync(instance, node); } // 4. 如果卡在审批节点发通知 if (node?.Type NodeType.Approve) { await _notifier.NotifyApproverAsync(instance, node); } return new WorkflowResult(instance.Status, node?.NodeId, visitedNodes); } }这个类的设计妙处在于它把“自动节点”和“人工节点”的处理完全分离开来。自动节点条件判断、消息推送、数据回写可以一口气跑完直到遇到审批节点停下来等待人工。这就解释了为什么自动化能把效率拉起来——一条链路里七八个自动节点一个循环全跑完完全不耗人时。防死循环那一行也是血泪教训。最初我们没有这个熔断机制有一次线上流程定义配错了一个条件判断节点指回自己导致引擎无限循环直接把数据库读写放大到异常。后来加了这个100节点的上限再配合告警日志这类问题就能在造成事故前及时发现。3. 高性能路径从100个手动审批到10万TPS的实战调优3.1 第一步从同步阻塞到异步事件流性能优化的第一步永远不是加机器而是找到CPU和IO都在等的场景。原型的第一个版本审批动作是同步调用的引擎启动一个线程调数据库查询查完调动作动作里如果有外部接口就继续等响应整个流程是串行阻塞模型。这种模式的直接后果是高峰期一百个审批同时进来线程池被挤得满满当当事件循环卡死系统响应时间飙到几秒。把同步改成异步整体过程是痛苦的但收益巨大。关键在于理解async/await的本质是让出线程而不是把同步代码包一层async就完事。正确的做法是这样的public async TaskWorkflowResult HandleSubmittedAsync(string instanceId, int userId) { var instance await _repository.GetInstanceAsync(instanceId); var ctx new WorkflowContext(instance) { OperatorId userId, StartedAt DateTime.UtcNow }; // 这里完全是非阻塞的IO等待时线程自动让出 var node await StepAsync(ctx, instance.CurrentNodeId); await _repository.SaveInstanceAsync(instance); await _notifier.SendNotificationAsync(ctx, node); return new WorkflowResult(instance.Status, node?.NodeId); }改造完成后单从线程利用率上看同一个线程单位时间能处理的请求量就翻了几倍。这只是性能优化的第一层后面还有更狠的。3.2 第二步把热点数据全部打进内存缓存工作流的流量特征和普通业务系统不太一样流程定义的读频率极高但写频率极低。一个流程定义上线之后可能几个月都不变但每天有成千上万个实例在按照它的定义流转。我们一开始傻乎乎地每次都从数据库里查流程定义结果数据库的读IO在节点流转时成了瓶颈。后来加了一个内存缓存层用ConcurrentDictionary做定义缓存public sealed class InMemoryDefinitionCache : IDefinitionCache { private readonly ConcurrentDictionarystring, WorkflowDefinition _cache new(); public WorkflowDefinition GetDefinition(string workflowId, string version) { var key ${workflowId}:{version}; return _cache.GetOrAdd(key, k LoadFromDatabase(workflowId, version)); } public void Invalidate(string workflowId, string version) { var key ${workflowId}:{version}; _cache.TryRemove(key, out _); } }这个优化立竿见影。流程定义的读取从一次数据库查询变成一次字典查找耗时从毫秒级降为微秒级。而且ConcurrentDictionary的并发读性能极高完全能满足10万TPS场景下的定义读取需求。除了流程定义审批人列表、部门层级、常用通知模板也是热点数据我们统一做了缓存处理。缓存失效策略很简单管理员在后台修改定义后手动调用Invalidate方法清掉对应key不给缓存留下长期脏数据的机会。3.3 第三步批量提交与缓冲队列把数据库读写压缩到极致TPS要想真正冲到10万还有一个绕不开的坎数据库写入。即便用最快的方式写库单条事务的提交起码也要几十微秒到数百微秒10万TPS意味着每秒要执行10万次数据库写入这在单机数据库上基本不可能。解决思路是拉长批量的时间窗口。我们设计了一个内存缓冲队列引擎将节点流转结果先写入内存队列后台的批量提交任务每隔100毫秒或者累积到1000条记录才统一执行批量插入public sealed class BatchPersistenceService : BackgroundService { private readonly ChannelWorkflowEvent _queue Channel.CreateBoundedWorkflowEvent( new BoundedChannelOptions(50000) { SingleWriter false, SingleReader true }); protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var buffer new ListWorkflowEvent(1000); while (!stoppingToken.IsCancellationRequested) { buffer.Clear(); // 100ms 超时窗口内尽可能多地收集事件 using var cts CancellationTokenSource.CreateLinkedTokenSource(stoppingToken); cts.CancelAfter(TimeSpan.FromMilliseconds(100)); try { while (buffer.Count 1000) { var item await _queue.Reader.ReadAsync(cts.Token); buffer.Add(item); } } catch (OperationCanceledException) { // 时间窗口到执行批量写 } if (buffer.Count 0) await BulkInsertAsync(buffer); } } private Task BulkInsertAsync(ListWorkflowEvent events) { var histories events.Select(e new WorkflowHistory { InstanceId e.InstanceId, FromNode e.FromNode, ToNode e.ToNode, Operator e.Operator, Action e.Action, Timestamp e.Timestamp }).ToList(); return _repository.BulkInsertHistoryAsync(histories); } }代码里用了ChannelT作为生产者消费者队列这是.NET内置的高性能内存队列吞吐量比普通ConcurrentQueue高出几个量级。批量插入用的是EF Core的SqlBulkCopy扩展一次可以插入上千条历史记录。这里要强调一点批量写入必须考虑故障恢复。内存缓冲意味着事件还没有落库一旦进程崩溃数据就丢了。我们的补偿方案是批量插入完成后同时记录一个批量事件的聚合日志并给主表打上最后一次实例状态的快照。如果崩溃发生重启时可以从快照恢复实例状态丢失的中间历史事件通过重放原始请求重新生成。这个机制我们内部叫“半异步持久化”主实例状态同步写库历史流水异步批量落库。3.4 性能压测结果与热点瓶颈复盘优化完成以后我们做了一轮完整的压测。测试环境是8核16G云主机SQL Server 2019标准版压测工具用NBomber开了一个协程压力客户端。场景是一个三节点的直连流程启动节点、审批节点、结束节点。优化阶段平均延迟P50P99延迟TPS同步阻塞版420ms1.3s850异步改造后45ms180ms6200加缓存后12ms40ms21000批量持久化后3ms15ms102400可以看到瓶颈是一层一层被削掉的。同步改成异步解决的是线程池耗尽问题加缓存解决的是IO次数过多问题批量写库解决的是数据库写入吞吐问题。每一步的收益都清晰可见。有一个数据很有意思最终瓶颈不再是数据库或引擎本身而是网络包处理。压测机器上的网卡带宽跑满了单机吞吐上不去了。这时候想再往上提TPS就需要横向扩展多台实例配合负载均衡来做集群部署。4. 自动化落地的集成与扩展4.1 把引擎嵌入业务系统的三种方式引擎写完只是第一步怎么和现有业务系统无缝集成才是真正的高频问题。我们实际尝试过三种方案各有各的适用场景。第一种是引入NuGet包直接嵌入业务进程。这种方案最简单业务流程代码直接在同一个进程里调用引擎没有网络开销排错也直观。缺点是引擎和业务系统强耦合引擎版本升级会影响整个业务系统。第二种是独立部署为微服务通过gRPC对外提供流程服务。这个方案的好处是业务系统只需要关心提交指令和接收回调不接触引擎内部的任何代码。缺点是跨进程调用的序列化开销大对分布式事务的要求也随之提高。第三种是事件驱动集成推荐这种模式。引擎只负责状态的流转不直接调用业务动作。节点动作改由事件订阅方来实现事件订阅方动作流程启动订单服务生成订单状态快照审批通过财务服务创建付款单审批驳回通知服务发送驳回通知邮件超时定时任务重新提醒审批人事件驱动的好处是引擎不需要知道业务代码在哪里、长什么样它只管发出领域事件。订阅方自己决定如何处理。相当于引擎从“指挥家”变成了“信号灯”业务逻辑真正分散到各个服务里自治。4.2 多租户与动态审批路由企业级需求逃不开多租户问题。不同部门、不同业务线的审批规则可能完全不同。为此我们在流程定义里增加了一个上下文路由机制不通过硬编码的if-else而是通过一个可插拔的路由策略接口public interface IRoutingStrategy { string ResolveNode(WorkflowContext ctx); } public sealed class AmountRoutingStrategy : IRoutingStrategy { public string ResolveNode(WorkflowContext ctx) { var amount ctx.Input.Getdecimal(Amount); if (amount 50000) return gm_approve; // 50万以上走总经理审批 if (amount 10000) return dir_approve; // 1万以上走总监审批 return dept_approve; // 默认部门经理审批 } }这个接口非常干净。新增加一种路由维度比如按区域、按部门、按风险等级只需要实现一个接口并在配置里声明启用哪个策略就行不需要改动引擎核心代码。多租户的实际做法是每个租户一套自己的流程定义版本定义缓存里用租户ID加上WFID做key。这样就不会出现“A部门的流程配置污染B部门”的情况。同时每个流程实例也要带上租户标识所有的查询和写入都走租户隔离。这是企业级系统的基本素养。4.3 监控与运维自动化引擎的“仪表盘”自动化之后最怕的就是自动化出问题你都不知道。我们上线之前就搭了一套监控体系现在回头想这绝对是最该提前做的事情。核心监控指标有四类流转量每秒节点流转数、流程实例创建数、审批节点阻塞数延迟节点流转P50/P95/P99延迟、审批节点停留时间错误引擎异常数、超时数、并发冲突数、批量写失败数资源线程池等待队列长度、内存缓冲队列水位、数据库连接池用量。日志统一采用结构化日志每条日志都带上公司内部要求的追踪ID格式方便查询链路时把整个流程执行过程拉起来看。告警策略是节点流转延迟P99超过200毫秒持续5分钟或者内存缓冲队列水位超过80%就立即告警到值班群。这里有个非常容易踩的坑监控系统本身不能成为性能瓶颈。我们之前用了纯日志库逐条打日志结果日志吞吐反而把CPU先打满了。后来改成批量写日志压缩采样才解决这个问题。监控的优先级永远是“诊断能力第一实时性第二”千万不要为了所谓的实时看板把系统拖垮。5. 上线五个月踩过的坑问题排查与应对策略5.1 高频问题速查表以下这些问题是我们实际上线过程中真实遇到过的也是面试时我问候选人的高频考题。整理成速查表发给大家问题出现原因解决方案同一流程实例状态错乱多个节点同时提交乐观锁失效校验RowVersion并抛出ConcurrencyException由调用方重试流程丢失批量未落库就崩溃主表状态同步写库历史流水异步批量落库审批卡死无响应节点没有超时机制所有审批节点默认配置24小时超时超时自动转交条件路由匹配异常多个节点Condition同时为true路由规则里加优先级字段取优先级最高的数据库死锁高并发批量写同一流程历史按InstanceId哈希同一实例路由到同一分区定义缓存脏数据后台改定义没清缓存所有定义修改API统一调用Invalidate方法线程池饥饿异步方法里写了Wait()导致线程阻塞全链路使用async/await消灭任何阻塞调用第一个问题是最折磨人的。上线初期因为多个审批人同时点了同意同一流程被并发执行导致状态跳变到错误节点。我们当时的处理是在仓储层加上乐观锁校验谁先提交谁成功后提交的人拿到异常前端弹窗提示重新刷新。尽管体验不太好至少数据不会乱。第二个问题的应对方式前面已经详细说过。核心原则只有一个流程数据不能丢。宁可多一次状态快照的成本也不要在崩溃恢复时无从下手。5.2 幂等性设计自动化引擎必须过的鬼门关工作流引擎的调用方可能是消息队列可能是定时任务也可能是人工点击。这三类调用方有一个共同特点都可能重复调用。消息队列至少一次投递定时任务可能多实例并发跑人工点击可能手抖点了两次。这就要求引擎的每个接口都必须幂等。我们当时的做法是引入一个全局唯一的RequestId每次提交指令时带上来。引擎执行前先去查指令流水表如果同一RequestId已经执行过直接返回之前的结果不再重复流转public async TaskWorkflowResult ExecuteIdempotentAsync(string requestId, ...) { var exists await _repository.IsRequestProcessedAsync(requestId); if (exists) { return new WorkflowResult(InstanceStatus.AlreadyProcessed, null, null); } try { var result await ExecuteAsync(...); await _repository.MarkRequestProcessedAsync(requestId); return result; } catch (Exception ex) { await _repository.MarkRequestFailedAsync(requestId, ex.ToString()); throw; } }有了这个幂等层之后我们才敢放心接入各类消息队列。因为无论队列重投多少次流程实例都不会被重复执行。这是自动化系统稳定运行的根基。5.3 超时与补偿机制的完整设计审批流程有三个需要注意的超时场景节点超时、外部系统调用超时、数据库操作超时。每个场景的应对策略不一样。节点超时是指审批人在某个节点停留时间过长我们的方案是每个审批节点默认挂一个Timeout属性后台有一个定时任务扫描所有状态为“审批中”的实例当当前时间超过节点的Timeout时限时自动触发超时回调public async Task CheckTimeoutAsync() { var expiredInstances await _repository.GetExpiredInstancesAsync(DateTime.UtcNow, 1000); foreach (var instance in expiredInstances) { var ctx new WorkflowContext(instance); var node _cache.GetDefinition(instance.WorkflowId, instance.Version) .GetNode(instance.CurrentNodeId); await _notifier.SendTimeoutAlertAsync(ctx, node); if (node.AutoEscalate) { var next _routing.ResolveEscalationNode(ctx); await MoveToNodeAsync(instance, next); } } }超时处理不是简单的催办就完事。我们的经验是提供两档升级策略第一档超时给当前审批人发提醒消息如果超过第二档时限自动转交给上一级领导。这才真正解决了审批阻塞问题。外部系统调用超时则比较简单所有下游调用必须在设定的时间内给出响应否则引擎默认该节点处理失败抛出异常回滚到上一个稳定状态。这里不要偷偷吞掉异常否则业务就在你毫无察觉的情况下中断了。宁可让调用方感知也不要让流程默默卡死。5.4 给开发者的几个善意提醒最后分享几点我踩了无数坑以后总结出来的经验这些比代码本身更值钱。第一先跑通单机再考虑分布式。很多团队一上来就想搞分布式工作流又是Temporal又是Saga结果基础的单机流程都没跑顺畅。真正的性能提升来自优化单机的资源利用率和代码质量分布式带来的复杂度是你难以想象的。我们做到10万TPS时其实还是单机架构。第二流程定义必须有版本管理。流程上线后不可避免要改版如果老流程实例仍然使用旧定义新提交的实例却要按新定义走你就必须给定义加版本号并制定切换策略。我们在定义表增加了版本号字段支持同ID多版本共存切换版本时只影响新实例。这个看似简单却是避免“改一个流程全部崩盘”的关键。第三不要过度设计。我们的引擎核心代码只有不到两万行相比Flowable动辄几十万行的代码量反而更容易维护。工作流引擎的本质就是“状态管理 路由策略 事件通知”你只要抓住这三条主线其他功能都可以根据需要逐步加。第四自动化必须配可观测性。自动化程度越高出问题时越不容易定位。我们上线初期的最大教训就是没有配套的监控和链路追踪一旦流程出问题只能靠手工查数据库翻历史表效率极低。后来补上了全链路日志和追踪ID排查问题的效率提升了至少五倍。
返回列表