Logo
新用户订阅 RPC,首月享 6.5 折优惠查看优惠
OnFinality Learn
集成与开发阅读约 13 分钟

Solana getMultipleAccounts:批量账户读取

使用 getMultipleAccounts 在单个 JSON-RPC 请求中读取多个 Solana 账户,安全地对齐有序结果,并减少重复的往返请求。

TL;DR

getMultipleAccounts 是 Solana JSON-RPC 方法,它通过 base58 公钥在单个请求中读取一个有界数组的账户,并返回与输入顺序相同的账户条目数组。每个元素要么是账户对象,要么是在请求的 commitment 下账户不存在时的 null,结果数组长度始终等于输入长度,因此调用方必须按索引对齐,而不是过滤 null。响应信封携带 context.slot,用于标识整个批次读取所依据的快照,以及一个 value 数组。选择 jsonParsed 或 base64 编码会改变负载大小和客户端解析工作,而每请求的账户上限被记录为有界但因提供商而异。本指南涵盖该方法语义、一个可运行的 Node.js 示例、一个可复现的测量表、限制,以及针对差一错误对齐和意外 null 的故障排除。

Solana 客户端中的账户读取问题

钱包 UI、索引器或交易机器人很少只需要一个账户。它需要一个集合:用户的代币账户、几个 mint 账户、一些程序拥有的状态账户,可能还有一个费用支付者。朴素的实现会遍历公钥,为每个账户调用一次 getAccountInfo,这会使延迟乘以账户数量,并且每次调用消耗一个提供商预算单位。在每隔几秒刷新一次的页面上,这个循环就是响应式 UI 和重复请求队列之间的区别。(参见 Solana 官方 getAccountInfo 参考文档)

单账户方法在 Solana getAccountInfo 参考文档(https://solana.com/docs/rpc/http/getaccountinfo)中有记录,当你确实只需要一个账户时,它仍然是正确的工具。批量读取接口之所以存在,是因为常见情况是在同一时间点读取许多账户。如果你仍在构建单账户的心智模型,请先阅读[读取 Solana 账户:getAccountInfo 与租金](/en/learn/solana-account-info-rent-token-accounts-rpc),然后再在其上叠加批处理。

成本模型在这里很重要。你发送的每个 JSON-RPC 请求都是端点的一个工作单位,而提供商按请求而非按账户计量。用一次携带 N 个公钥的调用替换 N 次调用的循环,会同时改变往返次数和预算占用。确切的计量方式因提供商而异,因此请将任何具体数字视为提供商特定,并对照你的套餐进行验证。

  • N 次顺序 getAccountInfo 调用 = N 次往返和 N 个预算单位。
  • 一次 getMultipleAccounts 调用 = 一次携带 N 个公钥的往返。
  • 批次针对单个 context slot 读取,这比 N 次独立读取提供更强的一致性保证。

getMultipleAccounts 返回什么以及为什么顺序有保证

Solana getMultipleAccounts 参考文档(https://solana.com/docs/rpc/http/getmultipleaccounts)将该方法记录为接受一个 base58 编码公钥数组并返回一个账户条目数组。关键契约是位置性的:结果 value[i] 对应输入 pubkeys[i]。数组长度等于输入长度,缺失的账户在其索引处表示为 null,而不是被省略。

这种原地 null 行为是大多数集成出错的地方。如果你在对齐之前过滤 null,第一个缺失账户之后的每个索引都会偏移,你会悄悄地将错误的账户数据附加到错误的公钥上。安全的模式是按索引遍历输入数组,并为每个 i 读取 value[i],将 null 视为一等结果。

响应信封将数组包裹在 context 对象中。context.slot 字段标识批次读取所依据的 bank slot,并且由于一次调用中的所有账户共享该 slot,该批次是一致快照。与 N 次单独的 getAccountInfo 调用相比,这是一个有意义的优势,后者可能落在不同的 slot 上,并产生相关状态的撕裂视图。

  • value.length === pubkeys.length,始终如此。
  • value[i] 是 pubkeys[i] 的账户对象,如果未找到则为 null。
  • context.slot 是整个批次的快照标识。
  • 在将结果与输入重新配对之前,绝不要丢弃 null。

编码选择:jsonParsed 与 base64

getMultipleAccounts 接受一个 encoding 参数,用于控制账户数据的序列化方式。base64 返回编码为字符串的原始账户字节,紧凑且明确,但需要客户端反序列化。jsonParsed 要求节点将已知的账户布局(例如 SPL Token 账户)解码为结构化 JSON,这很方便,但会产生更大的负载,并且取决于节点是否识别账户所有者的布局。

权衡在于负载大小与客户端工作量。对于一百个代币账户的批次,jsonParsed 在传输中可能比 base64 大几倍,并且每次刷新都要支付额外的字节。对于具有自定义布局的程序拥有账户的批次,jsonParsed 可能只返回原始数据,因此 base64 加上你自己的解码器通常是更可预测的选择。

编码是每请求参数,因此你可以在不同调用中混合策略:对小型、面向人类的余额显示使用 jsonParsed,对高频索引器循环使用 base64。方法参考文档记录了可接受的编码;请确认你的提供商启用了哪些编码,因为支持情况虽有记录但可能因提供商而异。

  • base64:负载最小,客户端反序列化。
  • jsonParsed:对可识别布局提供结构化输出,负载更大。
  • 编码是每请求的,因此不同调用点可以做出不同选择。

getMultipleAccounts 与 getAccountInfo 和 getProgramAccounts 的对比

这三个读取接口回答不同的问题。getAccountInfo 按公钥读取恰好一个账户。getMultipleAccounts 在一个请求中按公钥读取一个有界账户集合。getProgramAccounts 扫描某个程序拥有的所有账户,可选过滤,这是一种根本不同的操作,具有不同的成本特征。程序范围扫描及其流式对应物在 getProgramAccounts 索引器账户流中有所介绍。

当你已经知道公钥并希望它们在同一个 slot 时,选择 getMultipleAccounts。当你不知道公钥并需要发现时,选择 getProgramAccounts。当你需要恰好一个账户并希望尽可能简单的调用,或者当你需要与批次其余部分不同的每次调用 commitment 时,选择 getAccountInfo。

还有第三个选项值得提及:JSON-RPC 2.0 批处理,它将多个独立方法调用包装在一个传输请求中。JSON-RPC 2.0 规范(https://www.jsonrpc.org/specification)定义了此信封,当你需要每次调用参数或每次调用 commitment 时,它是正确的工具。一批 getAccountInfo 调用会给你独立的结果和独立的错误;一次 getMultipleAccounts 调用会给你一个有序数组和一个 context slot。传输层机制在 JSON-RPC 批处理最佳实践中有所介绍。

  • getAccountInfo:一个公钥,一个账户,最简单的调用。
  • getMultipleAccounts:多个公钥,一个请求,有序数组,一个 slot。
  • getProgramAccounts:按程序所有者发现,而不是按已知公钥。
  • JSON-RPC 批处理:多个独立调用,每次调用参数和错误。

使用 @solana/web3.js 的可运行 Node.js 示例

下面的示例使用 @solana/web3.js,它在 Connection 上暴露 getMultipleAccountsInfo。它构建一个公钥数组,故意混合一个现有账户和一个不存在的账户,调用该方法,打印 context slot,然后按索引将结果与输入重新配对。重新配对这一步是要复制到生产代码中的部分。

将端点 URL 替换为你自己的 RPC 端点。该示例为每个索引打印 present 或 null,以便你可以看到位置契约的实际运作。请注意,不存在的公钥是一个有效的 base58 字符串,只是没有账户,这正是产生 null 而不是错误的情况。

import { Connection, PublicKey } from '@solana/web3.js';

const connection = new Connection('https://your-solana-rpc-endpoint', 'confirmed');

// A real, well-known account plus a valid-but-nonexistent pubkey.
const existing = new PublicKey('11111111111111111111111111111111');
const missing = new PublicKey('So11111111111111111111111111111111111111112');
const inputs = [existing, missing];

const res = await connection.getMultipleAccountsInfo(inputs);

console.log('context slot:', res.context.slot);
res.value.forEach((account, i) => {
  const label = account ? 'present' : 'null';
  console.log(`index ${i} ${inputs[i].toBase58()} -> ${label}`);
});

// Safe zip: iterate inputs by index, never filter nulls first.
const zipped = inputs.map((pubkey, i) => ({
  pubkey: pubkey.toBase58(),
  account: res.value[i] ?? null,
}));
console.log(zipped);

通过 fetch 的可运行原始 JSON-RPC 示例

如果你不使用 @solana/web3.js,同样的调用就是一个普通的 JSON-RPC POST。params 数组是 [pubkeys, options],其中 options 携带 encoding 和 commitment。响应形状是标准的 JSON-RPC 结果信封,包含 context 和 value。

此示例使用 base64 编码,并打印 slot 以及 present/null 摘要。它有意保持最小化,以便你可以将其粘贴到脚本中并指向任何端点。方法参考文档记录了确切的参数顺序;将公钥数组放在第一位,选项对象放在第二位。

const endpoint = 'https://your-solana-rpc-endpoint';

const body = {
  jsonrpc: '2.0',
  id: 1,
  method: 'getMultipleAccounts',
  params: [
    [
      '11111111111111111111111111111111',
      'So11111111111111111111111111111111111111112'
    ],
    { encoding: 'base64', commitment: 'confirmed' }
  ]
};

const response = await fetch(endpoint, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(body)
});

const json = await response.json();
if (json.error) throw new Error(JSON.stringify(json.error));

const { context, value } = json.result;
console.log('context slot:', context.slot);
value.forEach((account, i) => {
  console.log(`index ${i} -> ${account ? 'present' : 'null'}`);
});

可复现的测量:填写你自己的结果表

提供商行为、网络条件和账户大小都会影响真实数字,因此诚实的做法是针对你自己的端点进行测量,而不是信任已发布的数字。下表是一个模板。运行相同的工作负载两次:一次作为 N 次 getAccountInfo 调用的循环,一次作为单个 getMultipleAccounts 调用,并记录观察到的值。

使用固定的公钥集和固定的 commitment,以确保比较公平。每个变体运行多次并记录中位数而不是单个样本,因为冷启动后的第一次调用不具代表性。如果你的提供商暴露请求计数器,请记录消耗的预算单位以及墙钟时间。

  • 输入数量:批次中的公钥数量。
  • 请求数量:getMultipleAccounts 为 1,循环为 N。
  • 单循环墙钟时间:重复运行的中位数。
  • getMultipleAccounts 墙钟时间:重复运行的中位数。
  • 遇到的 null:结果中 null 条目的数量。
  • Context slot:批次报告的 slot。

批量账户读取的限制与权衡

每请求的账户上限被记录为有界但因提供商而异,因此在一个端点上可行的批次可能在另一个端点上被拒绝。常被引用的上限大约是一百个公钥,但请将其视为验证的起点,而不是保证。如果你需要的账户超过上限允许的数量,请拆分为多个调用,并接受它们可能落在不同的 slot 上。

格式错误的参数会导致整个调用失败。如果数组中的一个公钥不是有效的 base58,请求会报错,而不是在该索引处返回 null,因此在发送前验证输入。这与有效但没有账户的公钥不同,后者会产生 null。

null 意味着在该 slot 未找到,而不是空账户。一个账户可能在一个 slot 存在而在另一个 slot 不存在,因此 null 是关于请求的 commitment 和 slot 的陈述,而不是公钥的永久属性。非常大的批次即使账户数量在界限内,也可能超过提供商请求大小限制,因为负载大小取决于账户数据长度和编码。

  • 每请求账户上限:有记录,因提供商而异。
  • 一个无效公钥会导致整个调用失败。
  • null 意味着在该 slot 未找到,而不是空账户。
  • 大批次可能触及与账户数量无关的请求大小限制。

排查差一错误对齐和意外 null

差一错误对齐几乎总是来自在重新配对之前过滤或排序结果数组。如果你调用 value.filter(Boolean) 然后索引过滤后的数组,第一个 null 之后的每个条目都会错位。修复方法是保留原始数组,并为每个输入索引读取 value[i],如上面的示例所示。

意外的 null 通常意味着账户在请求的 commitment 下不存在,或者公钥正确但账户已关闭。检查 context slot,如果你怀疑存在竞争,请在不同的 commitment 下重新读取。如果某个你认为存在的账户出现 null,请验证公钥编码并确认你查询的是预期的集群。

大小限制错误表现为 JSON-RPC 错误,而不是部分结果。如果批次被拒绝,请减少账户数量,从 jsonParsed 切换到 base64 以缩小负载,或拆分为多个调用。对于不能错过 slot 的索引器,请将批量读取与间隙检测配对,如 getBlocks 与跳过 slot 实现无间隙索引中所述。

  • 错位:由重新配对前过滤或排序引起。
  • 意外 null:检查 commitment、slot、集群和公钥编码。
  • 大小限制错误:减少数量、切换编码或拆分批次。

生产批量读取的后续步骤

首先将你最热的 N 次调用循环替换为单个 getMultipleAccounts 调用,然后使用上面的结果表测量变化。对于确实只需要一个账户的情况,保留单账户路径;对于需要每次调用参数或每次调用 commitment 的情况,保留 JSON-RPC 批处理路径。

有关端点选择和网络详细信息,请参阅 Solana 网络页面。有关 Solana RPC 使用的更广泛介绍,Solana API 指南(RPC Assistant)涵盖了端点和常用方法。当你准备根据测量的请求量来规划套餐时,请查看 RPC 定价和 API 服务选项。

最后,浏览 OnFinality Learn 中心获取相关集成指南。批量读取接口是更大读取策略的一部分,该策略包括单账户读取、程序扫描和无间隙索引。

  • 将热 N 次调用循环替换为一次 getMultipleAccounts 调用。
  • 保留 getAccountInfo 用于单账户读取。
  • 保留 JSON-RPC 批处理用于每次调用参数和错误。
  • 使用你自己的结果表测量前后变化。

永远不用担心基础设施

OnFinality 消除了 DevOps 的繁重工作,让您能够更聪明、更快地构建。

开始