TigerBeetle Java 多笔两阶段转账实战:从 pending 预留到交替 post/void 的余额验证

发布时间:2026/9/14 18:08:13
TigerBeetle Java 多笔两阶段转账实战:从 pending 预留到交替 post/void 的余额验证 TigerBeetle Java 多笔两阶段转账实战从 pending 预留到交替 post/void 的余额验证【免费下载链接】tigerbeetleThe financial transactions database designed for mission critical safety and performance.项目地址: https://gitcode.com/GitHub_Trending/ti/tigerbeetle本文以 TigerBeetle 仓库中的 Java 示例 two-phase-many 示例 README 为主线完整讲解如何用 Java 客户端在一个事务批次中创建 5 笔待定pending转账再以交替 post入账/ void作废的方式结算每一笔并在每个阶段读取账户余额进行断言验证。读完本文你将掌握 TigerBeetle 两阶段转账Two-Phase Transfer在真实 Java 代码中的完整落地方式、余额字段debits_pending/credits_pending/debits_posted/credits_posted的语义以及如何通过TB_ADDRESS连接自建的 TigerBeetle 集群并运行该示例。示例概览与运行前准备该示例位于 src/clients/java/samples/two-phase-many其可执行源码为 Main.java项目构建配置见 pom.xml。它演示的核心场景是一次性发起多笔两阶段转账并对每一笔分别做出入账或作废的最终决定——这与支付结算、退款、扣款预授权等真实金融业务高度一致。前置条件Prerequisites根据示例 README运行环境要求如下Linux 5.6是官方唯一支持的生产环境为方便开发同时支持 macOS 与 WindowsJava 11pom.xml 中maven.compiler.source/target均为11Maven 3.6并非严格必需但官方指南以其为准。安装 Java 客户端依赖克隆仓库后进入示例目录然后安装 TigerBeetle Java 客户端cd src/clients/java/samples/two-phase-many mvn installmvn install会把本仓库的 Java 客户端artifactId 为tigerbeetle-java版本0.0.1-SNAPSHOT见 pom.xml安装到本地 Maven 仓库供示例工程引用。启动 TigerBeetle 服务器并运行示例启动单副本集群按仓库根 README.md 中的步骤启动一个单副本集群开发模式$ ./tigerbeetle format --cluster0 --replica0 --replica-count1 --development 0_0.tigerbeetle $ ./tigerbeetle start --addresses3000 --development 0_0.tigerbeetle上述命令在localhost:3000暴露服务。若你的服务器运行在其他地址需要通过环境变量TB_ADDRESS指定其完整地址示例代码会读取该变量来决定连接目标。运行示例mvn exec:javapom.xml中通过 exec-maven-plugin 指定了主类com.tigerbeetle.samples.Main因此该命令会直接执行示例程序。如果启动服务器时没有使用默认的3000端口请先设置export TB_ADDRESS127.0.0.1:4000 # 示例连接其他地址 mvn exec:java在示例源码中TB_ADDRESS的读取逻辑位于 Main.javareplicaAddress为null时默认使用3000否则使用环境变量指定的完整地址集群 ID 通过UInt128.asBytes(0)构造与--cluster0对应。逐步拆解示例流程Walkthrough第 1 步创建两个账户程序首先通过 AccountBatch 创建账户1和账户2两者均设置ledger 1、code 1AccountBatch accounts new AccountBatch(2); accounts.add(); accounts.setId(1); accounts.setLedger(1); accounts.setCode(1); accounts.add(); accounts.setId(2); accounts.setLedger(1); accounts.setCode(1); CreateAccountResultBatch accountResults client.createAccounts(accounts);创建结果通过CreateAccountResultBatch逐个检查状态任何非Created的状态都会抛出异常例如throw new Exception(String.format(Error creating account %d: %s\n, accountResults.getPosition(), accountResults.getStatus()));第 2 步创建 5 笔 pending 转账接下来使用 TransferBatch 一次性创建 5 笔待定转账TransferFlags.PENDING金额从100到500、每笔递增100全部为账户1借记debit到账户2贷记credit转账 iddebit_account_idcredit_account_idamountflags112100PENDING212200PENDING312300PENDING412400PENDING512500PENDING核心代码模式以第 1 笔为例完整 5 笔见 Main.javatransfers.add(); transfers.setId(1); transfers.setDebitAccountId(1); transfers.setCreditAccountId(2); transfers.setLedger(1); transfers.setCode(1); transfers.setAmount(100); transfers.setFlags(TransferFlags.PENDING);提交后立即断言transferResults.getLength() transfers.getLength()确保 5 笔转账全部被服务器受理随后逐条检查状态是否为Created。第 3 步获取并验证 pending 账户余额创建 5 笔 pending 转账后通过client.lookupAccounts(ids)同时读取账户1和2并断言其四个余额字段账户1借方debits_posted 0credits_posted 0debits_pending 1500credits_pending 0账户2贷方debits_posted 0credits_posted 0debits_pending 0credits_pending 1500原因说明pending 转账只会影响账户上的pending借/贷reserve 阶段不会改变posted借/贷。因此 5 笔金额合计100 200 300 400 500 1500全部体现在debits_pending账户 1与credits_pending账户 2上而 posted 均为 0。这正是两阶段转账先冻结、后结算语义的直接体现。源码中的余额字段读取方法定义于 AccountBatch.javagetDebitsPending()、getDebitsPosted()、getCreditsPending()均返回BigInteger示例中用intValueExact()精确转int后与期望值比较。第 4 步交替 post 与 void 每笔转账这是示例最核心的部分对第 15 笔 pending 转账奇偶交替地执行入账post或作废void每完成一笔就重新lookupAccounts并断言余额结算转账 idpending_idamountflags含义61100POST_PENDING_TRANSFER入账第 1 笔72200VOID_PENDING_TRANSFER作废第 2 笔83300POST_PENDING_TRANSFER入账第 3 笔94400VOID_PENDING_TRANSFER作废第 4 笔105500POST_PENDING_TRANSFER入账第 5 笔入账第 1 笔转账 id 6的构造方式如下完整代码见 Main.javatransfers.add(); transfers.setId(6); transfers.setPendingId(1); transfers.setDebitAccountId(1); transfers.setCreditAccountId(2); transfers.setLedger(1); transfers.setCode(1); transfers.setAmount(100); transfers.setFlags(TransferFlags.POST_PENDING_TRANSFER);作废第 2 笔转账 id 7则只需把标志位换成TransferFlags.VOID_PENDING_TRANSFER见 Main.java。PENDING、POST_PENDING_TRANSFER、VOID_PENDING_TRANSFER三个标志位的具体位值定义在 TransferFlags.java分别为1 1、1 2、1 3。每一步之后的账户余额变化如下账户1为借方、账户2为贷方步骤账户 1 debits_posted账户 1 debits_pending账户 2 credits_posted账户 2 credits_pending初始 5 笔 pending0150001500post 第 1 笔10010014001001400void 第 2 笔20010012001001200post 第 3 笔300400900400900void 第 4 笔400400500400500post 第 5 笔50090009000可以看到post 把 pending 金额转正为 posted 金额void 则把 pending 金额直接归零对应的 pending 扣减即释放冻结credits_posted/debits_pending等字段同步更新每一步的断言都精确对应源码中的assertTrue校验见 Main.java。最终步骤验证最终账户余额所有结算完成后再次读取两个账户断言最终状态——所有 pending 全部清空余额只以 posted 形态存在账户1借方debits_posted 900credits_posted 0debits_pending 0credits_pending 0账户2贷方debits_posted 0credits_posted 900debits_pending 0credits_pending 0900来源于被入账的 3 笔100 300 500而被作废的 2 笔200 400则没有产生任何 posted 变动最终资金只从账户1流向账户2共900。两阶段转账的语义原理要理解示例中的每一步需要结合 docs/coding/two-phase-transfers.md 中定义的两阶段转账模型一次两阶段转账把资金移动拆成预留Reserve→ 结算Resolve两个阶段预留资金pending transfer带flags.pending的转账把amount记入账户的debits_pending/credits_pending不触碰debits_posted/credits_posted结算资金pending 转账只能被 post、void或超时过期expire一次postflags.post_pending_transfer把全部或部分预留金额转为 postedvoidflags.void_pending_transfer把预留金额全额返还给原账户expire创建 pending 时可带timeout秒超时未结算则自动全额返还。关于金额与字段的约束参考 docs/reference/transfer.md 可以进一步确认示例用法的合法性带post_pending_transfer时pending_id必须指向一条 pending 转账post 的amount若等于AMOUNT_MAX2^128 - 1则自动取 pending 金额否则必须小于等于 pending 金额transfer.md#amount带void_pending_transfer时amount为 0 会自动设为 pending 金额非零则必须与 pending 金额相等post/void 转账的debit_account_id、credit_account_id、ledger、code可以为零自动沿用 pending 转账对应值非零则必须与 pending 转账一致post_pending_transfer与void_pending_transfer不能同时设置一条 pending 转账只能被结算一次重复 post 或 void 会分别返回pending_transfer_already_posted、pending_transfer_already_voided超时后结算会返回pending_transfer_expired见 docs/coding/two-phase-transfers.md。转账不可变原则需要特别强调结算不会修改原始 pending 转账。所有转账一经创建即不可变见 docs/reference/transfer.md。在示例中第 15 笔转账创建后永远保持pending标志入账/作废是通过新建第 610 笔转账、设置其pending_id指向目标转账 id 来完成的。这也是交替 post/void无需担心互相干扰的根本原因——每笔 pending 转账的结算彼此独立示例在每个步骤后都执行一次余额验证正是为了直观展示这种逐笔独立结算的过程。与账户不变量Account Invariants的配合两阶段转账的预留机制保证了后续的 post/void 永远不会破坏账户上配置的余额不变量credits_must_not_exceed_debits或debits_must_not_exceed_credits。从 docs/coding/two-phase-transfers.md 可知如果账户配置了debits_must_not_exceed_credits且当前credits_posted 100、debits_posted 70此时发起一笔导致debits_pending 50的 pending 转账该 pending 转账会立即失败而不是等到 post 阶段才失败——这就是悲观的 pending 转账Pessimistic Pending Transfers语义。示例中账户1/2未设置任何余额不变量标志因此 5 笔合计 1500 的 pending 预留均能成功若业务要求借记方余额充足只需在创建账户时追加对应 flagsTigerBeetle 会在预留阶段就执行约束检查。运行与验证小结启动服务器./tigerbeetle format --cluster0 --replica0 --replica-count1 --development 0_0.tigerbeetle与./tigerbeetle start --addresses3000 --development 0_0.tigerbeetle见 README.md安装客户端与运行示例mvn installmvn exec:java非默认地址通过环境变量TB_ADDRESS指定完整服务器地址验证结果示例内部通过assertTrue在每个阶段校验余额全部通过即代表 5 笔两阶段转账3 笔入账、2 笔作废按预期执行最终账户 1 的debits_posted与账户 2 的credits_posted均为900pending 全部归零。如果你需要进一步对比单笔两阶段转账的实现可以阅读 two-phase 示例 及其 Main.java若想深入了解余额字段在数据传输层的定义与读取方式可查阅 AccountBatch.java 与 AccountBalanceBatch.java。【免费下载链接】tigerbeetleThe financial transactions database designed for mission critical safety and performance.项目地址: https://gitcode.com/GitHub_Trending/ti/tigerbeetle创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考