1. 引言
在 DEX 链上数据索引器的设计中,NewScanner 构造函数承担着初始化扫描器的核心职责。它接收 config.Config 与 *sql.DB 两个参数,其中 db 参数看似简单,却在后续的断点续扫、池子状态恢复等关键流程中扮演着不可替代的角色。本文将从源码出发,逐行拆解 NewScanner 内部与 db 产生的每一次交互,剖析其设计意图与实现细节。
2. 函数签名与整体职责
func NewScanner(config config.Config, db *sql.DB) (*Scanner, error)
该函数接收两个参数:
config:应用配置,包含 RPC 节点地址、起始区块、合约地址等。db:标准库database/sql的数据库连接句柄。
返回值是 *Scanner 与 error。Scanner 结构体持有区块链客户端、数据库句柄、配置、池子集合、当前扫描高度等状态。db 被直接存入 Scanner 结构体,意味着后续所有扫描逻辑都能随时访问数据库。
3. 第一次交互:加载历史池子
3.1 查询语句
rows, err := db.Query("SELECT address FROM pools")
if err != nil {
return nil, fmt.Errorf("failed to load pools: %v", err)
}
defer rows.Close()
这是 NewScanner 中与 db 的第一次实际交互。它从 pools 表中读取所有已记录的池子地址,目的是在内存中重建池子集合。
3.2 逐行扫描
for rows.Next() {
var addr string
if err := rows.Scan(&addr); err != nil {
continue
}
scanner.Pools[common.HexToAddress(addr)] = true
}
每一行记录被扫描为字符串形式的地址,再通过 common.HexToAddress 转换为 common.Address 类型,作为 Pools map 的键存入。Pools 的类型是 map[common.Address]bool,这里用 true 作为占位值,表示该地址已被索引。
3.3 设计意图
这一步的意义在于状态恢复。扫描器重启后,内存中的 Pools 是空的。如果不从数据库恢复,扫描器会误以为所有池子都是新建的,从而重复处理历史事件。通过加载历史池子,扫描器能够跳过已索引的池子,只关注新增池子。
4. 第二次交互:断点续扫
4.1 获取网络标识
network := getNetworkFromURL(config.RPC.Url)
在查询扫描高度之前,先从 RPC URL 中提取网络标识(如 mainnet、sepolia)。这是因为同一个数据库可能服务于多个网络,需要按网络区分扫描进度。
4.2 查询扫描高度
var lastBlock sql.NullInt64
err = db.QueryRow("SELECT last_block FROM indexed_status WHERE network = $1", network).Scan(&lastBlock)
这里使用了参数化查询,$1 是 PostgreSQL 风格的占位符,network 作为参数传入。sql.NullInt64 用于处理数据库中的 NULL 值——如果该网络从未被索引过,last_block 字段可能为 NULL。
4.3 分支处理
if err == nil && lastBlock.Valid {
scanner.Current = uint64(lastBlock.Int64) + 1
log.Printf("Resuming from indexed_status: network=%s, last_block=%d, starting from block %d", network, lastBlock.Int64, scanner.Current)
} else {
scanner.Current = uint64(config.RPC.StartBlock)
log.Printf


316

被折叠的 条评论
为什么被折叠?



