Compare commits

..

No commits in common. "main" and "v1.0.1" have entirely different histories.
main ... v1.0.1

11 changed files with 105 additions and 145 deletions

16
AI.md Normal file
View File

@ -0,0 +1,16 @@
# 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,20 +1,5 @@
# 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` 互斥锁。

View File

@ -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)
```

View File

@ -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 3766 267470 ns/op
BenchmarkIDMaker-16 397779 3062 ns/op
BenchmarkGetSet-16 3924 256649 ns/op
BenchmarkIdMaker-16 392859 3017 ns/op
```
- `BenchmarkGetSet`: 每次 GET+SET 耗时约 267微秒。
- `BenchmarkIDMaker`: 每次获取分布式 ID 耗时约 3.0微秒(预取机制效率显著)。
- `BenchmarkGetSet`: 每次 GET+SET 耗时约 256微秒。
- `BenchmarkIdMaker`: 每次获取分布式 ID 耗时约 3.0微秒(预取机制效率显著)。

View File

@ -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++ {

View File

@ -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([]any{key}, stringsToAnys(fields)...)...).Results()
return rd.Do("HMGET", append(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([]any{key}, fieldAndValues...)...).Bool()
return rd.Do("HMSET", append(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([]any{key}, stringsToAnys(fields)...)...).Int()
return rd.Do("HDEL", append(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([]any{key}, stringsToAnys(values)...)...).Int()
return rd.Do("LPUSH", append(append([]any{}, key), stringsToAnys(values)...)...).Int()
}
func (rd *Redis) RPUSH(key string, values ...string) int {
return rd.Do("RPUSH", append([]any{key}, stringsToAnys(values)...)...).Int()
return rd.Do("RPUSH", append(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()
}

16
id.go
View File

@ -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)
// 每次 from Redis 预取 100 个序列号
// 每次 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)
}

View File

@ -231,16 +231,15 @@ func shouldRetry(err error) bool {
}
// 超时错误
var opErr *net.OpError
if errors.As(err, &opErr) && opErr.Timeout() {
if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
return true
}
// Redis特定可恢复错误
var redisErr redis.Error
if errors.As(err, &redisErr) {
errMsg := string(redisErr)
if strings.HasPrefix(errMsg, "LOADING") || strings.HasPrefix(errMsg, "CLUSTERDOWN") {
if errs, ok := err.(redis.Error); ok {
switch {
case strings.HasPrefix(string(errs), "LOADING"),
strings.HasPrefix(string(errs), "CLUSTERDOWN"):
return true
}
}
@ -268,19 +267,17 @@ 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, " ")
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
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 len(values) > 0 {
args = append(args, values...)
}
values = args
}
// 自动序列化
@ -290,9 +287,6 @@ 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()

View File

@ -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,34 +126,13 @@ 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")
}

View File

@ -2,7 +2,6 @@ package redis
import (
"encoding/json"
"errors"
"reflect"
"apigo.cc/go/cast"
@ -186,10 +185,6 @@ 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)
@ -200,11 +195,10 @@ func (rs *Result) To(result any) error {
t := reflect.TypeOf(result)
v := reflect.ValueOf(result)
if t.Kind() != reflect.Ptr || v.IsNil() {
return errors.New("result target must be a non-nil pointer")
if t.Kind() == reflect.Ptr {
t = t.Elem()
v = v.Elem()
}
t = t.Elem()
v = v.Elem()
if (t.Kind() == reflect.Struct || t.Kind() == reflect.Map) && rs.keys != nil && rs.bytesDatas != nil {
rm := rs.ResultMap()
@ -213,15 +207,9 @@ func (rs *Result) To(result any) error {
k = cast.GetUpperName(k)
sf, found := t.FieldByName(k)
if found {
fieldV := v.FieldByName(k)
if fieldV.CanSet() {
fieldV.Set(r.ToValue(sf.Type))
}
v.FieldByName(k).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()))
}
}

View File

@ -130,10 +130,8 @@ func (rd *Redis) receiveSub(subStartChan chan bool) {
if strings.Contains(v.Error(), "i/o timeout") {
break
}
// 使用 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")
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")
}
rd.subLock.Lock()
if rd.subConn != nil {