diff --git a/cache/redis.go b/cache/redis.go index ba90632..a995a4e 100644 --- a/cache/redis.go +++ b/cache/redis.go @@ -17,14 +17,24 @@ type Redis struct { // RedisOpts redis 连接属性 type RedisOpts struct { - Host string `json:"host" yml:"host"` - Username string `json:"username" yaml:"username"` - Password string `json:"password" yml:"password"` - Database int `json:"database" yml:"database"` - MaxIdle int `json:"max_idle" yml:"max_idle"` - MaxActive int `json:"max_active" yml:"max_active"` - IdleTimeout int `json:"idle_timeout" yml:"idle_timeout"` // second - UseTLS bool `json:"use_tls" yml:"use_tls"` // 是否使用TLS + Host string `json:"host" yaml:"host"` + Username string `json:"username" yaml:"username"` + Password string `json:"password" yaml:"password"` + Database int `json:"database" yaml:"database"` + MinIdleConns int `json:"min_idle_conns" yaml:"min_idle_conns"` // 最小空闲连接数 + PoolSize int `json:"pool_size" yaml:"pool_size"` // 连接池大小,0 表示使用默认值(即 CPU 核心数 * 10) + MaxRetries int `json:"max_retries" yaml:"max_retries"` // 最大重试次数,-1 表示不重试,0 表示使用默认值(即 3 次) + DialTimeout int `json:"dial_timeout" yaml:"dial_timeout"` // 连接超时时间(秒),0 表示使用默认值(即 5 秒) + ReadTimeout int `json:"read_timeout" yaml:"read_timeout"` // 读取超时时间(秒),-1 表示不超时,0 表示使用默认值(即 3 秒) + WriteTimeout int `json:"write_timeout" yaml:"write_timeout"` // 写入超时时间(秒),-1 表示不超时,0 表示使用默认值(即 ReadTimeout) + PoolTimeout int `json:"pool_timeout" yaml:"pool_timeout"` // 连接池获取连接超时时间(秒),0 表示使用默认值(即 ReadTimeout + 1 秒) + IdleTimeout int `json:"idle_timeout" yaml:"idle_timeout"` // 空闲连接超时时间(秒),-1 表示禁用空闲连接超时检查,0 表示使用默认值(即 5 分钟) + UseTLS bool `json:"use_tls" yaml:"use_tls"` // 是否使用 TLS + + // Deprecated: 应使用 MinIdleConns 代替 + MaxIdle int `json:"max_idle" yaml:"max_idle"` + // Deprecated: 应使用 PoolSize 代替 + MaxActive int `json:"max_active" yaml:"max_active"` } // NewRedis 实例化 @@ -34,10 +44,38 @@ func NewRedis(ctx context.Context, opts *RedisOpts) *Redis { DB: opts.Database, Username: opts.Username, Password: opts.Password, - IdleTimeout: time.Second * time.Duration(opts.IdleTimeout), - MinIdleConns: opts.MaxIdle, + MinIdleConns: opts.MinIdleConns, + PoolSize: opts.PoolSize, + MaxRetries: opts.MaxRetries, } + // 兼容旧的 MaxIdle 参数,仅在未显式设置 MinIdleConns 时生效 + if opts.MaxIdle > 0 && opts.MinIdleConns == 0 { + uniOpt.MinIdleConns = opts.MaxIdle + } + + // 兼容旧的 MaxActive 参数,仅在未显式设置 PoolSize 时生效 + if opts.MaxActive > 0 && opts.PoolSize == 0 { + uniOpt.PoolSize = opts.MaxActive + } + + applyTimeout := func(seconds int, target *time.Duration) { + if seconds > 0 { + *target = time.Duration(seconds) * time.Second + } else if seconds == -1 { + // 当 seconds 为 -1 时,表示禁用超时:按 go-redis 约定,将超时时间设置为负值(如 -1ns)代表「无超时」 + *target = -1 + } + // 当 seconds 为 0 时,使用 go-redis 的默认超时配置: + // 不修改 target,保持其零值(0),由 go-redis 解释为“使用默认值” + } + + applyTimeout(opts.DialTimeout, &uniOpt.DialTimeout) + applyTimeout(opts.ReadTimeout, &uniOpt.ReadTimeout) + applyTimeout(opts.WriteTimeout, &uniOpt.WriteTimeout) + applyTimeout(opts.PoolTimeout, &uniOpt.PoolTimeout) + applyTimeout(opts.IdleTimeout, &uniOpt.IdleTimeout) + if opts.UseTLS { h, _, err := net.SplitHostPort(opts.Host) if err != nil { diff --git a/cache/redis_test.go b/cache/redis_test.go index a41a2f1..22a21f7 100644 --- a/cache/redis_test.go +++ b/cache/redis_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/alicebob/miniredis/v2" + "github.com/go-redis/redis/v8" ) func TestRedis(t *testing.T) { @@ -18,7 +19,16 @@ func TestRedis(t *testing.T) { timeoutDuration = time.Second ctx = context.Background() opts = &RedisOpts{ - Host: server.Addr(), + Host: server.Addr(), + Password: "", + Database: 0, + PoolSize: 10, + MinIdleConns: 5, + DialTimeout: 5, + ReadTimeout: 5, + WriteTimeout: 5, + PoolTimeout: 5, + IdleTimeout: 300, } redis = NewRedis(ctx, opts) val = "silenceper" @@ -44,3 +54,259 @@ func TestRedis(t *testing.T) { t.Errorf("delete Error , err=%v", err) } } + +// setupRedisServer 创建并返回一个 miniredis 服务器实例 +func setupRedisServer(t *testing.T) *miniredis.Miniredis { + server, err := miniredis.Run() + if err != nil { + t.Fatal("miniredis.Run Error", err) + } + t.Cleanup(server.Close) + return server +} + +// TestRedisMaxIdleMapping 测试只设置MaxIdle应该映射到MinIdleConns +func TestRedisMaxIdleMapping(t *testing.T) { + server := setupRedisServer(t) + ctx := context.Background() + + opts := &RedisOpts{ + Host: server.Addr(), + Database: 0, + MaxIdle: 10, + } + r := NewRedis(ctx, opts) + + // 获取底层的 UniversalClient 并断言为 *redis.Client + client, ok := r.conn.(*redis.Client) + if !ok { + t.Fatal("无法转换为 *redis.Client") + } + + // 注意:MinIdleConns 表示期望的最小空闲连接数,但实际空闲连接数可能不同 + // 我们需要通过 Options() 来验证配置是否正确应用 + clientOpts := client.Options() + if clientOpts.MinIdleConns != 10 { + t.Errorf("期望 MinIdleConns = 10, 实际 = %d", clientOpts.MinIdleConns) + } +} + +// TestRedisMaxActiveMapping 测试只设置MaxActive应该映射到PoolSize +func TestRedisMaxActiveMapping(t *testing.T) { + server := setupRedisServer(t) + ctx := context.Background() + + opts := &RedisOpts{ + Host: server.Addr(), + Database: 0, + MaxActive: 20, + } + r := NewRedis(ctx, opts) + + client, ok := r.conn.(*redis.Client) + if !ok { + t.Fatal("无法转换为 *redis.Client") + } + + clientOpts := client.Options() + if clientOpts.PoolSize != 20 { + t.Errorf("期望 PoolSize = 20, 实际 = %d", clientOpts.PoolSize) + } +} + +// TestRedisNewFieldsPriority 测试新字段应该优先于旧字段 +func TestRedisNewFieldsPriority(t *testing.T) { + server := setupRedisServer(t) + ctx := context.Background() + + opts := &RedisOpts{ + Host: server.Addr(), + Database: 0, + MaxIdle: 5, + MinIdleConns: 15, + MaxActive: 10, + PoolSize: 30, + } + r := NewRedis(ctx, opts) + + client, ok := r.conn.(*redis.Client) + if !ok { + t.Fatal("无法转换为 *redis.Client") + } + + clientOpts := client.Options() + if clientOpts.MinIdleConns != 15 { + t.Errorf("期望 MinIdleConns = 15 (新字段优先), 实际 = %d", clientOpts.MinIdleConns) + } + if clientOpts.PoolSize != 30 { + t.Errorf("期望 PoolSize = 30 (新字段优先), 实际 = %d", clientOpts.PoolSize) + } +} + +// TestRedisPositiveTimeouts 测试正值超时应该正确应用 +func TestRedisPositiveTimeouts(t *testing.T) { + server := setupRedisServer(t) + ctx := context.Background() + + opts := &RedisOpts{ + Host: server.Addr(), + Database: 0, + DialTimeout: 10, + ReadTimeout: 20, + WriteTimeout: 30, + PoolTimeout: 40, + IdleTimeout: 50, + } + r := NewRedis(ctx, opts) + + client, ok := r.conn.(*redis.Client) + if !ok { + t.Fatal("无法转换为 *redis.Client") + } + + clientOpts := client.Options() + if clientOpts.DialTimeout != 10*time.Second { + t.Errorf("期望 DialTimeout = 10s, 实际 = %v", clientOpts.DialTimeout) + } + if clientOpts.ReadTimeout != 20*time.Second { + t.Errorf("期望 ReadTimeout = 20s, 实际 = %v", clientOpts.ReadTimeout) + } + if clientOpts.WriteTimeout != 30*time.Second { + t.Errorf("期望 WriteTimeout = 30s, 实际 = %v", clientOpts.WriteTimeout) + } + if clientOpts.PoolTimeout != 40*time.Second { + t.Errorf("期望 PoolTimeout = 40s, 实际 = %v", clientOpts.PoolTimeout) + } + if clientOpts.IdleTimeout != 50*time.Second { + t.Errorf("期望 IdleTimeout = 50s, 实际 = %v", clientOpts.IdleTimeout) + } +} + +// TestRedisNegativeTimeouts 测试-1值应该禁用超时 +func TestRedisNegativeTimeouts(t *testing.T) { + server := setupRedisServer(t) + ctx := context.Background() + + opts := &RedisOpts{ + Host: server.Addr(), + Database: 0, + DialTimeout: -1, + ReadTimeout: -1, + WriteTimeout: -1, + PoolTimeout: -1, + IdleTimeout: -1, + } + r := NewRedis(ctx, opts) + + client, ok := r.conn.(*redis.Client) + if !ok { + t.Fatal("无法转换为 *redis.Client") + } + + clientOpts := client.Options() + // -1 应该被设置为负值表示禁用超时 + // DialTimeout, PoolTimeout, IdleTimeout 会被设置为 -1ns + if clientOpts.DialTimeout != -1 { + t.Errorf("期望 DialTimeout = -1ns (禁用), 实际 = %v", clientOpts.DialTimeout) + } + // ReadTimeout 和 WriteTimeout 在 go-redis 中有特殊处理 + // 当设置为负值时,会被规范化为 0,这也表示无超时 + t.Logf("ReadTimeout = %v (设置为-1后的值)", clientOpts.ReadTimeout) + t.Logf("WriteTimeout = %v (设置为-1后的值)", clientOpts.WriteTimeout) + + if clientOpts.PoolTimeout != -1 { + t.Errorf("期望 PoolTimeout = -1ns (禁用), 实际 = %v", clientOpts.PoolTimeout) + } + if clientOpts.IdleTimeout != -1 { + t.Errorf("期望 IdleTimeout = -1ns (禁用), 实际 = %v", clientOpts.IdleTimeout) + } +} + +// TestRedisZeroTimeouts 测试0值应该使用go-redis默认值 +func TestRedisZeroTimeouts(t *testing.T) { + server := setupRedisServer(t) + ctx := context.Background() + + opts := &RedisOpts{ + Host: server.Addr(), + Database: 0, + DialTimeout: 0, + ReadTimeout: 0, + WriteTimeout: 0, + PoolTimeout: 0, + IdleTimeout: 0, + } + r := NewRedis(ctx, opts) + + client, ok := r.conn.(*redis.Client) + if !ok { + t.Fatal("无法转换为 *redis.Client") + } + + clientOpts := client.Options() + // 0值应该保持为0,由 go-redis 使用默认值 + // go-redis 的默认值: + // DialTimeout: 5s + // ReadTimeout: 3s + // WriteTimeout: ReadTimeout + // PoolTimeout: ReadTimeout + 1s + // IdleTimeout: 5min + + if clientOpts.DialTimeout == 0 { + t.Error("期望 DialTimeout 使用 go-redis 默认值 (5s), 实际为 0") + } + if clientOpts.ReadTimeout == 0 { + t.Error("期望 ReadTimeout 使用 go-redis 默认值 (3s), 实际为 0") + } + if clientOpts.WriteTimeout == 0 { + t.Error("期望 WriteTimeout 使用 go-redis 默认值 (ReadTimeout), 实际为 0") + } + if clientOpts.PoolTimeout == 0 { + t.Error("期望 PoolTimeout 使用 go-redis 默认值 (ReadTimeout + 1s), 实际为 0") + } + if clientOpts.IdleTimeout == 0 { + t.Error("期望 IdleTimeout 使用 go-redis 默认值 (5min), 实际为 0") + } +} + +// TestRedisMixedTimeouts 测试混合超时配置 +func TestRedisMixedTimeouts(t *testing.T) { + server := setupRedisServer(t) + ctx := context.Background() + + opts := &RedisOpts{ + Host: server.Addr(), + Database: 0, + DialTimeout: 5, // 正值 + ReadTimeout: -1, // 禁用 + WriteTimeout: 0, // 使用默认值 + PoolTimeout: 10, // 正值 + IdleTimeout: -1, // 禁用 + } + r := NewRedis(ctx, opts) + + client, ok := r.conn.(*redis.Client) + if !ok { + t.Fatal("无法转换为 *redis.Client") + } + + clientOpts := client.Options() + if clientOpts.DialTimeout != 5*time.Second { + t.Errorf("期望 DialTimeout = 5s, 实际 = %v", clientOpts.DialTimeout) + } + // ReadTimeout 设置为 -1,会被 go-redis 处理为 0(无超时) + t.Logf("ReadTimeout = %v (设置为-1后的值)", clientOpts.ReadTimeout) + + // WriteTimeout 设置为 0,应该使用 go-redis 的默认值 + // 默认值通常是 ReadTimeout 的值 + t.Logf("WriteTimeout = %v (设置为0后使用的默认值)", clientOpts.WriteTimeout) + + if clientOpts.PoolTimeout != 10*time.Second { + t.Errorf("期望 PoolTimeout = 10s, 实际 = %v", clientOpts.PoolTimeout) + } + + // IdleTimeout 设置为 -1,应该被设置为 -1ns(禁用空闲超时) + if clientOpts.IdleTimeout != -1 { + t.Errorf("期望 IdleTimeout = -1ns (禁用), 实际 = %v", clientOpts.IdleTimeout) + } +}