
TigerBeetle Ruby 客户端实战Many Two-Phase Transfers 示例中的批量 Pending/Post/Void 转账与余额校验【免费下载链接】tigerbeetleThe financial transactions database designed for mission critical safety and performance.项目地址: https://gitcode.com/GitHub_Trending/ti/tigerbeetle导读本文围绕 TigerBeetle 官方 Ruby 示例two-phase-many展开讲解如何在一次会话中创建多笔待定转账Pending Transfer随后交替执行过账Post与作废Void并在每一步之后读取账户余额进行断言校验。读完本文你将掌握 TigerBeetle 两阶段转账Two-Phase Transfer的完整编码套路flags.pending预留资金、flags.post_pending_transfer结算资金、flags.void_pending_transfer释放资金以及debits_pending/credits_pending/debits_posted/credits_posted四组余额字段在不同阶段的取值规则并能在 Ruby 中编写可重复验证的余额断言逻辑。1. 示例概览这个项目到底做了什么示例完整代码位于 src/clients/ruby/samples/two-phase-many/main.rb配套说明即本仓库的 src/clients/ruby/samples/two-phase-many/README.md该 README 由 src/scripts/client_readmes.zig 自动生成与各语言客户端示例保持同步。整个程序围绕两个账户1和2展开执行五个阶段创建两个账户发起 5 笔待定转账金额从100到500每笔递增100读取账户并校验待定余额pending balances为1500逐笔交替过账/作废这 5 笔待定转账每处理一笔都重新读取余额并校验最终校验两个账户只剩已过账余额posted balances900待定余额归零。与只演示单笔转账的 src/clients/ruby/samples/two-phase/README.md 相比two-phase-many的关键进阶点在于同一批待定转账可以被逐笔、交错地结算过账或取消作废并且每一步都能精确验证余额变化——这正是现实中多笔未决交易并发结算场景的最小可运行范本。2. 环境准备Prerequisites示例 README 明确给出了运行前提操作系统生产环境仅支持 Linux 5.6为便于开发macOS 与 Windows 也可运行示例。Ruby 版本3.3。3. 安装与运行3.1 安装 TigerBeetle Ruby 客户端首先克隆本仓库并进入示例目录cd tigerbeetle/src/clients/ruby/samples/two-phase-many然后通过 gem 安装官方客户端gem install tigerbeetle安装后即可在 Ruby 代码中require tigerbeetle示例 main.rb 第一行即如此。3.2 启动 TigerBeetle 服务端按照仓库根目录 README.md 中Running TigerBeetle一节的步骤启动服务端。示例默认连接localhost:3000如果你的服务端不在该地址需要通过环境变量TB_ADDRESS指定完整地址export TB_ADDRESS3000 # 默认值等价于 localhost:3000 export TB_ADDRESSlocalhost:3001 # 非默认端口示例在 main.rb 中地址读取逻辑为replica_addresses ENV.fetch(TB_ADDRESS, 3000)即未设置TB_ADDRESS时默认使用3000本机 3000 端口。3.3 运行示例ruby main.rb全部断言通过后程序会输出ok任一步骤校验失败则会抛出带明确信息的异常并中断执行。4. 客户端生命周期与请求方法示例使用TigerBeetle::Client.open的块block形式打开客户端块结束时自动关闭连接避免手动管理关闭时机TigerBeetle::Client.open(cluster_id: 0, replica_addresses:) do |client| # 在此使用 client end对应的客户端实现见 src/clients/ruby/src/tigerbeetle/client.rbClient.open内部先new(cluster_id:, replica_addresses:)创建原生客户端yield给块使用随后在ensure中调用close等待所有在途请求完成后关闭。客户端可被多线程/多纤程共享公开请求方法均为同步阻塞在存在纤程调度器时会主动让出yield。本示例用到的请求方法同见 client.rb方法说明create_accounts(accounts)批量创建账户返回与输入一一对应的结果数组create_transfers(transfers)批量创建转账含 pending/post/void返回结果数组lookup_accounts(ids)按 ID 批量查找账户未找到的会被省略5. 余额断言辅助函数在进入正题前先看 main.rb 中定义的assert_accounts辅助函数。它接收lookup_accounts的返回值和一个以账户 ID 为键、以期望余额为值的哈希逐项比对四个余额字段def assert_accounts(accounts, expected) raise expected #{expected.length} accounts unless accounts.length expected.length accounts.each do |account| values expected.fetch(account.id) { raise unexpected account: #{account.inspect} } unless account.debits_posted values.fetch(:debits_posted) raise account #{account.id} debits_posted mismatch end unless account.credits_posted values.fetch(:credits_posted) raise account #{account.id} credits_posted mismatch end unless account.debits_pending values.fetch(:debits_pending) raise account #{account.id} debits_pending mismatch end unless account.credits_pending values.fetch(:credits_pending) raise account #{account.id} credits_pending mismatch end end endAccount对象的这四个字段debits_pending、debits_posted、credits_pending、credits_posted定义在自动生成的绑定 src/clients/ruby/src/tigerbeetle/bindings.rb 中对应字段语义可参考 docs/reference/account.md。正是这四个字段构成了整个示例的可观测校验面。6. 阶段一创建账户示例先创建两个账户1与2均处于 ledger1、code1account_results client.create_accounts( [ TigerBeetle::Account.new(id: 1, ledger: 1, code: 1), TigerBeetle::Account.new(id: 2, ledger: 1, code: 1) ] ) raise expected 2 account results unless account_results.length 2 account_results.each.with_index(1) do |result, index| unless result.status TigerBeetle::CreateAccountStatus::CREATED raise account #{index} was not created end end要点create_accounts按批处理batch语义返回结果数组每个结果对应一个输入账户需要逐一检查statusTigerBeetle::CreateAccountStatus::CREATED表示创建成功其常量值为4294967295见 bindings.rbledger和code对同一批次内的转账是必填的它们把账户划分到可互相交易的账本域内见 docs/coding/data-modeling.md 中关于 ledgers 的讨论。7. 阶段二创建 5 笔待定转账Pending Transfers随后程序一次性提交 5 笔待定转账账户1借记、账户2贷记金额分别为100、200、300、400、500id从 1 到 5金额为id * 100全部携带TigerBeetle::TransferFlags::PENDING标志transfers (1..5).map do |id| TigerBeetle::Transfer.new( id: id, debit_account_id: 1, credit_account_id: 2, amount: id * 100, ledger: 1, code: 1, flags: TigerBeetle::TransferFlags::PENDING ) end transfer_results client.create_transfers(transfers) unless transfer_results.length transfers.length raise expected #{transfers.length} pending transfer results end transfer_results.each do |result| unless result.status TigerBeetle::CreateTransferStatus::CREATED raise pending transfer was not created end end7.1 PENDING 标志的语义预留资金而非划拨资金根据 docs/coding/two-phase-transfers.md 中 Reserve Funds (Pending Transfer) 一节的说明带flags.pending的转账会将其amount记入借贷双方账户的debits_pending/credits_pending字段不会改动debits_posted/credits_posted。也就是说待定转账把资金冻结/预留下来但尚未真正入账。在 Ruby 绑定中TransferFlags::PENDING定义于 bindings.rb值为1 1即 2同文件还定义了POST_PENDING_TRANSFER 1 2、VOID_PENDING_TRANSFER 1 3等全部转移标志位。8. 阶段三读取并校验待定余额创建完 5 笔待定转账后程序调用lookup_accounts([1, 2])拉取两个账户并断言账户1debits_posted 0、credits_posted 0、debits_pending 1500、credits_pending 0账户2debits_posted 0、credits_posted 0、debits_pending 0、credits_pending 1500。校验逻辑为assert_accounts( client.lookup_accounts([1, 2]), { 1 { debits_posted: 0, credits_posted: 0, debits_pending: 1500, credits_pending: 0 }, 2 {debits_posted: 0, credits_posted: 0, debits_pending: 0, credits_pending: 1500} } )1500 100 200 300 400 500正是 5 笔待定转账金额之和。这说明待定转账只影响账户的pending借贷余额不影响posted借贷余额——这也是两阶段转账最核心的账务语义。9. 阶段四交替过账与作废逐笔校验接下来是示例的精华部分。程序定义了一张操作表依次对 5 笔待定转账做过账、作废、过账、作废、过账的交替处理并在每笔处理之后立即断言账户余额结束转账id引用的待定转账pending_idamount标志flags处理后账户1的debits_posted处理后账户1的debits_pending处理含义61100POST_PENDING_TRANSFER1001400过账待定转账 1全额 10072200VOID_PENDING_TRANSFER1001200作废待定转账 2释放 20083300POST_PENDING_TRANSFER400900过账待定转账 3全额 30094400VOID_PENDING_TRANSFER400500作废待定转账 4释放 400105500POST_PENDING_TRANSFER9000过账待定转账 5全额 500对应源码main.rboperations [ [6, 1, 100, TigerBeetle::TransferFlags::POST_PENDING_TRANSFER, 100, 1400], [7, 2, 200, TigerBeetle::TransferFlags::VOID_PENDING_TRANSFER, 100, 1200], [8, 3, 300, TigerBeetle::TransferFlags::POST_PENDING_TRANSFER, 400, 900], [9, 4, 400, TigerBeetle::TransferFlags::VOID_PENDING_TRANSFER, 400, 500], [10, 5, 500, TigerBeetle::TransferFlags::POST_PENDING_TRANSFER, 900, 0] ] operations.each do |id, pending_id, amount, flags, posted, pending| transfer_results client.create_transfers( [ TigerBeetle::Transfer.new( id: id, debit_account_id: 1, credit_account_id: 2, amount: amount, pending_id: pending_id, ledger: 1, code: 1, flags: flags ) ] ) raise expected 1 finishing transfer result unless transfer_results.length 1 unless transfer_results[0].status TigerBeetle::CreateTransferStatus::CREATED raise finishing transfer #{id} was not created end assert_accounts( client.lookup_accounts([1, 2]), { 1 { debits_posted: posted, credits_posted: 0, debits_pending: pending, credits_pending: 0 }, 2 {debits_posted: 0, credits_posted: posted, debits_pending: 0, credits_pending: pending} } ) end9.1 Post过账与 Void作废的底层约束根据 docs/coding/two-phase-transfers.md 与 docs/reference/transfer.md 的定义这里每笔结束转账finishing transfer都遵循以下规则Post-Pending Transfer过账通过pending_id引用一笔尚处于 pending 状态的转账过账金额等于待定金额时全额结算本示例每笔过账的amount都与对应待定转账金额相等因此全部全额过账若过账amount设为AMOUNT_MAX2^128 - 1同样表示按待定金额全额过账若过账金额小于待定金额则只过账该部分剩余部分自动释放回原账户部分过账见 docs/coding/two-phase-transfers.md 中 Post Partial Pending Amount 的表格示例若过账金额大于待定金额且不等于AMOUNT_MAX返回exceeds_pending_transfer_amount错误见 docs/reference/requests/create_transfers.md。Void-Pending Transfer作废同样通过pending_id引用待定转账amount为 0 时自动采用待定转账的金额完全释放非 0 则必须等于待定金额。本示例作废时显式传入与待定金额相等的200/400语义等价于全额释放。公共约束在 post/void 转账中debit_account_id、credit_account_id、ledger、code既可以为 0此时自动继承自待定转账也可以显式传入且必须与待定转账一致。示例代码显式传入了与待定转账完全相同的debit_account_id: 1、credit_account_id: 2、ledger: 1、code: 1。9.2 逐步校验的含义第 1 步过账后posted由 0 变为 100pending由 1500 减为 1400释放了被过账的 100第 2 步作废后posted保持 100 不变作废不产生入账pending再减 200 变为 1200如此交替推进直到第 5 步过账完毕posted累计为100 300 500 900pending归零。账户2始终呈镜像状态credits_posted与账户1的debits_posted同步增长credits_pending与账户1的debits_pending同步递减。这与单笔转账示例src/clients/ruby/samples/two-phase/README.md中过账后 posted 增加、pending 清零的规律完全一致只是被扩展到了多笔交错场景。10. 阶段五校验最终余额程序最后再次拉取两个账户断言最终状态为只存在 posted 余额不存在 pending 余额账户1debits_posted 900、credits_posted 0、debits_pending 0、credits_pending 0账户2debits_posted 0、credits_posted 900、debits_pending 0、credits_pending 0。这一终态说明全部 5 笔待定转账均已结算3 笔过账共 900 2 笔作废共 600 初始预留的 1500且两阶段转账不会留下任何悬空的待定余额。11. 深入原理两阶段转账的完整模型11.1 两阶段生命周期TigerBeetle 的两阶段转账参照了两阶段提交协议two-phase commit protocol的命名思路资金移动分两步预留Reserve创建flags.pending转账将金额记入 pending 余额结算Resolve创建post_pending_transfer或void_pending_transfer转账把待定金额转为已入账或释放回原账户。此外待定转账还可以带timeout单位为秒的间隔而非绝对时间戳若超时前既未过账也未作废则自动过期并将全额释放回原账户。过期后的待定转账不能再被手动过账或作废会返回pending_transfer_expired错误。关于时间间隔为何采用相对值而非绝对时间戳可参考 docs/coding/time.md。11.2 转账不可变结束转账是新转账而非修改需要特别强调无论过账还是作废都不会修改原始的待定转账。TigerBeetle 中的转账一旦创建即不可修改、不可删除见 docs/reference/transfer.md 的 Guarantees 一节。第二笔转账携带post_pending_transfer/void_pending_transfer标志、pending_id指向第一笔待定转账的id并且拥有自己独立且唯一的id本示例中为 610。11.3 错误处理与幂等一笔待定转账只能被过账或作废一次。重复结算会得到对应的错误结果码均定义在 bindings.rb 的CreateTransferStatus中PENDING_TRANSFER_ALREADY_POSTED已被过账PENDING_TRANSFER_ALREADY_VOIDED已被作废PENDING_TRANSFER_EXPIRED已过期。create_transfers返回的每个结果都携带status与timestamp字段应用层应逐条检查status是否等于CreateTransferStatus::CREATED。11.4 与账户不变量Account Invariants的交互待定转账的预留机制保证了无论第二步是过账还是作废都不会破坏账户上配置的余额不变量——例如credits_must_not_exceed_debits或debits_must_not_exceed_credits见 docs/reference/account.md 的 Flags 部分。反之如果账户在不变量约束下已经没有足够余量待定转账创建时而不是过账时就会失败即悲观式地拒绝预留。12. 测试佐证官方集成测试如何验证同一套行为Ruby 客户端的集成测试 src/clients/ruby/tests/integration/test_two_phase_transfer.rb 对本文讨论的机制提供了源码级印证覆盖了更多边界场景创建带timeout的待定转账后lookup_transfers能读回flags该测试中断言transfers[0].flags 2即PENDING 1 1以及timeout、timestamp等字段全额过账用amount: AMOUNT_MAX(1 128) - 1过账待定转账debits_pending/credits_pending归零debits_posted/credits_posted相应增加作废用amount: 0加VOID_PENDING_TRANSFER作废触发金额自动继承语义待定余额释放且 posted 余额不变过期创建timeout: 1的待定转账sleep(1.5)后待定余额被自动清除再尝试作废会得到PENDING_TRANSFER_EXPIRED状态。这套断言与two-phase-many示例中的余额校验互为补充示例验证多笔交错结算的余额演进测试验证全额/作废/过期等边界语义。13. 相关资源示例源码src/clients/ruby/samples/two-phase-many/main.rb单笔两阶段转账示例src/clients/ruby/samples/two-phase/README.md两阶段转账官方指南docs/coding/two-phase-transfers.mdTransfer字段与标志位参考docs/reference/transfer.mdAccount余额字段与不变量参考docs/reference/account.mdcreate_transfers请求与错误码参考docs/reference/requests/create_transfers.mdRuby 客户端实现src/clients/ruby/src/tigerbeetle/client.rb请求方法、src/clients/ruby/src/tigerbeetle/bindings.rb数据结构与状态常量两阶段转账集成测试src/clients/ruby/tests/integration/test_two_phase_transfer.rb【免费下载链接】tigerbeetleThe financial transactions database designed for mission critical safety and performance.项目地址: https://gitcode.com/GitHub_Trending/ti/tigerbeetle创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考