go ethClient复用安全性说明
Client复用安全性说明
问题:多个协程共享连接会混乱吗?
答案:不会混乱,是安全的!
技术原理
1. ethclient.Client 是线程安全的
根据 Go 以太坊库的设计:
ethclient.Client底层基于rpc.Clientrpc.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共享
- 减少连接数,降低内存占用

浙公网安备 33010602011771号