Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28308c2f58 |
16
AI.md
16
AI.md
@ -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。
|
|
||||||
15
CHANGELOG.md
15
CHANGELOG.md
@ -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` 互斥锁。
|
||||||
|
|||||||
@ -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)
|
||||||
```
|
```
|
||||||
|
|||||||
8
TEST.md
8
TEST.md
@ -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微秒(预取机制效率显著)。
|
||||||
|
|||||||
@ -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++ {
|
||||||
|
|||||||
86
commands.go
86
commands.go
@ -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
16
id.go
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
18
redis.go
18
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
|
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()
|
||||||
|
|||||||
@ -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")
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
18
result.go
18
result.go
@ -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()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user