TigerBeetle Rust 客户端两阶段转账实战pending 预留与 post 结算全流程解析【免费下载链接】tigerbeetleThe financial transactions database designed for mission critical safety and performance.项目地址: https://gitcode.com/GitHub_Trending/ti/tigerbeetle本文以 TigerBeetle 仓库自带的 Rust 两阶段转账示例src/clients/rust/samples/two-phase为主线完整讲解如何在 Rust 应用中用pending预留与post_pending_transfer结算两个原生原语实现先冻结资金、再完成入账的两阶段转账流程。读完本文你将掌握该示例的完整代码走读、TigerBeetle 服务端的启动方式、两阶段转账的余额语义与状态机并能直接复用这套模式实现支付预授权、托管结算等业务场景。示例概览这个 sample 到底做了什么示例的官方定位见 Two-Phase Transfer Rust Sample创建两个账户在两者之间发起一笔 pending 转账然后再将这笔转账 posted结算。完整代码只有约 160 行位于 src/main.rs自始至终通过assert!/assert_eq!校验每一步的账户余额与转账标志位——因此它既是可运行的演示程序也是一份可读性极高的可执行文档。整个流程分六个阶段创建账户1和账户2从账户1向账户2发起一笔金额为500的pending转账查询两个账户验证余额只体现在debits_pending/credits_pending上创建第二笔转账用post_pending_transfer标志把第一笔转账结算查询两笔转账验证第一笔仍带pending标志、第二笔带post_pending_transfer标志再次查询账户验证金额已从 pending 转为 posted。前置条件与示例目录结构原文档明确的环境要求README.mdLinux 5.6 是唯一的生产环境支持目标为了开发便利同时支持 macOS 与 WindowsRust 1.68。示例目录只包含两个关键文件src/clients/rust/samples/two-phase/ ├── Cargo.toml # 依赖声明与发布配置 └── src/ └── main.rs # 完整示例代码Cargo.toml 的内容非常精简[package] name tigerbeetle-sample-two-phase version 0.1.0 edition 2021 [dependencies] tokio.version 1.38.1 tokio.features [rt-multi-thread] tigerbeetle.path ../.. [profile.release] overflow-checks true两点值得注意客户端以本地路径引用tigerbeetle.path ../..即直接依赖仓库根下的 src/clients/rust 客户端源码构建时会静态链接tb_client原生库该库为所有官方客户端共享具体机制见 lib.rs 顶部说明。开启溢出检查Cargo.toml 中的注释指出强烈建议所有使用 TigerBeetle 的 Rust 应用开启 overflow checks因为账务的溢出错误是灾难性的当前配置只对该 crate 的测试生效最终应用需要在自身Cargo.toml中同样开启overflow-checks true。搭建环境克隆仓库并定位到示例目录按原文档步骤先克隆仓库再进入示例目录$ git clone 仓库地址 $ cd tigerbeetle/src/clients/rust/samples/two-phase随后安装 TigerBeetle 客户端依赖cargo build或直接cargo run时自动完成。Rust 客户端本身是异步接口但不绑定特定运行时而是自带独立线程的事件循环本示例选择用tokio运行时驱动fn main() - Result(), Boxdyn std::error::Error { tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .unwrap() .block_on(main_async()) }启动 TigerBeetle 服务端示例代码要连上一个真实运行的 TigerBeetle 集群。仓库根 README.md 给出了在 Linux 上启动单副本集群的完整命令$ curl -Lo tigerbeetle.zip https://linux.tigerbeetle.com unzip tigerbeetle.zip $ ./tigerbeetle version $ ./tigerbeetle format --cluster0 --replica0 --replica-count1 --development 0_0.tigerbeetle $ ./tigerbeetle start --addresses3000 --development 0_0.tigerbeetle关键参数说明format --cluster0集群 ID 为0客户端连接时传入的 cluster_id 必须与之匹配--replica-count1单副本开发集群--development开发模式便于本地单机运行start --addresses3000监听3000端口。Windows 下可用zig/download.ps1、macOS 可参考 docs/operating/installing.md 的安装指引需要容器化部署可参考 docs/operating/deploying/docker.md。地址配置TB_ADDRESS如果你不是在默认的localhost:3000上运行服务端就设置环境变量TB_ADDRESS为服务端完整地址。示例代码的默认地址逻辑为let port std::env::var(TB_ADDRESS).unwrap_or_else(|_| 3000.to_string()); let client tb::Client::new(0, port)?;Rust 客户端支持的合法地址格式见 src/clients/rust/README.md传入值实际连接地址3000127.0.0.1:3000127.0.0.1:3000127.0.0.1:3000127.0.0.1127.0.0.1:3001默认端口 3001运行示例服务端就绪后在示例目录执行$ cargo run若所有assert全部通过程序正常退出无任何输出说明两阶段转账的每一步余额与标志位都符合预期任何一步断言失败都会panic并打印对应的账户/转账状态便于定位问题。代码走读六步两阶段转账全流程以下代码均出自 src/main.rs与官方 README 的 walkthrough 一一对应。第 1 步创建账户创建两个账户ID 分别为1和2同属ledger 1、code 1let account_results client .create_accounts([ tb::Account { id: 1, ledger: 1, code: 1, ..Default::default() }, tb::Account { id: 2, ledger: 1, code: 1, ..Default::default() }, ])? .await?; assert!(account_results.len() 2); assert!(account_results[0].status tb::CreateAccountStatus::Created); assert!(account_results[1].status tb::CreateAccountStatus::Created);Rust 客户端的Account结构体定义于 src/clients/rust/src/tb_client.rs包含debits_pending、debits_posted、credits_pending、credits_posted四个余额字段以及ledger、code、flags、timestamp等字段——这正是后续第 3、6 步校验的对象。示例中用..Default::default()填充其余字段余额与用户数据字段保持为零。第 2 步创建 pending 转账预留资金发起一笔金额为500的转账关键在flags: tb::TransferFlags::Pendinglet transfer_results client .create_transfers([tb::Transfer { id: 1, debit_account_id: 1, credit_account_id: 2, amount: 500, ledger: 1, code: 1, flags: tb::TransferFlags::Pending, ..Default::default() }])? .await?; assert!(transfer_results.len() 1); assert!(transfer_results[0].status tb::CreateTransferStatus::Created);这笔转账此刻只是预留reserve资金金额计入账户的 pending 余额尚未真正完成结算。第 3 步查询账户并验证 pending 余额用lookup_accounts批量查询两个账户逐一断言余额。原文档要求的校验值如下账户1借方账户debits_posted 0、credits_posted 0、debits_pending 500、credits_pending 0账户2贷方账户debits_posted 0、credits_posted 0、debits_pending 0、credits_pending 500。代码实现let accounts client.lookup_accounts([1, 2])?.await?; assert_eq!(accounts.len(), 2); for account in accounts { if account.id 1 { assert_eq!(account.debits_posted, 0, account 1 debits, before posted); assert_eq!(account.credits_posted, 0, account 1 credits, before posted); assert_eq!(account.debits_pending, 500, account 1 debits pending, before posted); assert_eq!(account.credits_pending, 0, account 1 credits pending, before posted); } else if account.id 2 { assert_eq!(account.debits_posted, 0, account 2 debits, before posted); assert_eq!(account.credits_posted, 0, account 2 credits, before posted); assert_eq!(account.debits_pending, 0, account 2 debits pending, before posted); assert_eq!(account.credits_pending, 500, account 2 credits pending, before posted); } else { panic!(Unexpected account: {}, account.id); } }如原文档所述pending 转账只影响账户的 pending 借/贷不影响 posted 借/贷。这正是两阶段模型与单阶段转账的本质区别——posted 余额未发生变化资金只是被冻结在了 pending 余额中。第 4 步post 这笔 pending 转账结算创建第二笔转账pending_id 1指向第一笔flags PostPendingTransferlet transfer_results client .create_transfers([tb::Transfer { id: 2, debit_account_id: 1, credit_account_id: 2, amount: 500, pending_id: 1, ledger: 1, code: 1, flags: tb::TransferFlags::PostPendingTransfer, ..Default::default() }])? .await?; assert!(transfer_results.len() 1); assert!(transfer_results[0].status tb::CreateTransferStatus::Created);注意这里创建了一笔全新的转账id 2而不是修改第一笔转账。第二笔转账的amount 500与 pending 转账金额相等因此整笔金额被 posted这笔转账拥有自己独立的id绝不与第一笔的id相同详见 docs/coding/two-phase-transfers.md 的所有转账均不可变一节。第 5 步查询并验证两笔转账的标志位let transfers client.lookup_transfers([1, 2])?.await?; assert_eq!(transfers.len(), 2); for transfer in transfers { if transfer.id 1 { assert!( transfer.flags.0 tb::TransferFlags::Pending.0 ! 0, transfer 1 pending ); assert!( transfer.flags.0 tb::TransferFlags::PostPendingTransfer.0 0, transfer 1 post_pending_transfer ); } else if transfer.id 2 { assert!( transfer.flags.0 tb::TransferFlags::Pending.0 0, transfer 2 pending ); assert!( transfer.flags.0 tb::TransferFlags::PostPendingTransfer.0 ! 0, transfer 2 post_pending_transfer ); } else { panic!(Unknown transfer: {}, transfer.id); } }校验逻辑第一笔转账始终带pending标志且不带post_pending_transfer标志第二笔转账始终带post_pending_transfer标志且不带pending标志。这印证了不可变性完成两阶段转账post 或 void不会修改原 pending 转账其pending标志永远保留结算动作完全由第二笔独立转账携带的post_pending_transfer标志表达。第 6 步验证最终账户余额最后再次查询账户确认金额已经从 pending 转为 posted账户1debits_posted 500、credits_posted 0、debits_pending 0、credits_pending 0账户2debits_posted 0、credits_posted 500、debits_pending 0、credits_pending 0。代码实现let accounts client.lookup_accounts([1, 2])?.await?; assert_eq!(accounts.len(), 2); for account in accounts { if account.id 1 { assert_eq!(account.debits_posted, 500, account 1 debits); assert_eq!(account.credits_posted, 0, account 1 credits); assert_eq!(account.debits_pending, 0, account 1 debits pending); assert_eq!(account.credits_pending, 0, account 1 credits pending); } else if account.id 2 { assert_eq!(account.debits_posted, 0, account 2 debits); assert_eq!(account.credits_posted, 500, account 2 credits); assert_eq!(account.debits_pending, 0, account 2 debits pending); assert_eq!(account.credits_pending, 0, account 2 credits pending); } else { panic!(Unexpected account: {}, account.id); } }此时两笔转账的账务效果完全相同于一笔 500 的单阶段转账但中间经历了预留 → 结算两个明确阶段业务上可以在两者之间插入人工审核、超时判断或取消逻辑。深入原理TigerBeetle 的两阶段转账模型示例背后是 TigerBeetle 在数据库层直接内建的两阶段转账原语官方概念文档见 docs/coding/two-phase-transfers.md。其命名借鉴了分布式事务中的两阶段提交协议但在这里被用于资金的分阶段移动预留资金Reserveflags.pending将金额计入账户的debits_pending/credits_pending结算资金Resolve通过flags.post_pending_transferpost、flags.void_pending_transfervoid或超时expire三种方式之一结束这笔预留。预留阶段Pending Transferpending 转账在账户上的效果amount分别计入debit_account.debits_pending与credit_account.credits_pending而debits_posted/credits_posted保持不变。示例第 3 步验证的正是这一点。结算阶段Post / Void / ExpirePostpost_pending_transfer将 pending 转账全部或部分 posted。TigerBeetle原子地回滚debits_pending/credits_pending的变化并将其应用到debits_posted/credits_posted见 src/clients/rust/README.md 的 Two-Phase Transfers 一节。Voidvoid_pending_transfer回滚debits_pending/credits_pending的变化但不计入 posted 余额——资金原地退回如同未发生过。Expire超时pending 转账可附带 timeout 字段以秒为单位的间隔而非绝对时间戳零表示无超时。若超时到达仍未 post/void转账自动过期全额退回原账户。注意过期语义是 best-effort过期转账不能再被手动 post/void但客户端请求可能仍短暂观察到 pending 余额详见 docs/reference/transfer.md 与 docs/coding/time.md。部分结算与 AMOUNT_MAX 语义post 时的amount存在三种情况当前客户端版本语义见 docs/reference/transfer.mdpostedamount小于pending 转账金额只结算该金额剩余部分恢复到原账户postedamount等于pending 金额或等于AMOUNT_MAX即2^128 - 1结算整笔金额postedamount大于pending 金额但小于AMOUNT_MAX返回exceeds_pending_transfer_amount错误状态码 31见下文源码证据。void 时同理amount为 0 则自动取 pending 金额非 0 则必须与 pending 金额相等。状态转移三种典型路径官方概念文档用三张表总结了账户余额与转账标志在三个时间点的变化。全额 post对应本示例账户A借方账户B贷方转账pendingpostedpendingposteddebit_account_idcredit_account_idamountflagswxyz----w 123xy 123zAB123pendingwx 123yz 123AB123post_pending_transfer部分 post结算 123 中的 100剩余 23 退回账户A借方账户B贷方转账pendingpostedpendingposteddebit_account_idcredit_account_idamountflagswxyz----w 123xy 123zAB123pendingwx 100yz 100AB100post_pending_transfervoid作废资金全额退回账户A借方账户B贷方转账pendingpostedpendingposteddebit_account_idcredit_account_idamountflagswxyz----w 123xy 123zAB123pendingwxyzAB123void_pending_transfer错误处理一笔 pending 只能结算一次一笔 pending 转账只能被 post 或 void 一次不能 post 两次也不能先 void 再 post。重复结算会返回对应错误见 docs/reference/requests/create_transfers.mdpending_transfer_already_posted状态码 33pending_transfer_already_voided状态码 34pending_transfer_expired状态码 35。同时post/void 转账还必须遵守字段约束pending_id必须引用一笔 pending 转账post_pending_transfer与void_pending_transfer两个标志互斥debit_account_id、credit_account_id、ledger、code等字段可以为零自动继承 pending 转账的值非零则必须与 pending 转账完全一致见 docs/reference/transfer.md。与账户不变量的关系两阶段设计的一个精妙之处在于pending 预留的金额保证第二步post 或 void永远不会破坏账户配置的余额不变量debits_must_not_exceed_credits或credits_must_not_exceed_debits对应 Rust 客户端中的 AccountFlags。这是悲观的pessimistic检查例如某账户设置debits_must_not_exceed_credits其credits_posted 100、debits_posted 70此时发起一笔使debits_pending 50的 pending 转账会当场失败——它不会等到 posted 阶段才失败。因此示例中的两阶段流程天然与账户限额/冻结类业务兼容。转账的不可变性再次强调官方概念文档All Transfers Are Immutable一节完成两阶段转账不修改原 pending 转账而是创建一笔新转账。第一笔转账永远保留pending标志第二笔转账携带post_pending_transfer或void_pending_transfer标志并通过pending_id指向第一笔的id且拥有全新的id。这让整条资金链路完全可审计、可回溯。从源码看实现flag 位与状态码Rust 客户端的类型定义由 rust_bindings.zig 自动生成到 src/clients/rust/src/tb_client.rs。TransferFlags是一个 16 位 bitfieldpub struct TransferFlags(pub u16); impl TransferFlags { pub const Linked: TransferFlags TransferFlags(1 0); pub const Pending: TransferFlags TransferFlags(1 1); pub const PostPendingTransfer: TransferFlags TransferFlags(1 2); pub const VoidPendingTransfer: TransferFlags TransferFlags(1 3); pub const BalancingDebit: TransferFlags TransferFlags(1 4); pub const BalancingCredit: TransferFlags TransferFlags(1 5); pub const ClosingDebit: TransferFlags TransferFlags(1 6); pub const ClosingCredit: TransferFlags TransferFlags(1 7); pub const Imported: TransferFlags TransferFlags(1 8); }示例用到的三个标志正是Pending(11)、PostPendingTransfer(12)void 场景则用VoidPendingTransfer(13)。同文件中tb_transfer_t结构体的内存布局id、debit_account_id、credit_account_id、amount、pending_id、user_data 系列、timeout、ledger、code、flags、timestamp与 C ABI 严格对应这也是所有官方客户端共享同一tb_client原生库的原因。同一文件还给出了两阶段相关错误的状态码常量可直接作为排查依据pub const TB_CREATE_TRANSFER_STATUS_TB_CREATE_TRANSFER_PENDING_TRANSFER_ALREADY_POSTED: TB_CREATE_TRANSFER_STATUS 33; pub const TB_CREATE_TRANSFER_STATUS_TB_CREATE_TRANSFER_PENDING_TRANSFER_ALREADY_VOIDED: TB_CREATE_TRANSFER_STATUS 34; pub const TB_CREATE_TRANSFER_STATUS_TB_CREATE_TRANSFER_PENDING_TRANSFER_EXPIRED: TB_CREATE_TRANSFER_STATUS 35; pub const TB_CREATE_TRANSFER_STATUS_TB_CREATE_TRANSFER_EXCEEDS_PENDING_TRANSFER_AMOUNT: TB_CREATE_TRANSFER_STATUS 31;进阶void 作废、超时与批量结算Void 示例代码void_pending_transfer与 post 对称但效果是退回资金。Rust 客户端 README 中的示意如下let transfer0 tb::Transfer { id: 8, debit_account_id: 101, credit_account_id: 102, amount: 10, ledger: 1, code: 1, ..Default::default() }; let transfer_results client.create_transfers([transfer0])?.await?; let transfer1 tb::Transfer { id: 9, amount: 0, // void 时 amount 为 0 表示全额作废 pending_id: 8, flags: tb::TransferFlags::VoidPendingTransfer, ..Default::default() }; let transfer_results client.create_transfers([transfer1])?.await?;void 时debit_account_id、credit_account_id、ledger、code都可省略为零时自动继承 pending 转账的值amount为 0 表示整笔作废。超时自动过期若在创建 pending 转账时设置timeout单位秒32 位无符号整数例如let transfer tb::Transfer { id: 1, debit_account_id: 1, credit_account_id: 2, amount: 500, ledger: 1, code: 1, flags: tb::TransferFlags::Pending, timeout: 3600, // 1 小时后若未结算则自动过期并退回 ..Default::default() };那么到达timestamp timeout后若既未 post 也未 void全额自动退回。业务上可用于订单超时未支付自动取消等场景。同族示例与批量建议basic 示例单阶段转账直接创建账户并转一笔账two-phase-many 示例创建两个账户后发起多笔 pending 转账并交替 post 与 void展示批量两阶段处理生产建议TigerBeetle 在事件大批量提交时才能达到峰值性能Rust 客户端单次请求默认最大批次为8189个事件见 lib.rs 的 Request batching 说明应用层应尽量批量提交同一 Client 实例是线程安全的可跨并发任务共享以便自动合并请求。延伸阅读两阶段转账概念与状态转移docs/coding/two-phase-transfers.mdTransfer字段完整约束pending/post/void 各模式字段表docs/reference/transfer.mdcreate_transfers请求与全部错误码docs/reference/requests/create_transfers.md账户余额字段与不变量标志docs/reference/account.md关联事件批量原子提交docs/coding/linked-events.md时间与超时语义docs/coding/time.md【免费下载链接】tigerbeetleThe financial transactions database designed for mission critical safety and performance.项目地址: https://gitcode.com/GitHub_Trending/ti/tigerbeetle创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
