news 2026/9/14 3:00:32

TigerBeetle 限流实践:用漏桶算法与两阶段转账实现请求速率、带宽与转账金额限流

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TigerBeetle 限流实践:用漏桶算法与两阶段转账实现请求速率、带宽与转账金额限流

TigerBeetle 限流实践:用漏桶算法与两阶段转账实现请求速率、带宽与转账金额限流

【免费下载链接】tigerbeetleThe financial transactions database designed for mission critical safety and performance.项目地址: https://gitcode.com/GitHub_Trending/ti/tigerbeetle

TigerBeetle 的核心数据模型是账户(Account)、转账(Transfer)与账本(Ledger),但这套复式记账模型不仅能记录资金,也能用来"计量"任何可量化的资源。本文基于 限流配方文档,完整讲解如何用漏桶(leaky bucket)算法的思想,借助带超时(timeout)的挂起转账(pending transfer)与账户余额约束,在 TigerBeetle 上实现请求速率限流、带宽限流和转账金额限流,并深入到 src/state_machine.zig 的校验与过期清理源码,说明其背后的确定性与悲观校验原理。读完本文,你将掌握一套不需要外部限流中间件、天然具备审计与幂等保证的限流建模方案。

为什么可以用 TigerBeetle 做限流

限流的本质是"配额"的分配与回收:在每个时间窗口内,用户能消费多少资源,消费后配额减少,窗口结束后配额恢复。这恰好对应复式记账中的两个动作:

  • 配额发放 → 贷记(credit)用户账户
  • 配额消耗 → 借记(debit)用户账户

TigerBeetle 可以"为非金融资源记账"(见 recipes 目录),本配方即用这一思想实现基于用户请求速率、带宽和金额的限流。它的核心优势在于:

  • 确定性:所有判定都由 TigerBeetle 集群在提交时完成,不存在分布式系统中常见的竞态窗口;
  • 可审计:每次配额消耗都是一条不可变的转账记录,天然形成审计轨迹;
  • 幂等:转账 ID 由客户端指定,配合幂等提交语义可以安全重试(见 可靠事务提交)。

核心机制:配额账户 + 自动过期的挂起转账

整个方案的机制非常简单,分为四步:

  1. 按资源建账本:每一种要限流的资源(请求速率、带宽、金额)各建一个专属账本(ledger)。账本用来隔离不同类型的资源,避免"请求次数"与"字节数"混淆(账本定义见 Account.ledger)。
  2. 建账户对:每个账本上有一个运营方(Operator)账户和每个用户一个账户。
  3. 加余额约束:给每个用户账户设置debits_must_not_exceed_credits标志,使其借记总额不能超过贷记总额,即"已消费 + 已挂起 ≤ 已发放配额"。
  4. 发放配额 + 挂起消耗:先把资源限额一次性贷记给用户;随后每个请求创建一个指向运营方的 挂起转账(pending transfer) 并带上 超时(timeout)。永远不 post 也不 void 这些转账,让它们超时自动过期

为什么选择"让挂起转账过期"

挂起转账会将其金额计入账户的debits_pending/credits_pending(预留金额),而不改动debits_posted/credits_posted(已入账金额),详见 两阶段转账。其效果是:

  • 发起请求 → 创建挂起转账 → 用户的可用余额被"扣减"(计入 pending);
  • 超时到期 → TigerBeetle 自动把挂起金额归还到原账户,配额恢复;
  • 配额用尽 → 新的挂起转账因debits_must_not_exceed_credits约束被拒绝,create_transfers返回exceeds_credits

整个过程不需要应用侧做任何回收工作——回收由集群在提交时间戳推进到到期时刻时自动完成。

关键:挂起转账在校验时就是悲观的

与"先通过、post 时再失败"的乐观方案不同,TigerBeetle 对带余额约束账户的挂起转账做悲观校验debits_must_not_exceed_credits的判定在挂起转账创建时就执行(见 Account 标志说明 中account.debits_pending + account.debits_posted + transfer.amount > account.credits_posted的判定式)。因此配额超限的请求在提交时立刻失败,绝不会出现"先扣了、到 post 时才发现超限"的情况。

请求速率限流:每用户每分钟 10 次

假设我们要把每个用户限制为每分钟 10 次请求。

账户设置:为 "Request Rate" 资源建立一个账本,其中运营方账户不设约束,用户账户设置debits_must_not_exceed_credits

LedgerAccountFlags
Request RateOperator0
Request RateUserdebits_must_not_exceed_credits

初始化:先从运营方向用户转账 10 个配额单位:

TransferLedgerDebit AccountCredit AccountAmount
1Request RateOperatorUser10

每个请求:创建一个金额为 1、超时为 60 秒的挂起转账,方向是用户 → 运营方:

TransferLedgerDebit AccountCredit AccountAmountTimeoutFlags
2...NRequest RateUserOperator160pending

注意这里超时取 60(秒),正是因为我们想要的窗口是"每分钟"。超时是以秒为单位的相对间隔而非绝对时间戳,这一设计对集群与应用之间的时钟偏差更稳健(见 TigerBeetle 的时间模型 与 Transfer.timeout)。

就这么简单:每个挂起转账"预占"用户的一部分配额,超时后自动归还。用户在窗口内发起第 11 个请求时,因为 10 个配额单位全部被挂起,新转账会被拒绝;60 秒后挂起金额逐批归还,配额恢复,限流自动"漏"掉。

带宽限流:每用户每分钟 10 MB

基于带宽限流与基于请求速率限流的差别只在于金额的含义:不再用每次请求记 1 单位,而是用请求的实际大小作为转账金额。

假设要把每个用户限制为每分钟 10 MB(10,000,000 字节):

账户设置与请求速率场景一致,只是换一个账本:

LedgerAccountFlags
BandwidthOperator0
BandwidthUserdebits_must_not_exceed_credits

初始化:从运营方向用户转账 10,000,000 个单位(这里单位就是字节):

TransferLedgerDebit AccountCredit AccountAmount
1BandwidthOperatorUser10000000

每个请求:创建挂起转账,金额等于本次请求的大小,超时仍为 60 秒:

TransferLedgerDebit AccountCredit AccountAmountTimeoutFlags
2...NBandwidthUserOperatorRequest Size60pending

这里依然使用 60 秒超时,但你可以根据业务需要把它调整成任意时间窗口(例如按秒限流就调小,按小时限流就调大)。原理完全一致:一个 3 MB 的请求会挂起 3,000,000 个配额单位;当窗口内累计请求大小达到 10,000,000 字节时,后续请求会因为exceeds_credits被拒绝。

转账金额限流:每账户每天最多 1000 USD

前两种方案限的是"资源消耗",而有时我们需要限的是用户实际转账的金额——例如限制每个账户每天最多转出 1000 USD。这需要两个账本(限流账本 + 资金账本)和 链接事件(linked transfers) 配合,使"扣减配额"与"真实转账"要么同时成功、要么同时失败。

账户设置

LedgerAccountFlags
Rate LimitingOperator0
Rate LimitingUserdebits_must_not_exceed_credits
USDOperator0
USDUserdebits_must_not_exceed_credits

初始化:在 Rate Limiting 账本上从运营方向用户转账 1000:

TransferLedgerDebit AccountCredit AccountAmount
1Rate LimitingOperatorUser1000

每次用户发起转账:同时创建 2 个转账,并通过linked标志链在一起(|表示同时设置多个标志):

TransferLedgerDebit AccountCredit AccountAmountTimeoutFlags(\|表示同时设置多个标志)
2NRate LimitingUserOperatorTransfer Amount86400pending|linked
2N + 1USDUserDestinationTransfer Amount00

这里超时取 86400 秒,因为这是一天的秒数。

链路语义:根据 链接事件,flags.linked会把当前事件的结果与下一个事件绑定,整条链要么全部成功、要么全部失败,且链内事件按顺序执行、出错即回滚。因此:

  • 第 2N 条挂起转账在 Rate Limiting 账本上"消耗"配额(限额判定的关键一步);
  • 第 2N + 1 条是 USD 账本上的真实转账;
  • 如果用户当天已经转出了太多金额,第 2N 条会因exceeds_credits失败,链接的第 2N + 1 条真实转账也随之失败(返回linked_event_failed)。

需要注意:链中最后一个事件不能带linked标志,否则会返回linked_event_chain_open错误。另外,链中若存在多组链接,各组可以独立成败。

源码视角:限流判定与自动过期如何落地

校验发生在挂起转账创建时

账户余额约束的实际判定位于 src/state_machine.zig 的create_transfer执行路径:

if (dr_account.debits_exceed_credits(amount_actual)) return .exceeds_credits; if (cr_account.credits_exceed_debits(amount_actual)) return .exceeds_debits;

debits_exceed_creditsdebits_pending + debits_posted + amount > credits_posted的封装,其含义与 Account 标志文档 完全一致。紧接着(L3927-L3934),挂起转账会把金额计入账户的debits_pending/credits_pending,从而"冻结"这部分配额:

if (t.flags.pending) { dr_account_new.debits_pending += amount_actual; cr_account_new.credits_pending += amount_actual; ... }

也就是说:限流拒绝在提交时刻即生效,且是集群内确定性串行化的判定,多个并发请求之间不存在"同时通过再超卖"的窗口。

过期金额由集群自动归还

挂起转账的过期不是应用侧轮询的结果,而是由状态机在提交时间戳推进到到期时刻时触发。在 src/state_machine.zig 的execute_expire_pending_transfers中,TigerBeetle 找出所有到期未决的挂起转账,并把金额从debits_pending/credits_pending中减去:

const expires_at = p.timestamp + p.timeout_ns(); assert(expires_at <= timestamp_event); ... dr_account_new.debits_pending -= p.amount; cr_account_new.credits_pending -= p.amount;

关于过期语义,有几点需要结合 Transfer.timeout 的文档说明理解:

  • 转账在timestamp + timeout时刻精确到期,到期之前挂起余额绝不会被提前移除;
  • 已到期的挂起转账不能再被手动 post 或 void(会返回pending_transfer_expired);
  • 集群对过期清理是best-effort:不保证余额在到期那一刻被立即移除,客户端请求在短时间内仍可能观察到已到期但尚未清理的挂起余额。因此应用侧不应依赖"过期后余额立刻可用"的强实时性。

这正是限流配方选择"只挂起、不结算"方案的底层依据:无需任何后台任务,过期即自动回补配额。

落地要点与注意事项

金额语义与资源单位

TigerBeetle 的金额是128 位无符号整数。限流资源(请求次数、字节数、货币金额)都必须映射为整数单位:带宽场景直接把字节数作为金额;货币场景参照 小数金额与资产缩放 的建议,把最小有用单位映射为 1(例如 1000 USD 记作1000_00分或按你的资产缩放系数换算),避免浮点精度问题。

转账 ID 与幂等重试

  • 每个请求的挂起转账都要生成唯一的转账 ID(参考 数据建模中的 ID 建议,如使用时间基 ID)。同一请求重试时必须复用同一个 ID,create_transfers会返回exists而不是重复扣减配额;
  • 如果一条转账因瞬时错误(如exceeds_creditsdebit_account_not_found)失败,用相同 ID 重试会得到相同的失败结果id_already_failed机制,见 create_transfers 结果说明),这保证了"配额不足时反复重试不会绕过限流"。

配额发放与重置的建模

本文中的"初始转账"是给用户发放窗口配额。更复杂的场景(如定期重置配额、多档套餐、租户隔离)可以进一步参考 余额上下界配方、货币兑换配方 与 多借多贷转账配方 的组合用法;多个限流账本也可以放进同一次create_transfers批量请求中一次提交。

客户端接入

各语言客户端均提供了与账户/转账/链接事件对应的 API,可直接调用create_accountscreate_transfers实现上述流程,具体接口见:

  • .NET 客户端
  • Go 客户端
  • Java 客户端
  • Node.js 客户端
  • Python 客户端

更多客户端与示例可浏览 Clients 目录 及各子目录下的 samples。

小结

本文给出的三种限流方案共用同一套机制:按资源建账本 → 给用户账户加debits_must_not_exceed_credits约束 → 发放配额 → 每次消耗创建带 timeout 的挂起转账并让其自动过期。请求速率限流与带宽限流只差金额的语义,转账金额限流则通过两个账本加链接事件实现"配额扣减与真实转账原子绑定"。由于所有判定都发生在集群提交路径上(src/state_machine.zig),且过期回补由状态机自动执行(src/state_machine.zig),这套方案无需额外的限流组件,即可获得确定性、可审计、天然幂等的限流能力。更完整的账本与数据建模思路可继续阅读 数据建模 与 两阶段转账。

【免费下载链接】tigerbeetleThe financial transactions database designed for mission critical safety and performance.项目地址: https://gitcode.com/GitHub_Trending/ti/tigerbeetle

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 2:58:49

Madagascar下CGFWI全波形反演实践:从RSF到梯度更新

简介&#xff1a;面向地球物理勘探与地震数据处理研究者&#xff0c;这份资源聚焦Madagascar开源平台上的全波形反演&#xff08;Waveform Inversion&#xff09;&#xff0c;围绕初至波、多次反射与复杂波动信息&#xff0c;解决地下速度模型高精度反演问题。包内共24个文件&a…

作者头像 李华
网站建设 2026/9/14 2:58:08

PHP原生学生管理系统部署与CRUD实战指南

简介&#xff1a;这是一套基于PHP开发的轻量级学生信息管理系统源码&#xff0c;面向Web开发初学者与课程设计实践者&#xff0c;适用于高校计算机专业PHP入门实训、数据库应用开发练习及小型教务管理原型搭建。资源包含完整的前后端实现&#xff1a;后端以22个PHP文件构成MVC结…

作者头像 李华
网站建设 2026/9/14 2:55:53

OpenSEES单柱墩抗震分析:粘结滑移与捏缩效应建模

1. 项目概述在结构工程领域&#xff0c;单柱墩作为桥梁、建筑等结构的重要承重构件&#xff0c;其抗震性能研究一直备受关注。传统分析方法往往假设钢筋与混凝土完全粘结&#xff0c;忽略了实际工程中普遍存在的滑移现象。本项目基于OpenSEES平台&#xff0c;构建了考虑滑移粘接…

作者头像 李华
网站建设 2026/9/14 2:55:43

C#直发ZPL指令控制Zebra打印机:从demo到工程化排错指南

简介&#xff1a;面向需要对接Zebra打印机的开发人员&#xff0c;这套演示项目压缩包提供了完整的C#示例工程与使用说明&#xff0c;适合物流、仓储、零售等条码标签打印场景。包内含系统打印demo源码&#xff0c;ZebraUnity与Form1两个核心代码文件展示了关键打印交互逻辑&…

作者头像 李华
网站建设 2026/9/14 2:55:38

llama2.c 如何导出 Code Llama 并指定自定义 tokenizer 完成推理

llama2.c 如何导出 Code Llama 并指定自定义 tokenizer 完成推理 【免费下载链接】llama2.c Inference Llama 2 in one file of pure C 项目地址: https://gitcode.com/GitHub_Trending/ll/llama2.c 如果你已经拿到 Meta 发布的 Code Llama 7B 权重&#xff0c;想在本仓…

作者头像 李华
网站建设 2026/9/14 2:55:35

Python文件操作与异常处理实战指南

1. Python文件操作实战指南文件操作是Python编程中最基础也最重要的技能之一。让我们从最基础的打开文件开始&#xff0c;逐步深入到高级用法。1.1 文件打开模式详解Python的open()函数支持多种模式&#xff0c;每种模式都有其特定用途&#xff1a;# 基本读写模式 file open(e…

作者头像 李华