交易类应用发的是少量有价值的交易。另有一类应用发的是几千笔廉价交易 —— 游戏内动作、积分更新、按用户的状态变更、微额结算。
它们的失败模式并不相同。当单笔交易本身价值很低时,★手续费就不再是一个可以四舍五入掉的零头,而变成了产品的单位经济学★。
不是每个动作都需要一笔交易
最有价值的优化发生在写任何 Solana 代码之前:决定什么东西真的该上链。
| 该上链 | 不该上链 |
|---|---|
| 所有权转移 | 位置更新 |
| 价值结算 | 中间状态 |
| 任何可能有争议的 | 任何可以重算的 |
| ★最终结果★ | ★通向结果的那些步骤★ |
★一个把每一个中间步骤都放上链的系统,是在为它并不需要的持久性付钱。★ 交互过程放在链下跑,结果放到链上结算。
判据很简单:如果你能从最终状态把它重新算出来,那它就不需要是一笔交易。
批量是主要杠杆
一旦确定了哪些必须上链,问题就变成这需要几笔交易。每个签名都要带一份基础手续费,所以★一百笔独立转账的花费,大约是一笔批量交易的一百倍★,而且这一百笔里每一笔都是一次独立的失败机会。
// 一笔交易,多个接收方。
const tx = new Transaction();
for (const { to, lamports } of updates.slice(0, 20)) {
tx.add(SystemProgram.transfer({
fromPubkey: treasury.publicKey, toPubkey: to, lamports,
}));
}
有两个天花板决定能装多少:
1232 字节。 每个账户引用占 32 字节。这是大多数批量真正卡住的地方,也是 AddressLookupTableProgram 在这里有用的原因 —— 你的接收方集合往往是稳定的、可以跨批次复用。
计算单元。 每条指令都消耗计算,而单笔交易的预算是有限的。去模拟真实的批次形状,不要从一条指令外推。
const sim = await connection.simulateTransaction(tx, {
replaceRecentBlockhash: true, sigVerify: false,
});
console.log("单元:", sim.value.unitsConsumed);
console.log("字节:", tx.serialize().length, "/ 1232");
★用实测找出你的批次大小,然后留出余量。★ 一个刚好装满的批次,第一次遇到某条指令比平时稍大一点时就会崩。
如果你的批量做对了、吞吐还是不够,免费领个 BoltTx key 改一行就能测测提交路径。
写入争用才是真正的吞吐上限
这是大多数团队撞上了、却没认出来的那个约束。
Solana 会并行执行交易 —— 除非它们写入同一个账户。 写同一个账户的交易会被串行化,所以一个把所有动作都汇聚到单个共享账户的设计,不管你发多少笔交易都有一个硬上限。
★一个共享的计数器账户★
↓ 所有写入在这里串行
吞吐 ≈ 每个 slot 一次写入
★按用户分开的账户★
↓ 写入互相独立
吞吐随并行度扩展
★如果不管你发多少、吞吐都纹丝不动,先去找共享的可写账户,再去看你的 RPC。★
修法都是结构性的:
给状态分片。 把一个热账户按用户或按桶拆成很多个。
把计数器挪到链下。 在链下聚合,定期结算。
按用户建账户。 多花租金,但写入可以并行推进。
先聚合再写入。 一次代表一百个动作的写入,胜过一百次写入。
规模化之后的手续费经济学
到了这个量级,手续费策略是一个商业决策。
每个签名的基础手续费。 一笔只有一个签名的批量交易,比很多笔单独交易便宜得多。批量是一种手续费策略,不只是延迟策略。
优先费在这里是可选的。 交易机器人在争位置。★而例行的后台写入通常不需要赢下一场竞速 —— 它们只需要最终落块。★ 在交易机器人会多付的场景里,一个"几个 slot 内能落块"的适中费用往往才是对的。
租金是一笔实打实的开支。 你创建的每个账户都锁着租金豁免的 lamport。按用户、按会话去创建账户是一项不断增长的负债,而用完关闭账户可以把它取回。
★决定由谁付,并且尽早决定。★ 如果用户付,他们就需要 SOL 和一次钱包交互,那是摩擦。如果你付,你的成本会随使用量增长,必须从一开始就进入你的利润模型,而不是事后才发现。
按规模处理失败
一分钟几笔的时候,你可以逐个检查失败。几千笔的时候,你需要的是策略。
const outcome = await resolve(sig, lastValidBlockHeight);
switch (outcome.status) {
case "success":
await markComplete(batchIds); break;
case "expired":
await requeue(batchIds); break; // ★从没运行 —— 安全★
case "reverted":
// ★落块了但失败了。整个批次一起失败。★
await splitAndInvestigate(batchIds, outcome.err); break;
}
★revert 这一种,正是批量让你付出代价的地方。★ 一条坏指令会让整个批次 revert,因为交易是原子的。二十个更新一起失败,只因为有一个接收方账户不存在。
所以要在批量之前就校验,而不是失败之后再查。 在组装时检查账户是否存在和余额,并把已知有风险的条目排除在大批次之外 —— 让批量高效的那个原子性,同时也是让一个坏条目变得昂贵的那个属性。
拥堵在这里的表现不一样
对交易机器人来说,拥堵意味着输掉一场竞速。对高吞吐的后台系统来说,拥堵意味着队列增长得比它排空更快 —— 而且★积压会比拥堵本身持续得更久★。
要明确地为它做计划:
在你自己的队列里排优先级。 有价值的结算排在装饰性更新前面。
让低价值的活儿等着。 不是所有东西都必须这一分钟落块,而把所有东西都当成紧急的,意味着为全部东西付紧急的价钱。
给队列深度设上限。 用明确的信号拒绝新任务,好过积累一个要几个小时才能清完的积压。
按排空速率告警,不要按深度。 正在排空的深队列没问题;排不动的浅队列才是问题。
上链应该是什么水平
我们投递节点上的真实交易:中位确认 336 毫秒 —— 不到一个 slot。
★在规模化之后,决定队列行为的是尾巴。★ 一个"大部分交易很快落块、但有相当一部分要慢得多"的系统,恰恰会在你的用户最活跃的那些时段积累积压。
BoltTx 在这里的位置
我们负责提交。不管你的状态设计、不管你的批量、不管你的收费模式。
提交走我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool。对高吞吐系统来说,真正相关的属性是可预测性 —— 一个稳定的分布,才让你能估算队列规模、并设定在高负载下依然成立的超时。
你在本地签名。我们不托管资金、不代签、不修改交易内容。tip 带在交易里、从你自己的钱包链上支付,交易失败时跟着一起 revert —— 这是 Solana 原子交易的机制决定的。只有到达链上的交易才计费。
免费领 API key,没有月费:
const connection = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");
常见问题
一个应用在 Solana 上每秒能发多少笔交易? 通常受限于写入争用,而不是网络。写同一个账户的交易会串行化,所以一个共享的热账户会给吞吐设上限,不管你提交多少笔。
怎么批量发送大量小额 Solana 交易? 把多条指令放进一笔交易,受 1232 字节的体积上限和计算预算约束。通过模拟真实的批次形状测出这两个数,然后留余量。
为什么不管我发多少笔,吞吐都不动? 几乎肯定是某个共享的可写账户。Solana 会并行执行,除非交易写同一个账户 —— 一个热账户会把它后面的一切串起来。
游戏内的动作该上链吗? 只有那些需要持久或可能有争议的才该。如果某一步能从最终状态重算出来,把结果放上链结算、其余在链下跑,更便宜也更快。
规模化之后一笔 Solana 交易要多少钱? 每个签名一份基础手续费,加上优先费(如果有)。到几千笔的时候基础手续费占主导,这也是"把多条指令合并到一个签名下"成为主要杠杆的原因。
高吞吐应用需要优先费吗? 通常比交易机器人需要得少。例行的后台写入需要的是最终落块,不是赢下竞速,所以在交易机器人会多付的场景里,适中的费用往往才是对的。
批量里有一条指令失败会怎样? 整笔交易 revert,因为 Solana 的交易是原子的。这正是校验应该放在组装阶段、以及已知有风险的条目该排除在大批次之外的原因。
怎么给状态分片来避免写入争用? 把一个热账户按用户或按桶拆成很多个,让写入并行推进。代价是更多租金和更多账户要管,换来的是一个否则根本抬不高的吞吐上限。
租金对高吞吐应用是笔大成本吗? 当你按用户或按会话创建账户时会变成大成本。租金豁免的 lamport 在关闭时可以取回,所以清理通道和创建通道一样重要。
一个批次该多大? 在 1232 字节和你的计算预算之内尽量大,再减去余量。要针对你自己的指令组合实测,不要假设一个数字,因为体积主要被账户引用占据。
手续费该由用户还是应用来付? 都行,但要尽早决定。用户付意味着他们需要 SOL 和一次钱包交互;应用付意味着你的成本随使用量增长,必须从一开始就进入利润模型。
拥堵时的积压该怎么处理? 在你自己的队列里按价值排优先级,让低价值的活儿等着,给队列深度设上限,并按排空速率而不是深度告警。正在排空的深队列不是问题。
address lookup table 对高吞吐应用有用吗? 往往作用很大,因为每个 32 字节的账户引用通常正是限制批次大小的东西。它在这里很合适,因为接收方集合往往稳定且会复用。
怎么判断批量到底有没有起作用? 追踪每笔交易落块的指令数和每个动作的手续费成本,而不是交易笔数。用更少的交易完成同样的活儿,才是你想要的结果。
能并行发送多个批次吗? 能,前提是限制并发、并且这些批次不写同一批账户。在共享账户上争用的批次会串行化,不管你一次发多少。
高吞吐的 Solana 系统该监控什么? 队列排空速率、每个批次的落块数对尝试数、带失败指令定位的 revert 率、按分布看的 slot 距离,以及每个已完成动作的手续费成本。