1. 为什么全局速率控制在分布式工作流里是个硬骨头
先聊点实际的。做后端开发的兄弟应该都有过这种经历:系统跑得好好的,突然某个上游服务开始报429限流,紧接着下游数据库连接被打满,然后整个任务链路像多米诺骨牌一样连环崩掉。单体架构里我们会准备一个并发控制组件、一个令牌桶、或者干脆用一个分布式锁,但这些手段放到分布式工作流引擎的场景里,十分力只能使出三分。
我最早是在一个跑批系统里遇到这个问题的。每天凌晨一堆定时任务被调度起来,这些任务彼此之间有依赖关系,有的还需要调用外部接口。外部接口的阈值是固定的——比如每分钟最多600次调用——但工作流引擎只负责帮你编排任务的顺序和重试逻辑,它压根不知道"你的外部接口每秒能扛多少请求"。结果就是:只要任务一多,引擎发出去的并发请求数立刻超过外部服务的承受能力,对方开始拒绝服务,然后我们这边触发了大量重试,重试又加剧了拥堵,最终把整个集群拖垮。
后来我转到了Temporal的框架下做新系统,绕了一圈之后发现Temporal本身有不错的并发控制和速率限制能力。但它的默认机制更多是站在"单Worker"的角度去限制——每个Worker进程自己算自己每秒处理多少任务。问题在于:当你部署了十个Worker节点时,单节点的限流值叠加起来,对下游服务来说依然是十倍的冲击。这时候就需要一个全局视角的速率控制方案,让整个集群共享同一个速率阈值。
我这次做的"Temporal实现全局速率控制方案",核心思路就是用Temporal自身的能力,把速率控制从"每个Worker自己约束自己"升级为"所有Worker一起遵守同一个约定"。它适合的场景包括:
- 多Worker节点部署,下游依赖外部API或数据库等有固定吞吐上限的服务
- 需要在不同队列、不同任务类型之间共享同一个速率预算
- 希望限流逻辑不依赖额外基础设施(比如Redis),完全跑在Temporal职责边界内
- 想让速率配置能动态调整,不用重启Worker进程
这篇文章我先讲清楚方案的整体架构和两个关键实现路径,属于系列的第一篇。后续会展开细节坑点和进阶调优。
2. 先搞清楚Temporal限流机制的边界
在动手设计之前,我花了不少时间翻Temporal源码和官方文档,想弄明白它的速率控制到底管到哪一层。不把这个问题搞清楚,后面所有方案都会是空中楼阁。
2.1 Temporal自带的限流管的是什么
Temporal的默认限流分成几个层次,工作的时候它们各管各的:
- Task Queue级别的速率限制:挂在某个Task Queue上的,比如
MaxConcurrentWorkflowTaskExecutionSize、MaxConcurrentActivityTaskExecutionSize,这些参数控制的是单个Worker针对某个Queue的活动任务并发数。 - Worker进程级别的限流:在创建Worker时设置的
MaxConcurrentActivityExecutionSize、WorkerOptions里的速率限制项,作用于当前进程内的所有任务。 - 服务端本身的限流:Temporal Server端也会根据自己的承受能力做限制,这个通常不需要我们操心,我们关心的是客户端侧的控制。
实际跑起来你会发现,这些参数解决的是"单机并发天花板"问题,而不是"全局速率预算"问题。举个例子:MaxConcurrentActivityExecutionSize = 50意味着每个Worker最多同时运行50个Activity。但你部署5个Worker,每个Worker都允许50并发,下游看到的其实是250并发。外部服务可不管你是不是分布式架构,它只会觉得你在疯狂打它。
2.2 单机限流方案为什么在集群环境失效
我用一个类比来解释这个问题。想象一个收费站,Temporal的每个Worker就是一条收费通道。收费站规定"每条通道每分钟最多放行30辆车"——这就是单Worker限流。可是如果你修了10条通道,每分钟实际放行的车就是300辆。下游高速路主干道的设计容量只有100辆/分钟,那么拥堵和事故就成了必然。
真正需要的方案,是让所有通道共享同一个"总账本":不管你是哪条通道,整个收费站每分钟合计放行不能超过100辆。这就意味着限流的状态信息必须对所有Worker可见,而不是存在某个Worker自己的内存里。
Temporal体系里适合承载这种"全局可见状态"的东西有好几个,常见的做法包括:
- 利用Temporal的MutableSideEffect或LocalActivity读写某个全局存根
- 用一个长期运行的Workflow充当协调者,持有令牌桶状态,对外提供查询和信号接口
- 把限流状态放在数据库里,但这样就引入了外部依赖
我们这次的目标是"不引入Redis等额外中间件,纯靠Temporal自身能力实现全局限流",所以重点放在前两种路径上。下面我会把这两种方案的核心设计拆开来讲。
3. 方案A:单一协调者Workflow + 令牌桶状态机
第一个思路很直白:让整个Temporal集群里只存在一个特殊的"限流协调者Workflow",它持有全局令牌桶的全部状态。所有Worker在处理Activity之前,先通过信号/Signal或查询/Query机制从这个协调者那里"借"令牌,借到了才继续往下跑,借不到就等待。
3.1 为什么选单一协调者而不是多个协调者
你可以把这个协调者理解成一个大楼的物业总闸。整栋楼的用电预算只有一个总闸在管,各楼层的分电表都听从它的调配。如果每个楼层各自装一个总闸,那和单机限流没有本质区别——协调者一多,状态就碎片化了,还得处理协调者之间的同步问题,复杂度指数级上升。
在Temporal里,一个Workflow实例天然具备"单点执行"的特性:同一个Workflow ID在同一时间只会有一个实例在推进。这正是全局状态收敛的地方。我们只需要保证所有Worker都调用同一个Workflow的Query/Signal接口,状态就天然一致。
3.2 协调者Workflow的核心结构
先看代码骨架,我用Java SDK做示例,因为Temporal Java SDK在这块的API最直观:
@WorkflowInterface public interface RateLimiterWorkflow { @WorkflowMethod void run(); @QueryMethod RateLimitStatus getStatus(); @SignalMethod void applyLease(String clientId, int permits); }实现类:
public class RateLimiterWorkflowImpl implements RateLimiterWorkflow { private final RateLimiterState state = new RateLimiterState(); @Override public void run() { // 长期运行,靠BlockedSignal等待信号 Workflow.await(() -> state.isShutdown()); } @Override public RateLimitStatus getStatus() { return state.snapshot(); } @Override public void applyLease(String clientId, int permits) { state.tryAcquire(clientId, permits); } }这个run()方法是整个协调者的心脏。它不做任何重活,就靠Workflow.await挂在那里,等待信号驱动状态变更。为什么这么设计?因为Temporal的Workflow是有状态机快照机制的,await会让当前线程彻底挂起,状态被持久化,不会消耗计算资源。
然后是状态机部分。这个RateLimiterState最关键的是要有令牌桶算法:
public class RateLimiterState { private final Queue<Long> tokenBucket = new ArrayDeque<>(); public synchronized boolean tryAcquire(String clientId, int permits) { long now = Workflow.currentTimeMillis(); // 清理过期令牌 while (!tokenBucket.isEmpty() && now - tokenBucket.peek() >= REFILL_WINDOW_MILLIS) { tokenBucket.poll(); } long validTokens = tokenBucket.size(); if (validTokens + permits <= MAX_TOKENS) { for (int i = 0; i < permits; i++) { tokenBucket.add(now); } return true; } return false; } }这个实现是"滑动窗口+令牌数量上限"的混合体,相比传统每秒固定补充N个令牌的令牌桶,它更适合协调者模式的粒度控制。
3.3 Worker端如何与协调者交互
Worker端要做的事情很清晰:每处理一个Activity之前,先查询/申请令牌。这里有两种交互粒度,我建议组合使用:
第一种:同步查询令牌
RateLimitStatus status = WorkflowStub.fromTyped(workflow).getStatus();这种方式适合做"软检查"——比如在启动批量任务前先看一眼当前预算够不够,不够就直接抛业务异常,避免无谓的任务堆积。
第二种:信号申请令牌
WorkflowStub.fromTyped(workflow).signal("applyLease", clientId, permits);信号方式适合"硬申请"。但注意,Temporal的信号是异步的——发送方发出的信号不会立刻收到接收方的确认回复,所以不能把Signal当作同步的令牌领取过程。
这就引出一个很关键的实际操作技巧:让Worker先发Signal,然后在协调者Workflow里维护一个"待下发配额"事件,由Worker在自己的Activity里主动查询清算结果。别嫌绕,这是绕开Temporal Signal异步限制的通用手法。
具体流程如下:
- Worker准备执行某类任务,向协调者发送
applyLease信号,附带需要的令牌数。 - 协调者收到信号,在状态机里尝试扣减令牌,然后把扣减结果记录到一个"配额结果列表"里。
- Worker在启动Activity的代码块里,轮询查询协调者的
getStatus(),或者用一个本地缓冲区做异步回调等待。 - 如果拿到配额,执行Activity;如果没拿到,就抛
ApplicationFailure触发Temporal重试策略,等下一个周期再试。
3.4 这套方案的硬伤与取舍
先说我实际踩过的坑。单一协调者方案最大的问题有两个:
问题一:查询频率会成为性能瓶颈。如果每个Activity都同步查一次协调者Workflow,高并发下Query调用本身会有一层序列化和网络开销。我实测过,单个协调者Workflow大概能扛住每秒几百次查询,再往上就开始出现响应延迟波动。对于大多数业务系统这个量级够用,但如果你的目标是每秒上千任务的吞吐,这套方案撑不住。
问题二:协调者是长期运行的Workflow,意味着它会占用一个Workflow执行的历史记录。你每发一次Signal,都会在History里追加一个事件。量一大,History膨胀会拖慢该Workflow的恢复速度。这不是致命的,但要注意隔一段时间主动让协调者重置一次状态,或者定时归档历史。
所以我给的取舍建议是:协调者方案适合"低频、强一致"的限流场景,比如每小时最多跑100个批处理任务、每个任务最多并发10个文件传输。如果你要的是"毫秒级、每秒几千次"的高频限流,用第四部分的方案B更靠谱。
4. 方案B:本地缓冲 + 桶状态的分布化降级
如果说方案A是"中央集权",那方案B就是"地方自治+中央备案"。它的核心思路是:每个Worker本地维护一个小的令牌桶缓存,缓存容量由全局配置动态分配,定期和全局账本对齐。这样一来,大多数令牌申请都发生在本地内存里,不涉及跨Worker通信,性能上可以和单机限流持平。
4.1 为什么会想到分布化降级
还是在那个跑批系统里,我后来把协调者方案部署上去以后,刚开始还挺欣慰,至少下游不再被冲垮了。但很快发现一个新问题:业务的单批任务经常是500个Activity一起跑,每个Activity都要在几百毫秒内完成外部调用。协调者每秒钟处理的Query请求量直接冲到上千次,频繁的Signal事件也让那个协调者Workflow的历史记录越来越大,恢复时间从几十毫秒涨到了几秒。
那时候我才意识到,全局限流不能只看"全局"两个字,更重要的是怎么让全局状态和本地性能之间找到一个平衡点。分布式系统里的经典解法就是"缓存配额+定期同步"。
其实商业限流器的实现也有类似设计,比如一些API网关的令牌桶就是"每节点预领配额,配额耗尽后再向中央节点申请"。我觉得这个思路完全可以移植到Temporal上,于是就有了方案B。
4.2 本地桶缓存的数据结构
每个Worker节点维护一个本地令牌桶LocalBucket,它对外暴露和单机限流一样的tryAcquire()接口,但内部多了两个能力:
- 配额预领:启动时向全局协调者领一批"未来N秒内可用的配额"。
- 配额归还与补充:本地桶快用完时,向全局协调者申请下一批配额,同时把实际使用情况上报。
public class LocalBucket { private final int maxCapacity; private final int refillRatePerSecond; private int availableTokens; private long nextRefillTime; public LocalBucket(int maxCapacity, int refillRatePerSecond) { this.maxCapacity = maxCapacity; this.refillRatePerSecond = refillRatePerSecond; this.availableTokens = maxCapacity; this.nextRefillTime = System.currentTimeMillis() / 1000; } public boolean tryAcquire(int permits) { refillIfNeeded(); if (availableTokens >= permits) { availableTokens -= permits; return true; } return false; } private void refillIfNeeded() { long now = System.currentTimeMillis() / 1000; if (now >= nextRefillTime) { availableTokens = Math.min(maxCapacity, availableTokens + refillRatePerSecond); nextRefillTime = now + 1; } } }注意,这里我故意没有引入跨节点的同步锁。因为本地桶预领的配额本身就是一种"信用额度",即便出现两个Worker同时拿到配额且实际使用超过了总额度,也只是造成少数时刻的临时超限。对于大多数外部API来说,短暂的轻微超限是可以接受的,你只需要保证宏观速率均值控制在阈值以内。
4.3 全局账本与本地桶的同步协议
为了让全局账本不至于完全失控,我定义了三个同步操作:
leaseQuota(workerId, amount):向全局账本申请额度。reportUsage(workerId, actualUsed):上报实际使用量,用于账本扣减。revokeQuota(workerId):Worker闲置时主动归还多余额度。
同步协议的设计重点在于"什么时候触发申请,什么时候触发归还",这里我给你一个我实测调得还算顺的参数模板:
| 参数 | 推荐值 | 调整依据 |
|---|---|---|
| 预领周期 | 5秒 | 太长会导致配额浪费,太短会增加协调者压力 |
| 预领比例 | 全局配额 / Worker数 x 80% | 预留20%缓冲给突发 |
| 本地桶容量 | 预领周期的预计消耗量 | 保证至少覆盖一个周期 |
| 归还阈值 | 预领量剩余超过30%时 | 低于30%继续持有,避免抖动 |
每次预领时,全局协调者会做一次减法:全局剩余配额 -= 预领量。如果全局剩余配额不足,协调者可以拒绝或者部分批准,Worker端拿到部分批准之后就把本地桶容量下调,做好降级准备。
4.4 这个方案在Temporal里的落地实现
落地方式我还是用协调者Workflow,但交互逻辑变了:不再是每个Activity都去协调者那里查状态,而是Worker进程启动时批量预领,Activity运行时只查看本地内存。这样做的好处是:
- 高频路径零跨节点通信
- 低频路径(每5秒一次预领)负担极小
- 协调者Workflow的历史记录事件量骤降
具体代码层面,可以在Worker的WorkerFactory构造阶段注入一个GlobalRateLimiter客户端,它内部维护着LocalBucket和一个定时心跳任务。心跳任务每隔5秒向协调者Workflow发送信号,携带workerId和预领请求。协调者处理完预领后,把结果通过Query接口暴露给Worker下一次轮询读取。
这里有个绕不过去的异步等待问题,我当时的处理方法是:Worker固定用一个私有TaskQueue注册一个"配额回调" Activity。协调者把预领结果作为输入写入到该TaskQueue,回调Activity被触发后把结果填充进LocalBucket。这种方法虽然多了一个Activity调用,但完全避免了手写锁同步,而且Temporal的Activity重试机制还能自动处理协调者临时不可用的情况。
伪代码大概是:
@ActivityInterface public interface QuotaCallbackActivity { @ActivityMethod void applyResult(QuotaResult result); }Worker启动时调用:
QuotaRequest request = new QuotaRequest(workerId, preloadAmount); WorkflowStub rateLimiterStub = WorkflowStub.fromTyped(rateLimiterWorkflow); rateLimiterStub.signal("requestQuota", request); // 异步等待回调 QuotaResult result = Workflow.await( () -> quotaResultCache.containsKey(request.getRequestId()) ); localBucket.setCapacity(result.getAllocatedQuota());注意,上面的Workflow.await只能在Workflow上下文里用,所以这个预领动作会被设计成一个短命的QuotaPreloadWorkflow,而不是在普通Java代码里调用。
4.5 方案B的边界与注意事项
这套方案解决了我之前遇到的性能瓶颈,但也不是万能药。有几个点你必须心里有数:
首先,配额是"非强一致"的。两个Worker可能同时各自预领了80%的配额,瞬时全局总量达到160%,下游那几秒会看到一点超限。如果你下游的限流阈值是"严格不可超过",这可能是个问题。解决办法是压低预领比例,或者增大预领周期的粒度,让超限窗口缩短。
其次,Worker动态扩缩容时要处理配额的回收。Worker崩溃或者重启时,它之前预领的配额在全局账本上还挂着,不会自动释放。如果频繁扩缩容,全局剩余配额会越变越少。我建议在Worker进程关闭的钩子里主动发送一个revokeQuota信号,同时全局协调者维护一个lastSeenTime,对超过两倍预领周期未续约的Worker自动回收配额。这样即使Worker被强杀,也不会永久泄漏配额。
最后,这个方案对本地的时钟同步有隐性要求。本地桶的补充逻辑依赖System.currentTimeMillis(),如果某台Worker的时钟快了十几秒,它的本地桶就会比别的Worker更快恢复容量。在跨机房部署时这个影响会被放大,有条件的话可以用Temporal提供的Workflow.currentTimeMillis()来约束时间基准。
5. 动态调参:让速率控制可在运行时调整
方案做出来之后,另一个实际问题接踵而至:速率控制的阈值不可能永远恒定。比如下游服务方在凌晨会把API配额提高一倍,或者某天突发了运营活动,需要在不停机的情况下临时上调并发量。这要求限流方案的配置必须支持动态更新。
5.1 从配置文件到Workflow参数
第一版方案里,我把限流参数写死在了Worker的application.yml里。每次调整阈值都要经历"改配置文件 → 重新发布 → 逐个节点滚动重启 → 验证生效"这个流程。在集群规模小的时候还能忍,但节点一多,半小时就没了。
后来我把参数迁移到了Temporal的"动态配置"体系里。Temporal的Workflow可以通过DynamicValue查询一些运行时配置,不过更灵活的方式是把限流参数也封装成一个独立的配置Workflow,由它持有所有限流参数,并提供Query和Signal接口。
@WorkflowInterface public interface RateLimitConfigWorkflow { @WorkflowMethod void run(); @QueryMethod RateLimitConfig getConfig(); @SignalMethod void updateConfig(RateLimitConfig newConfig); }这样操作员就能通过Temporal的命令行工具或者管理页面,直接向这个配置Workflow发信号修改限流阈值。所有Worker的本地桶都会在下一个预领周期读取到新配置。
5.2 配置变更时的平滑过渡
我不建议在配置一变更就立刻让所有Worker同步到新值,原因很实在:假设当前全局限流是每分钟1000,你把它改成每分钟2000,立刻同步的话,每个Worker的本地桶都会瞬间翻倍,下游可能会经历一次2倍突发流量。某些质量较差的外部API对突发流量非常敏感,直接就拒了。
正确的做法是斜坡式过渡:
- 配置Workflow收到新值后,不直接写入生效状态,而是写入"目标值"字段。
- 每次Worker预领配额时,配置Workflow根据当前时间、过渡开始时间、过渡持续时间,计算出一个"当前生效的中间值"。
- 中间值平滑地从旧值爬到新值,预领周期越长,过渡越平滑。
实现上就是一段线性插值:
public int getEffectiveLimit() { long now = Workflow.currentTimeMillis(); if (now >= transitionEndTime) { return targetValue; } if (now <= transitionStartTime) { return sourceValue; } double progress = (now - transitionStartTime) / (double) (transitionEndTime - transitionStartTime); return (int) Math.round(sourceValue + (targetValue - sourceValue) * progress); }我实践中常用的过渡窗口是30秒到2分钟,具体取决于下游服务的容忍度。如果下游有明确文档说能承受多少突发,可以直接按那个文档来。
5.3 基于任务优先级的差异化限流
动态调参不仅能调全局总量,还能做"按任务类型分级配额"。这个需求最开始来自一个既有高优先级实时任务又有低优先级批量任务的系统。
实现方式不复杂:全局令牌桶被划分成几个子桶,每个子桶有独立的上限和补充速率。高优先级任务从高优先级子桶取令牌,低优先级任务从低优先级子桶取。当高优先级子桶不足时,可以"借用"低优先级子桶的配额;反过来低优先级任务永远不能借用高优先级配额。
这个"借用"逻辑我放在了协调者Workflow里:
public boolean tryAcquireWithPriority(PriorityLevel priority, int permits) { if (priority == PriorityLevel.HIGH) { boolean ok = highPriorityBucket.tryAcquire(permits); if (!ok) { // 额外借用低优先级剩余 ok = lowPriorityBucket.tryAcquire(permits); } return ok; } return lowPriorityBucket.tryAcquire(permits); }实际运营中,这个分级策略让我省了很多调优的精力。否则默认情况下,一个跑批量导数的任务可能会把当时正在处理的交易类任务全部堵在限流门外,引发线上投诉。
6. 实际运营中的告警与排障方法
方案上线之后,后面就是运维层面的持久战。这里分享几个我真实踩过的坑和对应的排障手段。
6.1 限流导致的"隐形饥饿"
最隐蔽的问题是:限流器正常工作,但部分任务的等待时间异常拉长。表面上看,令牌桶的容量是够的,全局速率也没有超过阈值,但某个任务种类总是拿不到配额。
排查链路是这样的:
- 先在Temporal的Web UI上查看某个被"饿死"的任务历史记录,确认它是否频繁遇到
ApplicationFailure且报错信息是配额不足。 - 检查协调者Workflow的Query返回结构,看看全局剩余配额和各子桶的持有情况。
- 如果全局剩余充足但某个Worker长期拿不到,大概率是预领比例过大或者预领不及时导致的——某个Worker把配额预领了但没用完也不归还,其他Worker就没得分了。
解决手段我推荐两条:一是缩短预领周期,让配额能更快地在节点间流转;二是设置一个"最低保留配额",全局协调者无论如何都保留20%~30%不预领出去,留给临时的新Worker或突发任务。
6.2 协调者Workflow卡在长History上
这是我之前提过的痛点,实际发生的时候确实让人抓狂。协调者Workflow每处理一个Signal,History里就会追加一个WorkflowExecutionSignaled事件。一旦跑了几天几万甚至十几万个事件,该Workflow的每次状态恢复都会变慢,最终表现为Worker的预领请求超时。
处理办法有三种,我按推荐顺序列出来:
- 定期归档:每隔一段时间,协调者Workflow主动完成当前执行,然后以同一个Workflow ID重新启动新的一轮执行。这样可以清空旧History。
- 减少Signal频率:把"每5秒预领一次"合并成"每30秒批量上报一次",牺牲一点实时性换取稳定性。
- 改用短命Workflow作为协调者:不维护一个长期运行的协调者,而是每次预领都开一个新的短命Workflow。这样虽然会有额外的启动开销,但完全规避了History膨胀问题。我之前提的
QuotaPreloadWorkflow就是这个思路的充分应用。
6.3 上下游耦合导致的"假限流"
还有一种情况会让排查特别迷惑:你明明把全局速率限制在了安全值以内,但下游还是报压力过大。这时候要检查的不只是Temporal这边的速率控制,还要看你的重试机制和超时设置。
如果某个Activity调用外部API超时时间是30秒,而外部API本身需要3秒响应,那同一个时间里堆积在途的Activity请求数可能远大于你通过限流器放行的数量。因为第一个请求还没结束,第二个请求已经被放行了。结果就是限流器放行了100个请求,但在途的实际负载可能已经达到200。
我当时修正的方式是在限流器计数上乘一个"在途因子":
public boolean tryAcquireForExternalCall(int outgoingCallTimeoutSeconds) { double factor = outgoingCallTimeoutSeconds / expectedResponseSeconds; int effectiveQuota = (int) Math.ceil(quota * factor); return localBucket.tryAcquire(effectiveQuota); }虽然这个做法牺牲了一些精度,但它能让限流器的行为更贴近下游真实的压力承受能力,而不是理想化地假设每个请求都能秒回。
7. 方案对比总结与选型建议
这篇文章介绍了两种全局速率控制方案,我在实际项目中都用过,各有明确的适用场景。简单做一个横向对比:
| 维度 | 方案A:单一协调者 | 方案B:本地缓冲+预领 |
|---|---|---|
| 一致性强度 | 强一致 | 最终一致(瞬时可能超限) |
| 性能上限 | 数百~一千次/秒查询 | 单机并发级,基本不受限 |
| 实现复杂度 | 较低 | 中等,需要处理预领协议 |
| 对协调者压力 | 高,每个Activity都来查 | 低,只有周期预领上报 |
| 适用场景 | 低频严格限流 | 高频吞吐型任务 |
| 动态调参能力 | 即时生效 | 下一个预领周期生效 |
如果让我给一个选型建议,我会这么划分:
- 如果你是首次接入全局限流,对Temporal的机制还不够熟,建议直接从方案A开始,代码少、逻辑直观、排障容易。把协调者Workflow写出来,配合Query和Signal,整个过程一两天就能完成。
- 如果你的任务吞吐量明确会超过每秒几百次,同时下游对瞬时超限有一定容忍度,果断选择方案B。它虽然要处理预领协议和配额回调,但换来的是长期稳定的性能表现。
- 如果你的场景同时需要强一致和高性能,那单靠Temporal自身能力是做不到的,得引入外部分布式限流中间件。这个我后续可能在系列文章里单独分析。
先写到这里。这个系列的第二篇我会重点聊"如何在不入侵业务代码的前提下,把限流逻辑做成可复用的Temporal组件",以及"Temporal限流的时序陷阱与测试方法",到时候见。