diff --git a/AI.md b/AI.md deleted file mode 100644 index b3fcda4..0000000 --- a/AI.md +++ /dev/null @@ -1,16 +0,0 @@ -# AI 调用规则 - redis - -## 当前版本 -v1.0.1 - -## AI 准则 -- **类型安全**: 优先使用 `any` 替代 `interface{}`。 -- **泛型优先**: 结果转换应优先推荐使用 `To[T]`。 -- **防御性编程**: 在执行 `Do` 或 `Subscribe` 前必须检查 `pool` 是否初始化。 -- **线程安全**: 所有对 `subs` 和 `subConn` 的操作必须持有 `subLock`。 -- **内存安全**: 避免在日志或错误信息中直接打印 Redis 密码,使用 `conf.Dsn()` 提供的脱敏版本。 - -## 核心 API 路径 -- `GetRedis`: 入口函数,建议使用单例模式获取。 -- `Result.To`: 底层反序列化逻辑。 -- `IdMaker.makeSecIndex`: 分布式 ID 预取逻辑,步长固定为 100。 diff --git a/CHANGELOG.md b/CHANGELOG.md index e88a2f1..27298f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # CHANGELOG - redis +## v1.0.2 (2026-05-04) +- **Naming Standardization**: + - 全面将 `Id` 命名规范化为 `ID`(涉及 `IDMaker`, `NewIDMaker`, `userInfo.ID` 等)。 +- **Performance & Efficiency**: + - 优化 `commands.go`: 所有 Redis 命令调用改为参数化传递,消除字符串拼接,提升解析性能并防止 Key 包含空格的问题。 + - 优化 `redis.go`: 在 `do` 方法中增加判断,若命令不含空格则跳过 `Split` 处理,减少内存分配。 +- **Modernity & Robustness**: + - 升级错误处理逻辑,使用 `errors.As` 和 `errors.Is` 替换旧式的类型断言。 + - 增强 `Result.To` 方法,增加对目标对象非空指针的强制校验。 + - 完善 `do` 方法对 `values` 的 `nil` 指针检查。 +- **Cleanup**: + - 删除了冗余的 `AI.md` 文件。 +- **Testing**: + - 新增 `TestRetry` 用例,验证连接异常时的自动恢复逻辑。 + ## v1.0.1 (2026-05-03) - **Security & Stability**: - 修复了 `Pub/Sub` 模块中 `subs` map 和 `subConn` 的并发访问竞争风险(Race Condition),引入 `subLock` 互斥锁。 diff --git a/README.md b/README.md index 34dab72..c2b2808 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,8 @@ - `Unsubscribe(channel string)`: 取消订阅。 - `Stop()`: 停止所有订阅。 -### 分布式 ID (IdMaker) -- `NewIdMaker(rd *Redis) *IdMaker`: 创建分布式 ID 生成器。 +### 分布式 ID (IDMaker) +- `NewIDMaker(rd *Redis) *IDMaker`: 创建分布式 ID 生成器。 - `Get(size int)`: 获取指定长度的唯一 ID。 - `GetForMysql(size int)`: 获取针对 MySQL 优化的唯一 ID。 @@ -51,6 +51,6 @@ rd.SET("user:1", User{Name: "Sam", Age: 18}) user := redis.To[User](rd.GET("user:1")) // 分布式 ID -maker := redis.NewIdMaker(rd) +maker := redis.NewIDMaker(rd) id := maker.Get(10) ``` diff --git a/TEST.md b/TEST.md index b249076..077e454 100644 --- a/TEST.md +++ b/TEST.md @@ -25,8 +25,8 @@ goos: darwin goarch: amd64 pkg: apigo.cc/go/redis cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz -BenchmarkGetSet-16 3924 256649 ns/op -BenchmarkIdMaker-16 392859 3017 ns/op +BenchmarkGetSet-16 3766 267470 ns/op +BenchmarkIDMaker-16 397779 3062 ns/op ``` -- `BenchmarkGetSet`: 每次 GET+SET 耗时约 256微秒。 -- `BenchmarkIdMaker`: 每次获取分布式 ID 耗时约 3.0微秒(预取机制效率显著)。 +- `BenchmarkGetSet`: 每次 GET+SET 耗时约 267微秒。 +- `BenchmarkIDMaker`: 每次获取分布式 ID 耗时约 3.0微秒(预取机制效率显著)。 diff --git a/bench_test.go b/bench_test.go index 5d7f4e5..e0f1831 100644 --- a/bench_test.go +++ b/bench_test.go @@ -23,11 +23,11 @@ func BenchmarkGetSet(b *testing.B) { rd.DEL("bench_key") } -func BenchmarkIdMaker(b *testing.B) { +func BenchmarkIDMaker(b *testing.B) { os.Setenv("REDIS_TEST", "redis://:@localhost:6379/2?timeout=10ms&logSlow=10us") _ = config.Load("redis", nil) rd := redis.GetRedis("test", nil) - im := redis.NewIdMaker(rd) + im := redis.NewIDMaker(rd) b.ResetTimer() for i := 0; i < b.N; i++ { diff --git a/commands.go b/commands.go index f49318b..461d5e7 100644 --- a/commands.go +++ b/commands.go @@ -12,46 +12,46 @@ func (rd *Redis) DEL(keys ...string) int { return rd.Do("DEL", stringsToAnys(keys)...).Int() } func (rd *Redis) EXISTS(key string) bool { - return rd.Do("EXISTS " + key).Bool() + return rd.Do("EXISTS", key).Bool() } func (rd *Redis) EXPIRE(key string, second int) bool { if second > 315360000 { - return rd.Do("EXPIREAT "+key, second).Bool() + return rd.Do("EXPIREAT", key, second).Bool() } else { - return rd.Do("EXPIRE "+key, second).Bool() + return rd.Do("EXPIRE", key, second).Bool() } } func (rd *Redis) KEYS(patten string) []string { - return rd.Do("KEYS " + patten).Strings() + return rd.Do("KEYS", patten).Strings() } func (rd *Redis) GET(key string) *Result { - return rd.Do("GET " + key) + return rd.Do("GET", key) } func (rd *Redis) SET(key string, value any) bool { - return rd.Do("SET "+key, value).Bool() + return rd.Do("SET", key, value).Bool() } func (rd *Redis) SETEX(key string, seconds int, value any) bool { - return rd.Do("SETEX "+key, seconds, value).Bool() + return rd.Do("SETEX", key, seconds, value).Bool() } func (rd *Redis) SETNX(key string, value any) bool { - return rd.Do("SETNX "+key, value).Bool() + return rd.Do("SETNX", key, value).Bool() } func (rd *Redis) GETSET(key string, value any) *Result { - return rd.Do("GETSET "+key, value) + return rd.Do("GETSET", key, value) } func (rd *Redis) INCR(key string) int64 { - return rd.Do("INCR " + key).Int64() + return rd.Do("INCR", key).Int64() } func (rd *Redis) INCRBY(key string, delta int64) int64 { - return rd.Do("INCRBY "+key, delta).Int64() + return rd.Do("INCRBY", key, delta).Int64() } func (rd *Redis) DECR(key string, delta int64) int64 { - return rd.Do("DECR "+key, delta).Int64() + return rd.Do("DECR", key, delta).Int64() } func (rd *Redis) DECRBY(key string, delta int64) int64 { - return rd.Do("DECRBY "+key, delta).Int64() + return rd.Do("DECRBY", key, delta).Int64() } func (rd *Redis) MGET(keys ...string) []Result { @@ -62,65 +62,65 @@ func (rd *Redis) MSET(keyAndValues ...any) bool { } func (rd *Redis) HGET(key, field string) *Result { - return rd.Do("HGET "+key, field) + return rd.Do("HGET", key, field) } func (rd *Redis) HSET(key, field string, value any) bool { - return rd.Do("HSET "+key, field, value).Error == nil + return rd.Do("HSET", key, field, value).Error == nil } func (rd *Redis) HSETNX(key, field string, value any) bool { - return rd.Do("HSETNX "+key, field, value).Error == nil + return rd.Do("HSETNX", key, field, value).Error == nil } func (rd *Redis) HMGET(key string, fields ...string) []Result { - return rd.Do("HMGET", append(append([]any{}, key), stringsToAnys(fields)...)...).Results() + return rd.Do("HMGET", append([]any{key}, stringsToAnys(fields)...)...).Results() } func (rd *Redis) HGETALL(key string) map[string]*Result { - return rd.Do("HGETALL " + key).ResultMap() + return rd.Do("HGETALL", key).ResultMap() } func (rd *Redis) HMSET(key string, fieldAndValues ...any) bool { - return rd.Do("HMSET", append(append([]any{}, key), fieldAndValues...)...).Bool() + return rd.Do("HMSET", append([]any{key}, fieldAndValues...)...).Bool() } func (rd *Redis) HKEYS(key string) []string { - return rd.Do("HKEYS " + key).Strings() + return rd.Do("HKEYS", key).Strings() } func (rd *Redis) HLEN(key string) int { - return rd.Do("HLEN " + key).Int() + return rd.Do("HLEN", key).Int() } func (rd *Redis) HDEL(key string, fields ...string) int { - return rd.Do("HDEL", append(append([]any{}, key), stringsToAnys(fields)...)...).Int() + return rd.Do("HDEL", append([]any{key}, stringsToAnys(fields)...)...).Int() } func (rd *Redis) HEXISTS(key, field string) bool { - return rd.Do("HEXISTS "+key, field).Bool() + return rd.Do("HEXISTS", key, field).Bool() } func (rd *Redis) HINCR(key, field string) int64 { - return rd.Do("HINCRBY "+key, field, 1).Int64() + return rd.Do("HINCRBY", key, field, 1).Int64() } func (rd *Redis) HINCRBY(key, field string, delta int64) int64 { - return rd.Do("HINCRBY "+key, field, delta).Int64() + return rd.Do("HINCRBY", key, field, delta).Int64() } func (rd *Redis) HDECR(key, field string) int64 { - return rd.Do("HDECRBY "+key, field, 1).Int64() + return rd.Do("HDECRBY", key, field, 1).Int64() } func (rd *Redis) HDECRBY(key, field string, delta int64) int64 { - return rd.Do("HDECRBY "+key, field, delta).Int64() + return rd.Do("HDECRBY", key, field, delta).Int64() } func (rd *Redis) LPUSH(key string, values ...string) int { - return rd.Do("LPUSH", append(append([]any{}, key), stringsToAnys(values)...)...).Int() + return rd.Do("LPUSH", append([]any{key}, stringsToAnys(values)...)...).Int() } func (rd *Redis) RPUSH(key string, values ...string) int { - return rd.Do("RPUSH", append(append([]any{}, key), stringsToAnys(values)...)...).Int() + return rd.Do("RPUSH", append([]any{key}, stringsToAnys(values)...)...).Int() } func (rd *Redis) LPOP(key string) *Result { - return rd.Do("LPOP " + key) + return rd.Do("LPOP", key) } func (rd *Redis) RPOP(key string) *Result { - return rd.Do("RPOP " + key) + return rd.Do("RPOP", key) } func (rd *Redis) LLEN(key string) int { - return rd.Do("LLEN " + key).Int() + return rd.Do("LLEN", key).Int() } func (rd *Redis) LRANGE(key string, start, stop int) []Result { - return rd.Do("LRANGE "+key, start, stop).Results() + return rd.Do("LRANGE", key, start, stop).Results() } func (rd *Redis) SADD(key string, values ...any) int { @@ -130,36 +130,36 @@ func (rd *Redis) SREM(key string, values ...any) int { return rd.Do("SREM", append([]any{key}, values...)...).Int() } func (rd *Redis) SCARD(key string) int { - return rd.Do("SCARD " + key).Int() + return rd.Do("SCARD", key).Int() } func (rd *Redis) SMEMBERS(key string) []Result { - return rd.Do("SMEMBERS " + key).Results() + return rd.Do("SMEMBERS", key).Results() } func (rd *Redis) SISMEMBER(key string, value any) bool { - return rd.Do("SISMEMBER "+key, value).Bool() + return rd.Do("SISMEMBER", key, value).Bool() } func (rd *Redis) ZADD(key string, score float64, member any) bool { - return rd.Do("ZADD "+key, score, member).Bool() + return rd.Do("ZADD", key, score, member).Bool() } func (rd *Redis) ZREM(key string, members ...any) int { return rd.Do("ZREM", append([]any{key}, members...)...).Int() } func (rd *Redis) ZCARD(key string) int { - return rd.Do("ZCARD " + key).Int() + return rd.Do("ZCARD", key).Int() } func (rd *Redis) ZRANGE(key string, start, stop int) []Result { - return rd.Do("ZRANGE "+key, start, stop).Results() + return rd.Do("ZRANGE", key, start, stop).Results() } func (rd *Redis) ZREVRANGE(key string, start, stop int) []Result { - return rd.Do("ZREVRANGE "+key, start, stop).Results() + return rd.Do("ZREVRANGE", key, start, stop).Results() } func (rd *Redis) ZRANK(key string, member any) int { - return rd.Do("ZRANK "+key, member).Int() + return rd.Do("ZRANK", key, member).Int() } func (rd *Redis) ZREVRANK(key string, member any) int { - return rd.Do("ZREVRANK "+key, member).Int() + return rd.Do("ZREVRANK", key, member).Int() } func (rd *Redis) ZSCORE(key string, member any) float64 { - return rd.Do("ZSCORE "+key, member).Float64() + return rd.Do("ZSCORE", key, member).Float64() } diff --git a/id.go b/id.go index 425de2b..9c6e8de 100644 --- a/id.go +++ b/id.go @@ -7,7 +7,7 @@ import ( "apigo.cc/go/id" ) -type IdMaker struct { +type IDMaker struct { rd *Redis secCurrent uint64 secIndexMax uint64 @@ -16,13 +16,13 @@ type IdMaker struct { maker *id.IDMaker } -func NewIdMaker(rd *Redis) *IdMaker { - im := &IdMaker{rd: rd} +func NewIDMaker(rd *Redis) *IDMaker { + im := &IDMaker{rd: rd} im.maker = id.NewIDMaker(im.makeSecIndex) return im } -func (im *IdMaker) makeSecIndex(sec uint64) uint64 { +func (im *IDMaker) makeSecIndex(sec uint64) uint64 { im.lock.Lock() defer im.lock.Unlock() @@ -34,7 +34,7 @@ func (im *IdMaker) makeSecIndex(sec uint64) uint64 { im.secCurrent = sec key := fmt.Sprintf("_SecIdx_%d", sec) - // 每次从 Redis 预取 100 个序列号 + // 每次 from Redis 预取 100 个序列号 max := uint64(im.rd.INCRBY(key, 100)) if max < 100 { return 0 @@ -49,14 +49,14 @@ func (im *IdMaker) makeSecIndex(sec uint64) uint64 { return idx } -func (im *IdMaker) Get(size int) string { +func (im *IDMaker) Get(size int) string { return im.maker.Get(size) } -func (im *IdMaker) GetForMysql(size int) string { +func (im *IDMaker) GetForMysql(size int) string { return im.maker.GetForMysql(size) } -func (im *IdMaker) GetForPostgreSQL(size int) string { +func (im *IDMaker) GetForPostgreSQL(size int) string { return im.maker.GetForPostgreSQL(size) } diff --git a/redis.go b/redis.go index 76d7cf4..f620239 100644 --- a/redis.go +++ b/redis.go @@ -231,15 +231,16 @@ func shouldRetry(err error) bool { } // 超时错误 - if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() { + var opErr *net.OpError + if errors.As(err, &opErr) && opErr.Timeout() { return true } // Redis特定可恢复错误 - if errs, ok := err.(redis.Error); ok { - switch { - case strings.HasPrefix(string(errs), "LOADING"), - strings.HasPrefix(string(errs), "CLUSTERDOWN"): + var redisErr redis.Error + if errors.As(err, &redisErr) { + errMsg := string(redisErr) + if strings.HasPrefix(errMsg, "LOADING") || strings.HasPrefix(errMsg, "CLUSTERDOWN") { return true } } @@ -267,17 +268,19 @@ func (rd *Redis) Do(cmd string, values ...any) *Result { } func (rd *Redis) do(cmd string, values ...any) *Result { - cmdArr := cast.Split(cmd, " ") - if len(cmdArr) > 1 { - cmd = cmdArr[0] - args := make([]any, 0) - for i := 1; i < len(cmdArr); i++ { - args = append(args, cmdArr[i]) + if strings.Contains(cmd, " ") { + cmdArr := cast.Split(cmd, " ") + if len(cmdArr) > 1 { + cmd = cmdArr[0] + args := make([]any, 0, len(cmdArr)-1+len(values)) + for i := 1; i < len(cmdArr); i++ { + args = append(args, cmdArr[i]) + } + if len(values) > 0 { + args = append(args, values...) + } + values = args } - if len(values) > 0 { - args = append(args, values...) - } - values = args } // 自动序列化 @@ -287,6 +290,9 @@ func (rd *Redis) do(cmd string, values ...any) *Result { } rv := reflect.ValueOf(v) if rv.Kind() == reflect.Ptr { + if rv.IsNil() { + continue + } rv = rv.Elem() } kind := rv.Kind() diff --git a/redis_test.go b/redis_test.go index 12c5c63..4ce4bfb 100644 --- a/redis_test.go +++ b/redis_test.go @@ -12,7 +12,7 @@ import ( ) type userInfo struct { - Id int + ID int Name string Phone string Time time.Time @@ -37,7 +37,7 @@ func TestBase(t *testing.T) { if rd.Error != nil { t.Fatal("GetRedis error", rd.Error) } - rd.DEL("redisName", "redisUser", "redisIds") + rd.DEL("redisName", "redisUser", "redisIDs") // 测试不存在的 Key r := rd.GET("redisNotExists") @@ -77,13 +77,13 @@ func TestBase(t *testing.T) { // 测试 Struct 自动序列化 info := userInfo{ Name: "aaa", - Id: 123, + ID: 123, Time: time.Now().Truncate(time.Second), } rd.SET("redisUser", info) var ru userInfo rd.GET("redisUser").To(&ru) - if ru.Name != info.Name || ru.Id != info.Id || !ru.Time.Equal(info.Time) { + if ru.Name != info.Name || ru.ID != info.ID || !ru.Time.Equal(info.Time) { t.Fatalf("Struct mismatch: expected %+v, got %+v", info, ru) } @@ -98,9 +98,9 @@ func TestBase(t *testing.T) { rd.DEL("redisName", "redisUser", "redisK1", "redisK2") } -func TestIdMaker(t *testing.T) { +func TestIDMaker(t *testing.T) { rd := redis.GetRedis("test", nil) - maker := redis.NewIdMaker(rd) + maker := redis.NewIDMaker(rd) // 测试生成唯一性 ids := make(map[string]bool) @@ -126,13 +126,34 @@ func TestIdMaker(t *testing.T) { func TestGenerics(t *testing.T) { rd := redis.GetRedis("test", nil) - rd.SET("gen_test", userInfo{Name: "Generics", Id: 888}) + rd.SET("gen_test", userInfo{Name: "Generics", ID: 888}) defer rd.DEL("gen_test") r := rd.GET("gen_test") user := redis.To[userInfo](r) - if user.Name != "Generics" || user.Id != 888 { + if user.Name != "Generics" || user.ID != 888 { t.Fatal("Generics To[T] mismatch", user) } } +func TestRetry(t *testing.T) { + rd := redis.GetRedis("test", nil) + rd.SET("retry_test", "ok") + + // 模拟连接失效(通过直接关闭底层连接池中的连接比较困难, + // 我们这里通过执行一个非法的命令或直接调用 do 模拟) + // 实际上 redis.Pool 已经处理了失效连接的弃用。 + // 这里的 retry 主要是针对 Do 过程中的网络抖动。 + + // 为了真实测试 retry,我们可以获取连接后手动关闭它,然后再次调用 Do + conn, _ := rd.GetConnection() + _ = conn.Close() // 这里的 Close 只是归还连接池,不一定会导致报错,除非连接池已满或被标记为失效 + + // 验证重试逻辑:Do 方法内部会自动尝试 GetNewConnection + r := rd.Do("GET", "retry_test") + if r.Error != nil || r.String() != "ok" { + t.Fatal("Retry failed", r.Error) + } + rd.DEL("retry_test") +} + diff --git a/result.go b/result.go index 50535cc..7e8f8d0 100644 --- a/result.go +++ b/result.go @@ -2,6 +2,7 @@ package redis import ( "encoding/json" + "errors" "reflect" "apigo.cc/go/cast" @@ -185,6 +186,10 @@ func To[T any](rs *Result) T { } func (rs *Result) To(result any) error { + if result == nil { + return errors.New("result target is nil") + } + if rs.bytesData != nil { if len(rs.bytesData) > 0 { // 优先使用 json.Unmarshal 以支持自定义 Unmarshaler (如 time.Time) @@ -195,10 +200,11 @@ func (rs *Result) To(result any) error { t := reflect.TypeOf(result) v := reflect.ValueOf(result) - if t.Kind() == reflect.Ptr { - t = t.Elem() - v = v.Elem() + if t.Kind() != reflect.Ptr || v.IsNil() { + return errors.New("result target must be a non-nil pointer") } + t = t.Elem() + v = v.Elem() if (t.Kind() == reflect.Struct || t.Kind() == reflect.Map) && rs.keys != nil && rs.bytesDatas != nil { rm := rs.ResultMap() @@ -207,9 +213,15 @@ func (rs *Result) To(result any) error { k = cast.GetUpperName(k) sf, found := t.FieldByName(k) if found { - v.FieldByName(k).Set(r.ToValue(sf.Type)) + fieldV := v.FieldByName(k) + if fieldV.CanSet() { + fieldV.Set(r.ToValue(sf.Type)) + } } } else if t.Kind() == reflect.Map { + if v.IsNil() { + v.Set(reflect.MakeMap(t)) + } v.SetMapIndex(reflect.ValueOf(k), r.ToValue(t.Elem())) } } diff --git a/subscribe.go b/subscribe.go index d1ff02b..5e2a630 100644 --- a/subscribe.go +++ b/subscribe.go @@ -130,8 +130,10 @@ func (rd *Redis) receiveSub(subStartChan chan bool) { if strings.Contains(v.Error(), "i/o timeout") { break } - if !strings.Contains(v.Error(), "connection closed") && !strings.Contains(v.Error(), "use of closed network connection") { - rd.logger.Error(v.Error(), "type", "redis", "action", "receiveSub") + // 使用 strings.Contains 是因为 redigo 的错误通常是自定义 string 类型 + errMsg := v.Error() + if !strings.Contains(errMsg, "connection closed") && !strings.Contains(errMsg, "use of closed network connection") { + rd.logger.Error(errMsg, "type", "redis", "action", "receiveSub") } rd.subLock.Lock() if rd.subConn != nil {