以太坊区块链如何实现高效分页查询?技术难点与优化方向是什么?
摘要:
这是一个非常重要且常见的需求,因为以太坊是一个持续增长的链上数据世界,一次性获取所有数据是不现实的,分页的核心思想是高效、可控地获取大量数据,与传统的中心化数据库(如 MySQL,... 这是一个非常重要且常见的需求,因为以太坊是一个持续增长的链上数据世界,一次性获取所有数据是不现实的,分页的核心思想是高效、可控地获取大量数据。
与传统的中心化数据库(如 MySQL, PostgreSQL)不同,以太坊是一个去中心化的、全球分布的账本,因此它的分页机制有其独特的挑战和实现方式。
传统中心化数据库的分页(为什么以太坊不同?)
在传统数据库中,分页通常很简单,主要依赖 LIMIT 和 OFFSET 子句:
SELECT * FROM transactions ORDER BY block_number DESC LIMIT 10 OFFSET 20;
工作原理:
- 数据库引擎找到排序后的第 21 条记录。
- 然后读取接下来的 10 条记录。
问题所在:
- 性能随
OFFSET增加而急剧下降:当OFFSET值很大时(OFFSET 1000000),数据库必须扫描并跳过前 100 万条记录,这非常消耗资源。 - 数据不稳定性:在你查询和翻页之间,新的数据可能被插入(新的区块被挖出),导致你看到的数据可能重复或遗漏。
以太坊区块链分页的核心挑战
以太坊数据存储在每个节点上,数据结构是“追加式”的,新的区块总是添加在链的末尾,这为分页带来了几个关键挑战:
- 数据量巨大且持续增长:以太坊上有数千万个区块和数十亿笔交易,无法一次性加载。
- 数据是只读的(Append-Only):数据一旦写入,几乎不会被修改或删除,这既是优势(数据稳定),也带来了挑战(如何高效定位)。
- 去中心化:没有单一的“数据库服务器”来执行高效的
OFFSET操作,每个节点都需要独立完成查询。 - 索引的复杂性:要按特定字段(如地址、交易哈希)分页,需要依赖第三方索引服务(如 The Graph, Etherscan API),因为全节点本身不提供所有字段的复杂索引。
以太坊分页的核心方法:基于游标的分页
为了克服 OFFSET 的性能问题,以太坊生态中普遍采用基于游标的分页,这种方法的核心思想是:不要告诉数据库“跳过多少条”,而是告诉它“从哪里开始”。
工作原理:
- 获取第一页数据:执行一个查询,获取 N 条数据(最新的 10 个区块)。
- 记录“游标”:获取这最后一笔数据(或区块)的唯一标识符,这个标识符就是“游标”,对于区块,通常是区块号;对于交易,通常是区块号和交易索引的组合。
- 获取下一页数据:下一次查询时,使用这个游标作为起点,并请求接下来的 N 条数据。
伪代码示例(获取最新的区块):
-
第一页请求:
(图片来源网络,侵删)blocks = getBlocks(sort: "desc", limit: 10)last_block_on_page = blocks[9](假设数组索引从0开始)cursor = last_block_on_page.number// 游标是区块号 12345678- 返回给用户:
blocks[0]到blocks[9]
-
第二页请求:
next_blocks = getBlocks(sort: "desc", limit: 10, startAfter: 12345677)// 注意是startAfter,而不是startAtlast_block_on_page = next_blocks[9]cursor = last_block_on_page.number// 新的游标是 12345667- 返回给用户:
next_blocks[0]到next_blocks[9]
优势:
- 性能稳定:无论你翻到第多少页,查询的性能都基本恒定,因为它总是直接定位到游标位置。
- 数据一致性:只要游标是基于链上数据的唯一标识,就能有效避免数据重复或遗漏的问题。
具体场景下的分页实现
按区块高度分页(最简单)
这是最直接的分页方式,因为区块号是天然递增且唯一的。
- 获取最新区块:查询
block.number的最大值。 - 上一页:
cursor = current_block_number - 1。 - 下一页:
cursor = current_block_number + 1。
使用 web3.js 获取最新的 10 个区块:
const Web3 = require('web3');
const web3 = new Web3('https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID');
async function getLatestBlocks(pageSize = 10) {
try {
// 1. 获取最新的区块号
const latestBlockNumber = await web3.eth.getBlockNumber();
console.log(`Latest block number: ${latestBlockNumber}`);
// 2. 定义查询范围
const fromBlock = latestBlockNumber - pageSize + 1;
const toBlock = latestBlockNumber;
// 3. 获取这一批区块
// 注意:web3.eth.getBlockRange 是一个高级方法,底层会调用 eth_getLogs
// 更通用的做法是循环调用 eth.getBlock
const blocks = [];
for (let i = toBlock; i >= fromBlock; i--) {
const block = await web3.eth.getBlock(i);
blocks.push(block);
}
// 4. 设置游标
const firstBlockOnPage = blocks[blocks.length - 1];
const lastBlockOnPage = blocks[0];
return {
data: blocks,
cursor: {
next: lastBlockOnPage.number - 1, // 用于“下一页”(更早的区块)
prev: firstBlockOnPage.number + 1 // 用于“上一页”(更新的区块)
}
};
} catch (error) {
console.error("Error fetching blocks:", error);
return null;
}
}
// 使用示例
getLatestBlocks(10).then(result => {
if (result) {
console.log("Fetched Blocks:", result.data.map(b => b.number));
console.log("Cursor for next page (older blocks):", result.cursor.next);
}
});
按地址的交易分页(需要索引服务)
如果你想获取某个特定地址(如你的钱包地址)的所有交易并分页,事情会变得复杂,全节点没有为每个地址建立交易索引,所以你需要遍历所有区块,检查每个交易中的 from 和 to 字段,这极其低效。
解决方案:使用第三方索引服务
这些服务(如 The Graph, Alchemy, Etherscan API)已经为你构建了复杂的索引,让你可以高效查询。
示例:使用 Etherscan API 获取地址交易
Etherscan API 的 txlist 和 txlistinternal 端点就使用了基于游标的分页(通过 startblock 和 endblock 参数,或者 page 和 offset,但官方文档更推荐使用 startblock 和 endblock 来模拟游标)。
请求示例:
https://api.etherscan.io/api?
module=account&
action=txlist&
address=0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae&
startblock=0&
endblock=99999999&
page=1&
offset=10&
sort=asc&
apikey=YourApiKeyToken
page和offset: 这是LIMIT/OFFSET的实现,对于少量页面可用,但不推荐用于深度分页。startblock和endblock: 这更接近游标的思想,你可以先获取第一页(page=1, offset=10),记录最后一笔交易的区块号tx.blockNumber,然后下一页的请求就可以设置startblock = tx.blockNumber + 1来避免重复。
示例:使用 The Graph 协议
The Graph 是一个去中心化的索引协议,专为复杂查询设计,你可以为你的数据(如某个地址的交易)定义一个“子图”(Subgraph)。
子图查询示例(GraphQL):
# 获取地址 0x... 的前 10 笔交易,按区块号升序排列
{
account(id: "0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae") {
transactions(first: 10, orderBy: blockNumber, orderDirection: asc) {
id
blockNumber
from
to
value
}
}
}
# 获取下一页
# 假设上一页最后一笔交易的 id 是 "0x..."
{
account(id: "0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae") {
transactions(
first: 10,
orderBy: blockNumber,
orderDirection: asc,
# 关键:游标
skip: 1, # 跳过上一页的最后一条记录
after: "0x_last_transaction_id_from_previous_page"
) {
id
blockNumber
from
to
value
}
}
}
The Graph 的 first, last, skip, after, before 等参数是游标分页的标准实现。
总结与最佳实践
| 特性 | 传统 LIMIT/OFFSET 分页 |
以太坊游标分页 |
|---|---|---|
| 原理 | 跳过 N 条,取 M 条 | 从特定点开始,取 M 条 |
| 性能 | OFFSET 越大,性能越差 |
性能稳定,与页码无关 |
| 数据一致性 | 易受新数据插入影响 | 更稳定,但需正确实现游标 |
| 适用场景 | 中心化数据库,数据量不大 | 以太坊等区块链,任何大数据集 |
最佳实践:
- 优先选择游标分页:在任何可能的情况下,都应基于唯一、有序的标识符(如区块号、交易哈希、事件日志索引)来实现分页。
- 利用索引服务:对于非主键查询(如按地址、按合约事件查询),不要直接查询全节点,使用 The Graph、Alchemy、Etherscan API 等专业的索引服务,它们已经为你解决了性能和索引的难题。
- 明确游标的生成规则:在 API 设计中,清晰地定义如何生成和使用游标(“
cursor是上一页最后一个区块的blockNumber”)。 - 处理边界情况:考虑当游标指向的数据不存在或已被删除时的处理逻辑。
- 为不同场景选择不同工具:
- 浏览最新活动:直接调用
eth.getBlock进行分页。 - 查询钱包历史:使用 Etherscan API 或 The Graph。
- 分析 DeFi 协议数据:强烈推荐使用 The Graph,因为它灵活、高效且是去中心化的。
- 浏览最新活动:直接调用
通过理解并应用这些方法,你就可以在以太坊这个庞大的数据海洋中,高效、准确地“航行”了。
作者:咔咔本文地址:https://jits.cn/content/27225.html发布于 02-21
文章转载或复制请以超链接形式并注明出处杰思科技・AI 股讯



还没有评论,来说两句吧...