11 Commits
Author SHA1 Message Date
mym b90d7ec808 Merge pull request 'Update README to change the go get command to use the correct repository URL for the reconnect package.' (#2) from reconnect-mym into main
Reviewed-on: #2
2025-12-12 15:55:56 +08:00
mayiming a47462ffd4 Update README to change the go get command to use the correct repository URL for the reconnect package. 2025-12-12 15:55:25 +08:00
mym dfe2b6c806 Merge pull request 'reconnect-mym' (#1) from reconnect-mym into main
Reviewed-on: #1
2025-12-12 15:50:43 +08:00
mayiming c68b77b1b7 Add example implementations for etcd, PostgreSQL, and Redis clients demonstrating automatic reconnection management and basic operations. 2025-12-12 15:49:00 +08:00
mayiming 597e1cf840 Add RedisClient implementation for managing Redis connections, including methods for connecting, closing, pinging, and checking connection status. 2025-12-12 15:48:54 +08:00
mayiming 4382d345f6 Add Reconnect package for managing automatic reconnections, including a Reconnectable interface, configuration options, and a Manager for connection monitoring and retries. 2025-12-12 15:48:48 +08:00
mayiming a8180f5115 Add PostgresClient implementation for managing PostgreSQL connections, including methods for connecting, closing, pinging, and checking connection status. 2025-12-12 15:48:39 +08:00
mayiming 915b5dd0bb Add EtcdClient implementation for managing etcd connections with methods for connecting, closing, pinging, and checking connection status. 2025-12-12 15:48:33 +08:00
mayiming 896d518c54 Add go.mod and go.sum files for dependency management 2025-12-12 15:48:25 +08:00
mayiming c70c2e9f1c 修改README 2025-12-12 15:48:13 +08:00
mym 65fe52ceb0 Initial commit 2025-12-12 15:20:58 +08:00
16 changed files with 905 additions and 1085 deletions
+18
View File
@@ -0,0 +1,18 @@
MIT License
Copyright (c) 2025 blueocean-go
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
+218
View File
@@ -0,0 +1,218 @@
# reconnect
一个通用的Go语言重连库,支持Redis、PostgreSQL和etcd的自动重连功能。
## 特性
- 🔄 自动重连机制
- ⚙️ 可配置的重连策略
- 🔍 连接健康检查
- 📊 连接状态监控
- 🎯 支持多种服务(Redis、PostgreSQL、etcd
- 🛡️ 线程安全
## 安装
```bash
go get git.whblueocean.cn/blueocean-go/reconnect
```
## 快速开始
### Redis重连
```go
package main
import (
"context"
"time"
"github.com/go-redis/redis/v8"
"github.com/blueocean-go/reconnect"
)
func main() {
options := &redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
}
config := reconnect.DefaultConfig()
config.RetryInterval = 3 * time.Second
config.OnReconnect = func() {
fmt.Println("Redis重连成功!")
}
manager := reconnect.RedisManager(options, config)
ctx := context.Background()
if err := manager.Start(ctx); err != nil {
log.Fatal(err)
}
defer manager.Stop()
// 使用Redis客户端
redisClient := reconnect.NewRedisClient(options)
redisClient.Connect(ctx)
client := redisClient.GetClient()
// ... 使用client进行操作
}
```
### PostgreSQL重连
```go
package main
import (
"context"
"time"
"github.com/blueocean-go/reconnect"
)
func main() {
dsn := "host=localhost user=postgres password=postgres dbname=testdb sslmode=disable"
config := reconnect.DefaultConfig()
config.RetryInterval = 3 * time.Second
config.OnReconnect = func() {
fmt.Println("PostgreSQL重连成功!")
}
manager := reconnect.PostgresManager(
dsn,
25, // maxConnections
5, // maxIdleConnections
30*time.Minute, // maxConnectionAge
config,
)
ctx := context.Background()
if err := manager.Start(ctx); err != nil {
log.Fatal(err)
}
defer manager.Stop()
// 使用数据库连接
pgClient := reconnect.NewPostgresClient(dsn, 25, 5, 30*time.Minute)
pgClient.Connect(ctx)
db := pgClient.GetDB()
// ... 使用db进行操作
}
```
### etcd重连
```go
package main
import (
"context"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
"github.com/blueocean-go/reconnect"
)
func main() {
etcdConfig := clientv3.Config{
Endpoints: []string{"localhost:2379"},
DialTimeout: 5 * time.Second,
}
config := reconnect.DefaultConfig()
config.RetryInterval = 3 * time.Second
config.OnReconnect = func() {
fmt.Println("etcd重连成功!")
}
manager := reconnect.EtcdManager(etcdConfig, config)
ctx := context.Background()
if err := manager.Start(ctx); err != nil {
log.Fatal(err)
}
defer manager.Stop()
// 使用etcd客户端
etcdClient := reconnect.NewEtcdClient(etcdConfig)
etcdClient.Connect(ctx)
client := etcdClient.GetClient()
// ... 使用client进行操作
}
```
## 配置选项
```go
type Config struct {
// MaxRetries 最大重试次数,0表示无限重试
MaxRetries int
// RetryInterval 重试间隔
RetryInterval time.Duration
// Timeout 连接超时时间
Timeout time.Duration
// OnReconnect 重连成功后的回调函数
OnReconnect func()
// OnDisconnect 断开连接时的回调函数
OnDisconnect func(error)
}
```
## API文档
### Manager
重连管理器,负责监控连接状态并自动重连。
- `Start(ctx context.Context) error` - 启动连接并开始监控
- `Stop() error` - 停止重连管理器
- `IsConnected() bool` - 返回当前连接状态
- `WaitForError() error` - 等待错误(用于阻塞等待)
### RedisClient
Redis客户端包装器。
- `Connect(ctx context.Context) error` - 建立连接
- `Close() error` - 关闭连接
- `Ping(ctx context.Context) error` - 健康检查
- `IsConnected() bool` - 检查连接状态
- `GetClient() *redis.Client` - 获取底层Redis客户端
### PostgresClient
PostgreSQL客户端包装器。
- `Connect(ctx context.Context) error` - 建立连接
- `Close() error` - 关闭连接
- `Ping(ctx context.Context) error` - 健康检查
- `IsConnected() bool` - 检查连接状态
- `GetDB() *sql.DB` - 获取底层数据库连接
### EtcdClient
etcd客户端包装器。
- `Connect(ctx context.Context) error` - 建立连接
- `Close() error` - 关闭连接
- `Ping(ctx context.Context) error` - 健康检查
- `IsConnected() bool` - 检查连接状态
- `GetClient() *clientv3.Client` - 获取底层etcd客户端
## 示例
更多示例代码请查看 `examples/` 目录。
## 许可证
MIT License
Copyright (c) 2025 blueocean-go
-117
View File
@@ -1,117 +0,0 @@
package reconnect
import "time"
// StrategyType 定义重连策略类型
type StrategyType string
const (
// StrategyExponentialBackoff 指数退避策略
StrategyExponentialBackoff StrategyType = "exponential"
// StrategyFixedInterval 固定间隔策略
StrategyFixedInterval StrategyType = "fixed"
// StrategyLinearBackoff 线性退避策略
StrategyLinearBackoff StrategyType = "linear"
)
// ConnectionState 连接状态
type ConnectionState int
const (
// StateDisconnected 断开连接
StateDisconnected ConnectionState = iota
// StateConnecting 正在连接
StateConnecting
// StateConnected 已连接
StateConnected
// StateReconnecting 正在重连
StateReconnecting
)
func (s ConnectionState) String() string {
switch s {
case StateDisconnected:
return "disconnected"
case StateConnecting:
return "connecting"
case StateConnected:
return "connected"
case StateReconnecting:
return "reconnecting"
default:
return "unknown"
}
}
// Config 重连配置
type Config struct {
// Strategy 重连策略类型
Strategy StrategyType
// MaxRetries 最大重试次数,-1 表示无限重试
MaxRetries int
// InitialDelay 初始延迟时间
InitialDelay time.Duration
// MaxDelay 最大延迟时间
MaxDelay time.Duration
// Multiplier 指数退避乘数(仅用于指数退避策略)
Multiplier float64
// LinearIncrement 线性增量(仅用于线性退避策略)
LinearIncrement time.Duration
// HealthCheckInterval 健康检查间隔
HealthCheckInterval time.Duration
// HealthCheckTimeout 健康检查超时时间
HealthCheckTimeout time.Duration
// OnStateChange 状态变化回调函数
OnStateChange func(oldState, newState ConnectionState)
// OnReconnect 重连回调函数(重连成功时调用)
OnReconnect func(attempt int)
// OnError 错误回调函数
OnError func(err error)
}
// DefaultConfig 返回默认配置
func DefaultConfig() Config {
return Config{
Strategy: StrategyExponentialBackoff,
MaxRetries: -1, // 无限重试
InitialDelay: 1 * time.Second,
MaxDelay: 30 * time.Second,
Multiplier: 2.0,
LinearIncrement: 1 * time.Second,
HealthCheckInterval: 10 * time.Second,
HealthCheckTimeout: 5 * time.Second,
}
}
// Validate 验证配置
func (c *Config) Validate() {
if c.InitialDelay <= 0 {
c.InitialDelay = 1 * time.Second
}
if c.MaxDelay <= 0 {
c.MaxDelay = 30 * time.Second
}
if c.Multiplier <= 0 {
c.Multiplier = 2.0
}
if c.LinearIncrement <= 0 {
c.LinearIncrement = 1 * time.Second
}
if c.HealthCheckInterval <= 0 {
c.HealthCheckInterval = 10 * time.Second
}
if c.HealthCheckTimeout <= 0 {
c.HealthCheckTimeout = 5 * time.Second
}
}
-170
View File
@@ -1,170 +0,0 @@
package reconnect
import (
"context"
"database/sql"
"sync"
"time"
"gorm.io/gorm"
)
// DatabaseConfig 数据库配置
type DatabaseConfig struct {
// Dialector gorm dialector(如 postgres.Open(dsn), mysql.Open(dsn)
Dialector gorm.Dialector
// GormConfig gorm配置
GormConfig *gorm.Config
// MaxIdleConns 最大空闲连接数
MaxIdleConns int
// MaxOpenConns 最大打开连接数
MaxOpenConns int
// ConnMaxLifetime 连接最大生命周期
ConnMaxLifetime time.Duration
// ReconnectConfig 重连配置
ReconnectConfig Config
}
// DatabaseClient 带重连功能的数据库客户端
type DatabaseClient struct {
config DatabaseConfig
db *gorm.DB
sqlDB *sql.DB
manager *ConnectionManager
mu sync.RWMutex
}
// databaseConnector 数据库连接器
type databaseConnector struct {
client *DatabaseClient
}
func (d *databaseConnector) Connect(ctx context.Context) error {
d.client.mu.Lock()
defer d.client.mu.Unlock()
gormConfig := d.client.config.GormConfig
if gormConfig == nil {
gormConfig = &gorm.Config{}
}
db, err := gorm.Open(d.client.config.Dialector, gormConfig)
if err != nil {
return err
}
sqlDB, err := db.DB()
if err != nil {
return err
}
// 配置连接池
if d.client.config.MaxIdleConns > 0 {
sqlDB.SetMaxIdleConns(d.client.config.MaxIdleConns)
}
if d.client.config.MaxOpenConns > 0 {
sqlDB.SetMaxOpenConns(d.client.config.MaxOpenConns)
}
if d.client.config.ConnMaxLifetime > 0 {
sqlDB.SetConnMaxLifetime(d.client.config.ConnMaxLifetime)
}
// 验证连接
if err := sqlDB.PingContext(ctx); err != nil {
sqlDB.Close()
return err
}
d.client.db = db
d.client.sqlDB = sqlDB
return nil
}
func (d *databaseConnector) Close() error {
d.client.mu.Lock()
defer d.client.mu.Unlock()
if d.client.sqlDB != nil {
err := d.client.sqlDB.Close()
d.client.db = nil
d.client.sqlDB = nil
return err
}
return nil
}
// databaseHealthChecker 数据库健康检查器
type databaseHealthChecker struct {
client *DatabaseClient
}
func (d *databaseHealthChecker) HealthCheck(ctx context.Context) error {
d.client.mu.RLock()
sqlDB := d.client.sqlDB
d.client.mu.RUnlock()
if sqlDB == nil {
return context.Canceled
}
return sqlDB.PingContext(ctx)
}
// NewDatabaseClient 创建带重连功能的数据库客户端
func NewDatabaseClient(cfg DatabaseConfig) (*DatabaseClient, error) {
client := &DatabaseClient{
config: cfg,
}
connector := &databaseConnector{client: client}
checker := &databaseHealthChecker{client: client}
client.manager = NewConnectionManager(connector, checker, cfg.ReconnectConfig)
// 首次连接
ctx := context.Background()
if err := client.manager.ConnectWithRetry(ctx); err != nil {
return nil, err
}
return client, nil
}
// GetDB 获取gorm.DB实例
func (c *DatabaseClient) GetDB() *gorm.DB {
c.mu.RLock()
defer c.mu.RUnlock()
return c.db
}
// GetSqlDB 获取sql.DB实例
func (c *DatabaseClient) GetSqlDB() *sql.DB {
c.mu.RLock()
defer c.mu.RUnlock()
return c.sqlDB
}
// State 获取连接状态
func (c *DatabaseClient) State() ConnectionState {
return c.manager.State()
}
// Close 关闭客户端
func (c *DatabaseClient) Close() error {
return c.manager.Close()
}
// TriggerReconnect 手动触发重连
func (c *DatabaseClient) TriggerReconnect() {
c.manager.TriggerReconnect()
}
// Ping 执行Ping命令
func (c *DatabaseClient) Ping(ctx context.Context) error {
sqlDB := c.GetSqlDB()
if sqlDB == nil {
return context.Canceled
}
return sqlDB.PingContext(ctx)
}
+60 -96
View File
@@ -2,143 +2,107 @@ package reconnect
import (
"context"
"fmt"
"sync"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
)
// EtcdClientConfig etcd客户端配置
type EtcdClientConfig struct {
// Endpoints etcd服务地址列表
Endpoints []string
// DialTimeout 连接超时时间
DialTimeout time.Duration
// Username 用户名(可选)
Username string
// Password 密码(可选)
Password string
// ReconnectConfig 重连配置
ReconnectConfig Config
}
// EtcdClient 带重连功能的etcd客户端
// EtcdClient etcd客户端包装器
type EtcdClient struct {
config EtcdClientConfig
client *clientv3.Client
manager *ConnectionManager
mu sync.RWMutex
config clientv3.Config
client *clientv3.Client
mu sync.RWMutex
}
// etcdConnector etcd连接器
type etcdConnector struct {
client *EtcdClient
// NewEtcdClient 创建新的etcd客户端
func NewEtcdClient(config clientv3.Config) *EtcdClient {
return &EtcdClient{
config: config,
}
}
func (e *etcdConnector) Connect(ctx context.Context) error {
e.client.mu.Lock()
defer e.client.mu.Unlock()
// Connect 建立etcd连接
func (e *EtcdClient) Connect(ctx context.Context) error {
e.mu.Lock()
defer e.mu.Unlock()
cfg := clientv3.Config{
Endpoints: e.client.config.Endpoints,
DialTimeout: e.client.config.DialTimeout,
Username: e.client.config.Username,
Password: e.client.config.Password,
if e.client != nil {
_ = e.client.Close()
}
if cfg.DialTimeout == 0 {
cfg.DialTimeout = 5 * time.Second
}
cli, err := clientv3.New(cfg)
client, err := clientv3.New(e.config)
if err != nil {
return err
return fmt.Errorf("etcd new client failed: %w", err)
}
// 验证连接
ctx, cancel := context.WithTimeout(ctx, cfg.DialTimeout)
// 测试连接
timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
_, err = cli.Status(ctx, cfg.Endpoints[0])
_, err = client.Status(timeoutCtx, e.config.Endpoints[0])
if err != nil {
cli.Close()
return err
_ = client.Close()
return fmt.Errorf("etcd status check failed: %w", err)
}
e.client.client = cli
e.client = client
return nil
}
func (e *etcdConnector) Close() error {
e.client.mu.Lock()
defer e.client.mu.Unlock()
// Close 关闭etcd连接
func (e *EtcdClient) Close() error {
e.mu.Lock()
defer e.mu.Unlock()
if e.client.client != nil {
err := e.client.client.Close()
e.client.client = nil
if e.client != nil {
err := e.client.Close()
e.client = nil
return err
}
return nil
}
// etcdHealthChecker etcd健康检查器
type etcdHealthChecker struct {
client *EtcdClient
}
// Ping 检查etcd连接是否健康
func (e *EtcdClient) Ping(ctx context.Context) error {
e.mu.RLock()
defer e.mu.RUnlock()
func (e *etcdHealthChecker) HealthCheck(ctx context.Context) error {
e.client.mu.RLock()
cli := e.client.client
endpoints := e.client.config.Endpoints
e.client.mu.RUnlock()
if cli == nil || len(endpoints) == 0 {
return context.Canceled
if e.client == nil {
return fmt.Errorf("etcd client is nil")
}
_, err := cli.Status(ctx, endpoints[0])
if len(e.config.Endpoints) == 0 {
return fmt.Errorf("etcd endpoints is empty")
}
timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
_, err := e.client.Status(timeoutCtx, e.config.Endpoints[0])
return err
}
// NewEtcdClient 创建带重连功能的etcd客户端
func NewEtcdClient(cfg EtcdClientConfig) (*EtcdClient, error) {
client := &EtcdClient{
config: cfg,
}
// IsConnected 检查连接状态
func (e *EtcdClient) IsConnected() bool {
e.mu.RLock()
defer e.mu.RUnlock()
connector := &etcdConnector{client: client}
checker := &etcdHealthChecker{client: client}
client.manager = NewConnectionManager(connector, checker, cfg.ReconnectConfig)
// 首次连接
ctx := context.Background()
if err := client.manager.ConnectWithRetry(ctx); err != nil {
return nil, err
}
return client, nil
return e.client != nil
}
// GetClient 获取etcd客户端
func (c *EtcdClient) GetClient() *clientv3.Client {
c.mu.RLock()
defer c.mu.RUnlock()
return c.client
// GetClient 获取底层的etcd客户端
func (e *EtcdClient) GetClient() *clientv3.Client {
e.mu.RLock()
defer e.mu.RUnlock()
return e.client
}
// State 获取连接状态
func (c *EtcdClient) State() ConnectionState {
return c.manager.State()
}
// Close 关闭客户端
func (c *EtcdClient) Close() error {
return c.manager.Close()
}
// TriggerReconnect 手动触发重连
func (c *EtcdClient) TriggerReconnect() {
c.manager.TriggerReconnect()
// EtcdManager 创建etcd重连管理器
func EtcdManager(config clientv3.Config, reconnectConfig *Config) *Manager {
client := NewEtcdClient(config)
return NewManager(client, reconnectConfig)
}
+73
View File
@@ -0,0 +1,73 @@
package main
import (
"context"
"fmt"
"log"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
"github.com/blueocean-go/reconnect"
)
func main() {
// etcd配置
etcdConfig := clientv3.Config{
Endpoints: []string{"localhost:2379"},
DialTimeout: 5 * time.Second,
}
// 创建重连配置
config := reconnect.DefaultConfig()
config.RetryInterval = 3 * time.Second
config.MaxRetries = 0 // 无限重试
config.OnReconnect = func() {
fmt.Println("etcd重连成功!")
}
config.OnDisconnect = func(err error) {
fmt.Printf("etcd连接断开: %v\n", err)
}
// 创建重连管理器
manager := reconnect.EtcdManager(etcdConfig, config)
// 启动连接
ctx := context.Background()
if err := manager.Start(ctx); err != nil {
log.Fatalf("启动失败: %v", err)
}
defer manager.Stop()
// 获取etcd客户端并使用
etcdClient := reconnect.NewEtcdClient(etcdConfig)
if err := etcdClient.Connect(ctx); err != nil {
log.Fatalf("连接失败: %v", err)
}
client := etcdClient.GetClient()
if client == nil {
log.Fatal("客户端为空")
}
// 使用etcd客户端
kv := clientv3.NewKV(client)
_, err := kv.Put(ctx, "key", "value")
if err != nil {
log.Printf("设置键值失败: %v", err)
} else {
fmt.Println("设置键值成功")
}
resp, err := kv.Get(ctx, "key")
if err != nil {
log.Printf("获取键值失败: %v", err)
} else {
if len(resp.Kvs) > 0 {
fmt.Printf("获取键值: %s = %s\n", resp.Kvs[0].Key, resp.Kvs[0].Value)
}
}
// 保持运行
select {}
}
+65
View File
@@ -0,0 +1,65 @@
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/blueocean-go/reconnect"
)
func main() {
// PostgreSQL连接字符串
dsn := "host=localhost user=postgres password=postgres dbname=testdb sslmode=disable"
// 创建重连配置
config := reconnect.DefaultConfig()
config.RetryInterval = 3 * time.Second
config.MaxRetries = 0 // 无限重试
config.OnReconnect = func() {
fmt.Println("PostgreSQL重连成功!")
}
config.OnDisconnect = func(err error) {
fmt.Printf("PostgreSQL连接断开: %v\n", err)
}
// 创建重连管理器
manager := reconnect.PostgresManager(
dsn,
25, // maxConnections
5, // maxIdleConnections
30*time.Minute, // maxConnectionAge
config,
)
// 启动连接
ctx := context.Background()
if err := manager.Start(ctx); err != nil {
log.Fatalf("启动失败: %v", err)
}
defer manager.Stop()
// 获取数据库连接并使用
pgClient := reconnect.NewPostgresClient(dsn, 25, 5, 30*time.Minute)
if err := pgClient.Connect(ctx); err != nil {
log.Fatalf("连接失败: %v", err)
}
db := pgClient.GetDB()
if db == nil {
log.Fatal("数据库连接为空")
}
// 使用数据库连接
var version string
err := db.QueryRow("SELECT version()").Scan(&version)
if err != nil {
log.Printf("查询失败: %v", err)
} else {
fmt.Printf("PostgreSQL版本: %s\n", version)
}
// 保持运行
select {}
}
+70
View File
@@ -0,0 +1,70 @@
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/blueocean-go/reconnect"
"github.com/redis/go-redis/v9"
)
func main() {
// 创建Redis选项
options := &redis.Options{
Addr: "localhost:6379",
Password: "", // 无密码
DB: 0,
}
// 创建重连配置
config := reconnect.DefaultConfig()
config.RetryInterval = 3 * time.Second
config.MaxRetries = 0 // 无限重试
config.OnReconnect = func() {
fmt.Println("Redis重连成功!")
}
config.OnDisconnect = func(err error) {
fmt.Printf("Redis连接断开: %v\n", err)
}
// 创建重连管理器
manager := reconnect.RedisManager(options, config)
// 启动连接
ctx := context.Background()
if err := manager.Start(ctx); err != nil {
log.Fatalf("启动失败: %v", err)
}
defer manager.Stop()
// 获取Redis客户端并使用
redisClient := reconnect.NewRedisClient(options)
if err := redisClient.Connect(ctx); err != nil {
log.Fatalf("连接失败: %v", err)
}
client := redisClient.GetClient()
if client == nil {
log.Fatal("客户端为空")
}
// 使用Redis客户端
err := client.Set(ctx, "key", "value", 0).Err()
if err != nil {
log.Printf("设置键值失败: %v", err)
} else {
fmt.Println("设置键值成功")
}
val, err := client.Get(ctx, "key").Result()
if err != nil {
log.Printf("获取键值失败: %v", err)
} else {
fmt.Printf("获取键值: %s\n", val)
}
// 保持运行
select {}
}
+22 -23
View File
@@ -1,32 +1,31 @@
module blueocean.local/reconnect
module github.com/blueocean-go/reconnect
go 1.24.0
go 1.21
require (
github.com/go-redis/redis/v8 v8.11.5
go.etcd.io/etcd/client/v3 v3.6.5
google.golang.org/grpc v1.75.1
gorm.io/gorm v1.31.1
github.com/lib/pq v1.10.9
github.com/redis/go-redis/v9 v9.3.0
go.etcd.io/etcd/client/v3 v3.5.10
)
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
go.etcd.io/etcd/api/v3 v3.6.5 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.5 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
google.golang.org/protobuf v1.36.6 // indirect
github.com/golang/protobuf v1.5.3 // indirect
go.etcd.io/etcd/api/v3 v3.5.10 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.10 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.17.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
)
+61 -78
View File
@@ -1,74 +1,56 @@
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0=
github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/etcd/api/v3 v3.6.5 h1:pMMc42276sgR1j1raO/Qv3QI9Af/AuyQUW6CBAWuntA=
go.etcd.io/etcd/api/v3 v3.6.5/go.mod h1:ob0/oWA/UQQlT1BmaEkWQzI0sJ1M0Et0mMpaABxguOQ=
go.etcd.io/etcd/client/pkg/v3 v3.6.5 h1:Duz9fAzIZFhYWgRjp/FgNq2gO1jId9Yae/rLn3RrBP8=
go.etcd.io/etcd/client/pkg/v3 v3.6.5/go.mod h1:8Wx3eGRPiy0qOFMZT/hfvdos+DjEaPxdIDiCDUv/FQk=
go.etcd.io/etcd/client/v3 v3.6.5 h1:yRwZNFBx/35VKHTcLDeO7XVLbCBFbPi+XV4OC3QJf2U=
go.etcd.io/etcd/client/v3 v3.6.5/go.mod h1:ZqwG/7TAFZ0BJ0jXRPoJjKQJtbFo/9NIY8uoFFKcCyo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.etcd.io/etcd/api/v3 v3.5.10 h1:szRajuUUbLyppkhs9K6BRtjY37l66XQQmw7oZRANE4k=
go.etcd.io/etcd/api/v3 v3.5.10/go.mod h1:TidfmT4Uycad3NM/o25fG3J07odo4GBB9hoxaodFCtI=
go.etcd.io/etcd/client/pkg/v3 v3.5.10 h1:kfYIdQftBnbAq8pUWFXfpuuxFSKzlmM5cSn76JByiT0=
go.etcd.io/etcd/client/pkg/v3 v3.5.10/go.mod h1:DYivfIviIuQ8+/lCq4vcxuseg2P2XbHygkKwFo9fc8U=
go.etcd.io/etcd/client/v3 v3.5.10 h1:W9TXNZ+oB3MCd/8UjxHTWK5J9Nquw9fQBLJd5ne5/Ao=
go.etcd.io/etcd/client/v3 v3.5.10/go.mod h1:RVeBnDz2PUEZqTpgqwAtUd8nAPf5kjyFyND7P1VkOKc=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -78,20 +60,20 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -100,22 +82,23 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU=
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY=
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4=
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q=
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
-162
View File
@@ -1,162 +0,0 @@
package reconnect
import (
"context"
"sync"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/connectivity"
"google.golang.org/grpc/credentials/insecure"
)
// GRPCClientConfig gRPC客户端配置
type GRPCClientConfig struct {
// Target gRPC服务地址
Target string
// DialOptions gRPC拨号选项
DialOptions []grpc.DialOption
// ReconnectConfig 重连配置
ReconnectConfig Config
}
// GRPCClient 带重连功能的gRPC客户端
type GRPCClient struct {
config GRPCClientConfig
conn *grpc.ClientConn
manager *ConnectionManager
mu sync.RWMutex
}
// grpcConnector gRPC连接器
type grpcConnector struct {
client *GRPCClient
}
func (g *grpcConnector) Connect(ctx context.Context) error {
g.client.mu.Lock()
defer g.client.mu.Unlock()
opts := g.client.config.DialOptions
if len(opts) == 0 {
opts = []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
}
}
conn, err := grpc.NewClient(g.client.config.Target, opts...)
if err != nil {
return err
}
// 等待连接就绪
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
conn.Connect()
for {
state := conn.GetState()
if state == connectivity.Ready {
break
}
if state == connectivity.TransientFailure || state == connectivity.Shutdown {
conn.Close()
return context.DeadlineExceeded
}
if !conn.WaitForStateChange(ctx, state) {
conn.Close()
return ctx.Err()
}
}
g.client.conn = conn
return nil
}
func (g *grpcConnector) Close() error {
g.client.mu.Lock()
defer g.client.mu.Unlock()
if g.client.conn != nil {
err := g.client.conn.Close()
g.client.conn = nil
return err
}
return nil
}
// grpcHealthChecker gRPC健康检查器
type grpcHealthChecker struct {
client *GRPCClient
}
func (g *grpcHealthChecker) HealthCheck(ctx context.Context) error {
g.client.mu.RLock()
conn := g.client.conn
g.client.mu.RUnlock()
if conn == nil {
return context.Canceled
}
state := conn.GetState()
if state == connectivity.Ready || state == connectivity.Idle {
return nil
}
// 尝试连接并等待
conn.Connect()
if !conn.WaitForStateChange(ctx, state) {
return ctx.Err()
}
newState := conn.GetState()
if newState != connectivity.Ready && newState != connectivity.Idle {
return context.DeadlineExceeded
}
return nil
}
// NewGRPCClient 创建带重连功能的gRPC客户端
func NewGRPCClient(cfg GRPCClientConfig) (*GRPCClient, error) {
client := &GRPCClient{
config: cfg,
}
connector := &grpcConnector{client: client}
checker := &grpcHealthChecker{client: client}
client.manager = NewConnectionManager(connector, checker, cfg.ReconnectConfig)
// 首次连接
ctx := context.Background()
if err := client.manager.ConnectWithRetry(ctx); err != nil {
return nil, err
}
return client, nil
}
// GetConn 获取gRPC连接
func (c *GRPCClient) GetConn() *grpc.ClientConn {
c.mu.RLock()
defer c.mu.RUnlock()
return c.conn
}
// State 获取连接状态
func (c *GRPCClient) State() ConnectionState {
return c.manager.State()
}
// Close 关闭客户端
func (c *GRPCClient) Close() error {
return c.manager.Close()
}
// TriggerReconnect 手动触发重连
func (c *GRPCClient) TriggerReconnect() {
c.manager.TriggerReconnect()
}
-226
View File
@@ -1,226 +0,0 @@
package reconnect
import (
"context"
"log"
"sync"
"sync/atomic"
"time"
)
// HealthChecker 健康检查接口
type HealthChecker interface {
// HealthCheck 执行健康检查,返回 nil 表示健康
HealthCheck(ctx context.Context) error
}
// Connector 连接器接口
type Connector interface {
// Connect 建立连接
Connect(ctx context.Context) error
// Close 关闭连接
Close() error
}
// ConnectionManager 连接管理器
type ConnectionManager struct {
config Config
strategy Strategy
connector Connector
checker HealthChecker
state atomic.Int32
attempts atomic.Int32
mu sync.RWMutex
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
closeOnce sync.Once
}
// NewConnectionManager 创建连接管理器
func NewConnectionManager(connector Connector, checker HealthChecker, cfg Config) *ConnectionManager {
cfg.Validate()
ctx, cancel := context.WithCancel(context.Background())
cm := &ConnectionManager{
config: cfg,
strategy: NewStrategy(cfg),
connector: connector,
checker: checker,
ctx: ctx,
cancel: cancel,
}
cm.state.Store(int32(StateDisconnected))
return cm
}
// State 获取当前连接状态
func (cm *ConnectionManager) State() ConnectionState {
return ConnectionState(cm.state.Load())
}
// setState 设置连接状态
func (cm *ConnectionManager) setState(newState ConnectionState) {
oldState := ConnectionState(cm.state.Swap(int32(newState)))
if oldState != newState && cm.config.OnStateChange != nil {
cm.config.OnStateChange(oldState, newState)
}
}
// Connect 建立连接并启动健康检查
func (cm *ConnectionManager) Connect(ctx context.Context) error {
cm.setState(StateConnecting)
if err := cm.connector.Connect(ctx); err != nil {
cm.setState(StateDisconnected)
if cm.config.OnError != nil {
cm.config.OnError(err)
}
return err
}
cm.setState(StateConnected)
cm.strategy.Reset()
cm.attempts.Store(0)
// 启动健康检查
if cm.checker != nil && cm.config.HealthCheckInterval > 0 {
cm.wg.Add(1)
go cm.healthCheckLoop()
}
return nil
}
// ConnectWithRetry 带重试的连接
func (cm *ConnectionManager) ConnectWithRetry(ctx context.Context) error {
cm.setState(StateConnecting)
for {
select {
case <-ctx.Done():
cm.setState(StateDisconnected)
return ctx.Err()
case <-cm.ctx.Done():
cm.setState(StateDisconnected)
return cm.ctx.Err()
default:
}
attempt := int(cm.attempts.Add(1))
if err := cm.connector.Connect(ctx); err != nil {
if cm.config.OnError != nil {
cm.config.OnError(err)
}
// 检查是否超过最大重试次数
if cm.config.MaxRetries >= 0 && attempt >= cm.config.MaxRetries {
cm.setState(StateDisconnected)
return err
}
delay := cm.strategy.NextDelay(attempt)
log.Printf("[reconnect] 连接失败 (尝试 %d): %v, %v 后重试...", attempt, err, delay)
select {
case <-ctx.Done():
cm.setState(StateDisconnected)
return ctx.Err()
case <-cm.ctx.Done():
cm.setState(StateDisconnected)
return cm.ctx.Err()
case <-time.After(delay):
continue
}
}
// 连接成功
cm.setState(StateConnected)
cm.strategy.Reset()
cm.attempts.Store(0)
if cm.config.OnReconnect != nil && attempt > 1 {
cm.config.OnReconnect(attempt)
}
// 启动健康检查
if cm.checker != nil && cm.config.HealthCheckInterval > 0 {
cm.wg.Add(1)
go cm.healthCheckLoop()
}
log.Printf("[reconnect] 连接成功")
return nil
}
}
// healthCheckLoop 健康检查循环
func (cm *ConnectionManager) healthCheckLoop() {
defer cm.wg.Done()
ticker := time.NewTicker(cm.config.HealthCheckInterval)
defer ticker.Stop()
for {
select {
case <-cm.ctx.Done():
return
case <-ticker.C:
if cm.State() != StateConnected {
continue
}
ctx, cancel := context.WithTimeout(cm.ctx, cm.config.HealthCheckTimeout)
err := cm.checker.HealthCheck(ctx)
cancel()
if err != nil {
log.Printf("[reconnect] 健康检查失败: %v, 开始重连...", err)
if cm.config.OnError != nil {
cm.config.OnError(err)
}
cm.reconnect()
}
}
}
}
// reconnect 执行重连
func (cm *ConnectionManager) reconnect() {
if cm.State() == StateReconnecting {
return // 已经在重连中
}
cm.setState(StateReconnecting)
// 关闭旧连接
_ = cm.connector.Close()
// 重新连接
go func() {
if err := cm.ConnectWithRetry(cm.ctx); err != nil {
log.Printf("[reconnect] 重连失败: %v", err)
}
}()
}
// Close 关闭连接管理器
func (cm *ConnectionManager) Close() error {
var err error
cm.closeOnce.Do(func() {
cm.cancel()
cm.wg.Wait()
err = cm.connector.Close()
cm.setState(StateDisconnected)
})
return err
}
// TriggerReconnect 手动触发重连
func (cm *ConnectionManager) TriggerReconnect() {
cm.reconnect()
}
+106
View File
@@ -0,0 +1,106 @@
package reconnect
import (
"context"
"database/sql"
"fmt"
"sync"
"time"
_ "github.com/lib/pq"
)
// PostgresClient PostgreSQL客户端包装器
type PostgresClient struct {
dsn string
db *sql.DB
mu sync.RWMutex
maxAge time.Duration
}
// NewPostgresClient 创建新的PostgreSQL客户端
func NewPostgresClient(dsn string, maxConnections int, maxIdleConnections int, maxConnectionAge time.Duration) *PostgresClient {
return &PostgresClient{
dsn: dsn,
maxAge: maxConnectionAge,
}
}
// Connect 建立PostgreSQL连接
func (p *PostgresClient) Connect(ctx context.Context) error {
p.mu.Lock()
defer p.mu.Unlock()
if p.db != nil {
_ = p.db.Close()
}
db, err := sql.Open("postgres", p.dsn)
if err != nil {
return fmt.Errorf("postgres open failed: %w", err)
}
// 设置连接池参数
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
if p.maxAge > 0 {
db.SetConnMaxLifetime(p.maxAge)
}
// 测试连接
if err := db.PingContext(ctx); err != nil {
_ = db.Close()
return fmt.Errorf("postgres ping failed: %w", err)
}
p.db = db
return nil
}
// Close 关闭PostgreSQL连接
func (p *PostgresClient) Close() error {
p.mu.Lock()
defer p.mu.Unlock()
if p.db != nil {
err := p.db.Close()
p.db = nil
return err
}
return nil
}
// Ping 检查PostgreSQL连接是否健康
func (p *PostgresClient) Ping(ctx context.Context) error {
p.mu.RLock()
defer p.mu.RUnlock()
if p.db == nil {
return fmt.Errorf("postgres db is nil")
}
return p.db.PingContext(ctx)
}
// IsConnected 检查连接状态
func (p *PostgresClient) IsConnected() bool {
p.mu.RLock()
defer p.mu.RUnlock()
return p.db != nil
}
// GetDB 获取底层的数据库连接
func (p *PostgresClient) GetDB() *sql.DB {
p.mu.RLock()
defer p.mu.RUnlock()
return p.db
}
// PostgresManager 创建PostgreSQL重连管理器
func PostgresManager(dsn string, maxConnections int, maxIdleConnections int, maxConnectionAge time.Duration, config *Config) *Manager {
client := NewPostgresClient(dsn, maxConnections, maxIdleConnections, maxConnectionAge)
return NewManager(client, config)
}
+162
View File
@@ -0,0 +1,162 @@
package reconnect
import (
"context"
"errors"
"fmt"
"time"
)
// Reconnectable 定义可重连的接口
type Reconnectable interface {
// Connect 建立连接
Connect(ctx context.Context) error
// Close 关闭连接
Close() error
// Ping 检查连接是否健康
Ping(ctx context.Context) error
// IsConnected 检查连接状态
IsConnected() bool
}
// Config 重连配置
type Config struct {
// MaxRetries 最大重试次数,0表示无限重试
MaxRetries int
// RetryInterval 重试间隔
RetryInterval time.Duration
// Timeout 连接超时时间
Timeout time.Duration
// OnReconnect 重连成功后的回调函数
OnReconnect func()
// OnDisconnect 断开连接时的回调函数
OnDisconnect func(error)
}
// DefaultConfig 返回默认配置
func DefaultConfig() *Config {
return &Config{
MaxRetries: 0, // 无限重试
RetryInterval: 3 * time.Second,
Timeout: 10 * time.Second,
}
}
// Manager 重连管理器
type Manager struct {
client Reconnectable
config *Config
ctx context.Context
cancel context.CancelFunc
connected bool
errCh chan error
}
// NewManager 创建新的重连管理器
func NewManager(client Reconnectable, config *Config) *Manager {
if config == nil {
config = DefaultConfig()
}
ctx, cancel := context.WithCancel(context.Background())
return &Manager{
client: client,
config: config,
ctx: ctx,
cancel: cancel,
errCh: make(chan error, 1),
}
}
// Start 启动连接并开始监控
func (m *Manager) Start(ctx context.Context) error {
// 初始连接
if err := m.connect(ctx); err != nil {
return fmt.Errorf("initial connection failed: %w", err)
}
// 启动监控 goroutine
go m.monitor()
return nil
}
// Stop 停止重连管理器
func (m *Manager) Stop() error {
m.cancel()
if m.client != nil {
return m.client.Close()
}
return nil
}
// connect 执行连接
func (m *Manager) connect(ctx context.Context) error {
connectCtx, cancel := context.WithTimeout(ctx, m.config.Timeout)
defer cancel()
if err := m.client.Connect(connectCtx); err != nil {
return err
}
m.connected = true
if m.config.OnReconnect != nil {
m.config.OnReconnect()
}
return nil
}
// monitor 监控连接状态并自动重连
func (m *Manager) monitor() {
ticker := time.NewTicker(m.config.RetryInterval)
defer ticker.Stop()
retryCount := 0
for {
select {
case <-m.ctx.Done():
return
case <-ticker.C:
// 检查连接状态
if !m.client.IsConnected() {
m.connected = false
if m.config.OnDisconnect != nil {
m.config.OnDisconnect(errors.New("connection lost"))
}
// 检查是否超过最大重试次数
if m.config.MaxRetries > 0 && retryCount >= m.config.MaxRetries {
m.errCh <- fmt.Errorf("max retries (%d) exceeded", m.config.MaxRetries)
return
}
// 尝试重连
if err := m.connect(context.Background()); err != nil {
retryCount++
continue
}
// 重连成功,重置计数器
retryCount = 0
} else {
// 连接正常,执行健康检查
if err := m.client.Ping(context.Background()); err != nil {
m.connected = false
if m.config.OnDisconnect != nil {
m.config.OnDisconnect(err)
}
}
}
}
}
}
// IsConnected 返回当前连接状态
func (m *Manager) IsConnected() bool {
return m.connected && m.client.IsConnected()
}
// WaitForError 等待错误(用于阻塞等待)
func (m *Manager) WaitForError() error {
return <-m.errCh
}
+50 -88
View File
@@ -2,125 +2,87 @@ package reconnect
import (
"context"
"fmt"
"sync"
"github.com/go-redis/redis/v8"
"github.com/redis/go-redis/v9"
)
// RedisClientConfig Redis客户端配置
type RedisClientConfig struct {
// Options Redis连接选项
Options *redis.Options
// ReconnectConfig 重连配置
ReconnectConfig Config
}
// RedisClient 带重连功能的Redis客户端
// RedisClient Redis客户端包装器
type RedisClient struct {
config RedisClientConfig
options *redis.Options
client *redis.Client
manager *ConnectionManager
mu sync.RWMutex
}
// redisConnector Redis连接器
type redisConnector struct {
client *RedisClient
// NewRedisClient 创建新的Redis客户端
func NewRedisClient(options *redis.Options) *RedisClient {
return &RedisClient{
options: options,
}
}
func (r *redisConnector) Connect(ctx context.Context) error {
r.client.mu.Lock()
defer r.client.mu.Unlock()
// Connect 建立Redis连接
func (r *RedisClient) Connect(ctx context.Context) error {
r.mu.Lock()
defer r.mu.Unlock()
rdb := redis.NewClient(r.client.config.Options)
// 验证连接
if err := rdb.Ping(ctx).Err(); err != nil {
rdb.Close()
return err
if r.client != nil {
_ = r.client.Close()
}
r.client.client = rdb
client := redis.NewClient(r.options)
if err := client.Ping(ctx).Err(); err != nil {
return fmt.Errorf("redis ping failed: %w", err)
}
r.client = client
return nil
}
func (r *redisConnector) Close() error {
r.client.mu.Lock()
defer r.client.mu.Unlock()
// Close 关闭Redis连接
func (r *RedisClient) Close() error {
r.mu.Lock()
defer r.mu.Unlock()
if r.client.client != nil {
err := r.client.client.Close()
r.client.client = nil
if r.client != nil {
err := r.client.Close()
r.client = nil
return err
}
return nil
}
// redisHealthChecker Redis健康检查器
type redisHealthChecker struct {
client *RedisClient
}
// Ping 检查Redis连接是否健康
func (r *RedisClient) Ping(ctx context.Context) error {
r.mu.RLock()
defer r.mu.RUnlock()
func (r *redisHealthChecker) HealthCheck(ctx context.Context) error {
r.client.mu.RLock()
rdb := r.client.client
r.client.mu.RUnlock()
if rdb == nil {
return context.Canceled
if r.client == nil {
return fmt.Errorf("redis client is nil")
}
return rdb.Ping(ctx).Err()
return r.client.Ping(ctx).Err()
}
// NewRedisClient 创建带重连功能的Redis客户端
func NewRedisClient(cfg RedisClientConfig) (*RedisClient, error) {
client := &RedisClient{
config: cfg,
}
// IsConnected 检查连接状态
func (r *RedisClient) IsConnected() bool {
r.mu.RLock()
defer r.mu.RUnlock()
connector := &redisConnector{client: client}
checker := &redisHealthChecker{client: client}
client.manager = NewConnectionManager(connector, checker, cfg.ReconnectConfig)
// 首次连接
ctx := context.Background()
if err := client.manager.ConnectWithRetry(ctx); err != nil {
return nil, err
}
return client, nil
return r.client != nil
}
// GetClient 获取Redis客户端
func (c *RedisClient) GetClient() *redis.Client {
c.mu.RLock()
defer c.mu.RUnlock()
return c.client
// GetClient 获取底层的Redis客户端
func (r *RedisClient) GetClient() *redis.Client {
r.mu.RLock()
defer r.mu.RUnlock()
return r.client
}
// State 获取连接状态
func (c *RedisClient) State() ConnectionState {
return c.manager.State()
// RedisManager 创建Redis重连管理器
func RedisManager(options *redis.Options, config *Config) *Manager {
client := NewRedisClient(options)
return NewManager(client, config)
}
// Close 关闭客户端
func (c *RedisClient) Close() error {
return c.manager.Close()
}
// TriggerReconnect 手动触发重连
func (c *RedisClient) TriggerReconnect() {
c.manager.TriggerReconnect()
}
// Ping 执行Ping命令
func (c *RedisClient) Ping(ctx context.Context) error {
client := c.GetClient()
if client == nil {
return context.Canceled
}
return client.Ping(ctx).Err()
}
-125
View File
@@ -1,125 +0,0 @@
package reconnect
import "time"
// Strategy 重连策略接口
type Strategy interface {
// NextDelay 返回下一次重试的延迟时间
// attempt 是当前重试次数(从1开始)
NextDelay(attempt int) time.Duration
// Reset 重置策略状态
Reset()
}
// ExponentialBackoff 指数退避策略
type ExponentialBackoff struct {
initialDelay time.Duration
maxDelay time.Duration
multiplier float64
}
// NewExponentialBackoff 创建指数退避策略
func NewExponentialBackoff(initialDelay, maxDelay time.Duration, multiplier float64) *ExponentialBackoff {
if multiplier <= 0 {
multiplier = 2.0
}
return &ExponentialBackoff{
initialDelay: initialDelay,
maxDelay: maxDelay,
multiplier: multiplier,
}
}
// NextDelay 计算下一次延迟时间
func (e *ExponentialBackoff) NextDelay(attempt int) time.Duration {
if attempt <= 0 {
attempt = 1
}
delay := float64(e.initialDelay)
for i := 1; i < attempt; i++ {
delay *= e.multiplier
if time.Duration(delay) >= e.maxDelay {
return e.maxDelay
}
}
if time.Duration(delay) > e.maxDelay {
return e.maxDelay
}
return time.Duration(delay)
}
// Reset 重置策略
func (e *ExponentialBackoff) Reset() {
// 无状态,无需重置
}
// FixedInterval 固定间隔策略
type FixedInterval struct {
interval time.Duration
}
// NewFixedInterval 创建固定间隔策略
func NewFixedInterval(interval time.Duration) *FixedInterval {
return &FixedInterval{
interval: interval,
}
}
// NextDelay 返回固定延迟时间
func (f *FixedInterval) NextDelay(attempt int) time.Duration {
return f.interval
}
// Reset 重置策略
func (f *FixedInterval) Reset() {
// 无状态,无需重置
}
// LinearBackoff 线性退避策略
type LinearBackoff struct {
initialDelay time.Duration
maxDelay time.Duration
increment time.Duration
}
// NewLinearBackoff 创建线性退避策略
func NewLinearBackoff(initialDelay, maxDelay, increment time.Duration) *LinearBackoff {
return &LinearBackoff{
initialDelay: initialDelay,
maxDelay: maxDelay,
increment: increment,
}
}
// NextDelay 计算下一次延迟时间
func (l *LinearBackoff) NextDelay(attempt int) time.Duration {
if attempt <= 0 {
attempt = 1
}
delay := l.initialDelay + time.Duration(attempt-1)*l.increment
if delay > l.maxDelay {
return l.maxDelay
}
return delay
}
// Reset 重置策略
func (l *LinearBackoff) Reset() {
// 无状态,无需重置
}
// NewStrategy 根据配置创建策略
func NewStrategy(cfg Config) Strategy {
switch cfg.Strategy {
case StrategyFixedInterval:
return NewFixedInterval(cfg.InitialDelay)
case StrategyLinearBackoff:
return NewLinearBackoff(cfg.InitialDelay, cfg.MaxDelay, cfg.LinearIncrement)
case StrategyExponentialBackoff:
fallthrough
default:
return NewExponentialBackoff(cfg.InitialDelay, cfg.MaxDelay, cfg.Multiplier)
}
}