go ethClient复用安全性说明

Client复用安全性说明

问题:多个协程共享连接会混乱吗?

答案:不会混乱,是安全的!

技术原理

1. ethclient.Client 是线程安全的

根据 Go 以太坊库的设计:

  • ethclient.Client 底层基于 rpc.Client
  • rpc.Client线程安全的,允许多个 goroutine 并发调用
  • 底层的 net.Conn 和 WebSocket 连接也是线程安全的

2. RPC 请求/响应匹配机制

RPC 客户端使用请求ID来匹配请求和响应:

请求1: {id: 1, method: "eth_getBalance", params: [...]}
请求2: {id: 2, method: "eth_getBlock", params: [...]}
请求3: {id: 3, method: "eth_getTransaction", params: [...]}

响应1: {id: 1, result: "0x123..."}  ← 匹配请求1
响应2: {id: 3, result: {...}}      ← 匹配请求3
响应3: {id: 2, result: {...}}      ← 匹配请求2

关键点

  • 每个请求都有唯一的ID
  • 响应通过ID匹配,不会混乱
  • 即使响应顺序与请求顺序不同,也能正确匹配

3. 代码验证

查看 BatchGetBalances 的实现:

// 构造批量RPC调用请求
requests := make([]rpc.BatchElem, len(addresses))
for i, address := range addresses {
    requests[i] = rpc.BatchElem{
        Method: "eth_getBalance",
        Args:   []interface{}{address.Hex(), "latest"},
        Result: new(string),  // 每个请求有自己的结果存储
    }
}

// 执行批量调用
if err := rpcClient.BatchCallContext(ctx, requests); err != nil {
    return nil, err
}

说明

  • 每个请求的 Result 字段是独立的
  • BatchCallContext 内部使用请求ID匹配响应
  • 多个goroutine并发调用是安全的

实际场景验证

场景1:ProcessBlockTaskData 中的两个goroutine

goUtils.RunParallelTasksEx(
    // goroutine 1: 处理区块+交易
    func() error {
        client, _ := GetClient(ctx, rpcAddress)
        defer client.Close()
        // 调用 BatchGetBlocks, BatchGetTransactions
    },
    // goroutine 2: 处理日志
    func() error {
        client, _ := GetClient(ctx, rpcAddress)  // 可以复用同一个client
        defer client.Close()
        // 调用 processBlockLogs
    },
)

如果共享client

  • 两个goroutine使用同一个client
  • 各自的请求有独立的请求ID
  • 响应通过ID匹配,不会混乱 ✅

场景2:aggregateTaskData 中的多个consumer

// 10个consumer并发执行
for i := 0; i < 10; i++ {
    go func() {
        client, _ := GetClient(ctx, rpcAddress)  // 可以共享
        defer client.Close()
        CountAccountTokens(ctx, accounts, client)
    }()
}

如果共享client

  • 10个goroutine使用同一个client
  • 每个goroutine的请求有独立的请求ID
  • 响应通过ID匹配,不会混乱 ✅

优化方案

方案1:Context级别复用(推荐)

在Context中存储client,所有使用该Context的goroutine共享:

// 在Context中存储
func (wc *Context) SetEthClient(client *ethclient.Client)
func (wc *Context) GetEthClient() *ethclient.Client

// GetClient 先检查Context中是否有
func GetClient(ctx workerCommon.Context, rpcAddress string) (*ethclient.Client, error) {
    // 先检查Context中是否有
    if client := ctx.GetEthClient(); client != nil {
        return client, nil
    }
    
    // 没有则创建新的
    client, err := getClient(ctx, rpcAddress)
    if err == nil {
        ctx.SetEthClient(client)  // 存储到Context
    }
    return client, err
}

优点

  • 同一个Context的所有goroutine共享一个client
  • 减少连接数量
  • 自动管理生命周期

方案2:连接池(更复杂,但更灵活)

type ClientPool struct {
    clients chan *ethclient.Client
    maxSize int
    create  func() (*ethclient.Client, error)
}

func (p *ClientPool) Get() (*ethclient.Client, error) {
    select {
    case client := <-p.clients:
        return client, nil
    default:
        return p.create()
    }
}

func (p *ClientPool) Put(client *ethclient.Client) {
    select {
    case p.clients <- client:
    default:
        client.Close()  // 池满了,关闭连接
    }
}

注意事项

1. Context生命周期

  • client的生命周期应该与Context一致
  • 当Context关闭时,应该关闭client
  • 使用 defer client.Close() 确保清理

2. 错误处理

  • 如果client连接断开,需要重新创建
  • 可以添加健康检查机制
  • 失败时从Context中移除,下次重新创建

3. 性能考虑

  • 共享client可以减少连接数
  • 但可能成为瓶颈(如果请求太多)
  • 建议:每个consumer一个client,而不是全局一个

推荐实现

每个consumer复用自己的client

// 在consumeChainTask开始时创建client
func consumeChainTask(ctx workerCommon.Context, consumerID int, ...) {
    // 检查Context中是否有client(这个consumer的)
    client := ctx.GetEthClient()
    if client == nil {
        // 创建新的并存储
        client, _ = GetClient(ctx, rpcAddress)
        ctx.SetEthClient(client)
    }
    
    // 后续所有调用都使用这个client
    // ProcessBlockTaskData 中的两个goroutine可以共享
    // aggregateTaskData 中的每个consumer有自己的client
}

优点

  • 每个consumer一个client(10个consumer = 10个连接)
  • 同一个consumer内的多个goroutine共享
  • 减少连接数:从30个减少到10个
  • 减少内存:从6GB减少到2GB

总结

可以安全复用

  • ethclient.Client 是线程安全的
  • RPC 使用请求ID匹配,不会混乱
  • 多个goroutine可以并发使用同一个client

推荐方案

  • 每个consumer维护一个client
  • 同一个consumer内的goroutine共享
  • 减少连接数,降低内存占用
posted @ 2025-12-05 11:11  若-飞  阅读(4)  评论(0)    收藏  举报