Compare commits

...

1 Commits
v1.0.1 ... main

Author SHA1 Message Date
AI Engineer
28308c2f58 refactor: standardize ID naming and optimize performance (by AI) 2026-05-04 00:46:17 +08:00
11 changed files with 145 additions and 105 deletions

16
AI.md
View File

@ -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。

View File

@ -1,5 +1,20 @@
# CHANGELOG - redis # 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) ## v1.0.1 (2026-05-03)
- **Security & Stability**: - **Security & Stability**:
- 修复了 `Pub/Sub` 模块中 `subs` map 和 `subConn` 的并发访问竞争风险Race Condition引入 `subLock` 互斥锁。 - 修复了 `Pub/Sub` 模块中 `subs` map 和 `subConn` 的并发访问竞争风险Race Condition引入 `subLock` 互斥锁。

View File

@ -31,8 +31,8 @@
- `Unsubscribe(channel string)`: 取消订阅。 - `Unsubscribe(channel string)`: 取消订阅。
- `Stop()`: 停止所有订阅。 - `Stop()`: 停止所有订阅。
### 分布式 ID (IdMaker) ### 分布式 ID (IDMaker)
- `NewIdMaker(rd *Redis) *IdMaker`: 创建分布式 ID 生成器。 - `NewIDMaker(rd *Redis) *IDMaker`: 创建分布式 ID 生成器。
- `Get(size int)`: 获取指定长度的唯一 ID。 - `Get(size int)`: 获取指定长度的唯一 ID。
- `GetForMysql(size int)`: 获取针对 MySQL 优化的唯一 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")) user := redis.To[User](rd.GET("user:1"))
// 分布式 ID // 分布式 ID
maker := redis.NewIdMaker(rd) maker := redis.NewIDMaker(rd)
id := maker.Get(10) id := maker.Get(10)
``` ```

View File

@ -25,8 +25,8 @@ goos: darwin
goarch: amd64 goarch: amd64
pkg: apigo.cc/go/redis pkg: apigo.cc/go/redis
cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
BenchmarkGetSet-16 3924 256649 ns/op BenchmarkGetSet-16 3766 267470 ns/op
BenchmarkIdMaker-16 392859 3017 ns/op BenchmarkIDMaker-16 397779 3062 ns/op
``` ```
- `BenchmarkGetSet`: 每次 GET+SET 耗时约 256微秒。 - `BenchmarkGetSet`: 每次 GET+SET 耗时约 267微秒。
- `BenchmarkIdMaker`: 每次获取分布式 ID 耗时约 3.0微秒(预取机制效率显著)。 - `BenchmarkIDMaker`: 每次获取分布式 ID 耗时约 3.0微秒(预取机制效率显著)。

View File

@ -23,11 +23,11 @@ func BenchmarkGetSet(b *testing.B) {
rd.DEL("bench_key") 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") os.Setenv("REDIS_TEST", "redis://:@localhost:6379/2?timeout=10ms&logSlow=10us")
_ = config.Load("redis", nil) _ = config.Load("redis", nil)
rd := redis.GetRedis("test", nil) rd := redis.GetRedis("test", nil)
im := redis.NewIdMaker(rd) im := redis.NewIDMaker(rd)
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {

View File

@ -12,46 +12,46 @@ func (rd *Redis) DEL(keys ...string) int {
return rd.Do("DEL", stringsToAnys(keys)...).Int() return rd.Do("DEL", stringsToAnys(keys)...).Int()
} }
func (rd *Redis) EXISTS(key string) bool { 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 { func (rd *Redis) EXPIRE(key string, second int) bool {
if second > 315360000 { if second > 315360000 {
return rd.Do("EXPIREAT "+key, second).Bool() return rd.Do("EXPIREAT", key, second).Bool()
} else { } else {
return rd.Do("EXPIRE "+key, second).Bool() return rd.Do("EXPIRE", key, second).Bool()
} }
} }
func (rd *Redis) KEYS(patten string) []string { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { func (rd *Redis) LPOP(key string) *Result {
return rd.Do("LPOP " + key) return rd.Do("LPOP", key)
} }
func (rd *Redis) RPOP(key string) *Result { func (rd *Redis) RPOP(key string) *Result {
return rd.Do("RPOP " + key) return rd.Do("RPOP", key)
} }
func (rd *Redis) LLEN(key string) int { 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 { 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 { 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() return rd.Do("SREM", append([]any{key}, values...)...).Int()
} }
func (rd *Redis) SCARD(key string) 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 { 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 { 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 { 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 { func (rd *Redis) ZREM(key string, members ...any) int {
return rd.Do("ZREM", append([]any{key}, members...)...).Int() return rd.Do("ZREM", append([]any{key}, members...)...).Int()
} }
func (rd *Redis) ZCARD(key string) 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 { 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 { 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 { 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 { 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 { func (rd *Redis) ZSCORE(key string, member any) float64 {
return rd.Do("ZSCORE "+key, member).Float64() return rd.Do("ZSCORE", key, member).Float64()
} }

16
id.go
View File

@ -7,7 +7,7 @@ import (
"apigo.cc/go/id" "apigo.cc/go/id"
) )
type IdMaker struct { type IDMaker struct {
rd *Redis rd *Redis
secCurrent uint64 secCurrent uint64
secIndexMax uint64 secIndexMax uint64
@ -16,13 +16,13 @@ type IdMaker struct {
maker *id.IDMaker maker *id.IDMaker
} }
func NewIdMaker(rd *Redis) *IdMaker { func NewIDMaker(rd *Redis) *IDMaker {
im := &IdMaker{rd: rd} im := &IDMaker{rd: rd}
im.maker = id.NewIDMaker(im.makeSecIndex) im.maker = id.NewIDMaker(im.makeSecIndex)
return im return im
} }
func (im *IdMaker) makeSecIndex(sec uint64) uint64 { func (im *IDMaker) makeSecIndex(sec uint64) uint64 {
im.lock.Lock() im.lock.Lock()
defer im.lock.Unlock() defer im.lock.Unlock()
@ -34,7 +34,7 @@ func (im *IdMaker) makeSecIndex(sec uint64) uint64 {
im.secCurrent = sec im.secCurrent = sec
key := fmt.Sprintf("_SecIdx_%d", sec) key := fmt.Sprintf("_SecIdx_%d", sec)
// 每次 Redis 预取 100 个序列号 // 每次 from Redis 预取 100 个序列号
max := uint64(im.rd.INCRBY(key, 100)) max := uint64(im.rd.INCRBY(key, 100))
if max < 100 { if max < 100 {
return 0 return 0
@ -49,14 +49,14 @@ func (im *IdMaker) makeSecIndex(sec uint64) uint64 {
return idx return idx
} }
func (im *IdMaker) Get(size int) string { func (im *IDMaker) Get(size int) string {
return im.maker.Get(size) return im.maker.Get(size)
} }
func (im *IdMaker) GetForMysql(size int) string { func (im *IDMaker) GetForMysql(size int) string {
return im.maker.GetForMysql(size) return im.maker.GetForMysql(size)
} }
func (im *IdMaker) GetForPostgreSQL(size int) string { func (im *IDMaker) GetForPostgreSQL(size int) string {
return im.maker.GetForPostgreSQL(size) return im.maker.GetForPostgreSQL(size)
} }

View File

@ -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 return true
} }
// Redis特定可恢复错误 // Redis特定可恢复错误
if errs, ok := err.(redis.Error); ok { var redisErr redis.Error
switch { if errors.As(err, &redisErr) {
case strings.HasPrefix(string(errs), "LOADING"), errMsg := string(redisErr)
strings.HasPrefix(string(errs), "CLUSTERDOWN"): if strings.HasPrefix(errMsg, "LOADING") || strings.HasPrefix(errMsg, "CLUSTERDOWN") {
return true return true
} }
} }
@ -267,10 +268,11 @@ func (rd *Redis) Do(cmd string, values ...any) *Result {
} }
func (rd *Redis) do(cmd string, values ...any) *Result { func (rd *Redis) do(cmd string, values ...any) *Result {
if strings.Contains(cmd, " ") {
cmdArr := cast.Split(cmd, " ") cmdArr := cast.Split(cmd, " ")
if len(cmdArr) > 1 { if len(cmdArr) > 1 {
cmd = cmdArr[0] cmd = cmdArr[0]
args := make([]any, 0) args := make([]any, 0, len(cmdArr)-1+len(values))
for i := 1; i < len(cmdArr); i++ { for i := 1; i < len(cmdArr); i++ {
args = append(args, cmdArr[i]) args = append(args, cmdArr[i])
} }
@ -279,6 +281,7 @@ func (rd *Redis) do(cmd string, values ...any) *Result {
} }
values = args values = args
} }
}
// 自动序列化 // 自动序列化
for i, v := range values { for i, v := range values {
@ -287,6 +290,9 @@ func (rd *Redis) do(cmd string, values ...any) *Result {
} }
rv := reflect.ValueOf(v) rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Ptr { if rv.Kind() == reflect.Ptr {
if rv.IsNil() {
continue
}
rv = rv.Elem() rv = rv.Elem()
} }
kind := rv.Kind() kind := rv.Kind()

View File

@ -12,7 +12,7 @@ import (
) )
type userInfo struct { type userInfo struct {
Id int ID int
Name string Name string
Phone string Phone string
Time time.Time Time time.Time
@ -37,7 +37,7 @@ func TestBase(t *testing.T) {
if rd.Error != nil { if rd.Error != nil {
t.Fatal("GetRedis error", rd.Error) t.Fatal("GetRedis error", rd.Error)
} }
rd.DEL("redisName", "redisUser", "redisIds") rd.DEL("redisName", "redisUser", "redisIDs")
// 测试不存在的 Key // 测试不存在的 Key
r := rd.GET("redisNotExists") r := rd.GET("redisNotExists")
@ -77,13 +77,13 @@ func TestBase(t *testing.T) {
// 测试 Struct 自动序列化 // 测试 Struct 自动序列化
info := userInfo{ info := userInfo{
Name: "aaa", Name: "aaa",
Id: 123, ID: 123,
Time: time.Now().Truncate(time.Second), Time: time.Now().Truncate(time.Second),
} }
rd.SET("redisUser", info) rd.SET("redisUser", info)
var ru userInfo var ru userInfo
rd.GET("redisUser").To(&ru) 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) 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") rd.DEL("redisName", "redisUser", "redisK1", "redisK2")
} }
func TestIdMaker(t *testing.T) { func TestIDMaker(t *testing.T) {
rd := redis.GetRedis("test", nil) rd := redis.GetRedis("test", nil)
maker := redis.NewIdMaker(rd) maker := redis.NewIDMaker(rd)
// 测试生成唯一性 // 测试生成唯一性
ids := make(map[string]bool) ids := make(map[string]bool)
@ -126,13 +126,34 @@ func TestIdMaker(t *testing.T) {
func TestGenerics(t *testing.T) { func TestGenerics(t *testing.T) {
rd := redis.GetRedis("test", nil) 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") defer rd.DEL("gen_test")
r := rd.GET("gen_test") r := rd.GET("gen_test")
user := redis.To[userInfo](r) 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) 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")
}

View File

@ -2,6 +2,7 @@ package redis
import ( import (
"encoding/json" "encoding/json"
"errors"
"reflect" "reflect"
"apigo.cc/go/cast" "apigo.cc/go/cast"
@ -185,6 +186,10 @@ func To[T any](rs *Result) T {
} }
func (rs *Result) To(result any) error { func (rs *Result) To(result any) error {
if result == nil {
return errors.New("result target is nil")
}
if rs.bytesData != nil { if rs.bytesData != nil {
if len(rs.bytesData) > 0 { if len(rs.bytesData) > 0 {
// 优先使用 json.Unmarshal 以支持自定义 Unmarshaler (如 time.Time) // 优先使用 json.Unmarshal 以支持自定义 Unmarshaler (如 time.Time)
@ -195,10 +200,11 @@ func (rs *Result) To(result any) error {
t := reflect.TypeOf(result) t := reflect.TypeOf(result)
v := reflect.ValueOf(result) v := reflect.ValueOf(result)
if t.Kind() == reflect.Ptr { if t.Kind() != reflect.Ptr || v.IsNil() {
return errors.New("result target must be a non-nil pointer")
}
t = t.Elem() t = t.Elem()
v = v.Elem() v = v.Elem()
}
if (t.Kind() == reflect.Struct || t.Kind() == reflect.Map) && rs.keys != nil && rs.bytesDatas != nil { if (t.Kind() == reflect.Struct || t.Kind() == reflect.Map) && rs.keys != nil && rs.bytesDatas != nil {
rm := rs.ResultMap() rm := rs.ResultMap()
@ -207,9 +213,15 @@ func (rs *Result) To(result any) error {
k = cast.GetUpperName(k) k = cast.GetUpperName(k)
sf, found := t.FieldByName(k) sf, found := t.FieldByName(k)
if found { 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 { } else if t.Kind() == reflect.Map {
if v.IsNil() {
v.Set(reflect.MakeMap(t))
}
v.SetMapIndex(reflect.ValueOf(k), r.ToValue(t.Elem())) v.SetMapIndex(reflect.ValueOf(k), r.ToValue(t.Elem()))
} }
} }

View File

@ -130,8 +130,10 @@ func (rd *Redis) receiveSub(subStartChan chan bool) {
if strings.Contains(v.Error(), "i/o timeout") { if strings.Contains(v.Error(), "i/o timeout") {
break break
} }
if !strings.Contains(v.Error(), "connection closed") && !strings.Contains(v.Error(), "use of closed network connection") { // 使用 strings.Contains 是因为 redigo 的错误通常是自定义 string 类型
rd.logger.Error(v.Error(), "type", "redis", "action", "receiveSub") 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() rd.subLock.Lock()
if rd.subConn != nil { if rd.subConn != nil {