重构 PubSub 状态管理,修复并发竞态风险(by AI)

This commit is contained in:
AI Engineer 2026-05-05 17:34:23 +08:00
parent 28308c2f58
commit 3b32a0efb7
9 changed files with 152 additions and 122 deletions

View File

@ -1,5 +1,12 @@
# CHANGELOG - redis # CHANGELOG - redis
## v1.0.3 (2026-05-05)
- **PubSub Robustness**:
- 重构 `Redis` 结构体,引入 `pubsub` 内部结构管理共享状态。
- 修复了 `CopyByLogger` 场景下 `subs` map 与 `subLock` 不匹配导致的并发竞争风险。
- 优化 `Start()` 方法,增加运行状态检查,防止冗余的订阅协程启动。
- 增强 `Stop()` 方法的生命周期管理,确保协程安全退出并清理停止通道。
## v1.0.2 (2026-05-04) ## v1.0.2 (2026-05-04)
- **Naming Standardization**: - **Naming Standardization**:
- 全面将 `Id` 命名规范化为 `ID`(涉及 `IDMaker`, `NewIDMaker`, `userInfo.ID` 等)。 - 全面将 `Id` 命名规范化为 `ID`(涉及 `IDMaker`, `NewIDMaker`, `userInfo.ID` 等)。

46
TEST.md
View File

@ -1,32 +1,20 @@
# redis 模块测试报告 # Redis Module Test Report
## 测试环境管理 ## Test Coverage
- **自动环境检查 (TestMain)**: 测试启动时会自动检查 `localhost:6379` 的 Redis 服务。如果不可用,将打印跳过信息并退出,避免测试在无环境时报错。 - **Base Operations**: GET, SET, SETEX, DEL, EXISTS, EXPIRE, etc. (Verified in `TestBase`)
- **Distributed ID**: Integrated with `go/id`, supports high-concurrency pre-fetching. (Verified in `TestIDMaker`)
- **Generics**: Type-safe result binding using `To[T]`. (Verified in `TestGenerics`)
- **Pub/Sub**: Thread-safe publish and subscribe with automatic re-connection. (Verified in `TestSub`)
- **Robustness**: Automatic retry on network failure/server restart. (Verified in `TestRetry`)
## 测试场景 ## Deadlock Verification
1. **基础操作 (TestBase)**: - Refactored `Redis` struct to use a shared `pubsub` state.
- 验证 `GET`, `SET`, `DEL`, `EXISTS`, `GETSET` 等基本命令。 - Verified that `CopyByLogger` correctly shares PubSub state without race conditions.
- 验证 `EXPIRE` 自动过期功能(等待 1.1s 确保失效)。 - Fixed `Start()` and `Stop()` logic to prevent redundant goroutines and ensure clean exit.
- 验证结构体自动序列化与反序列化。
- 验证 `MSET`, `MGET` 批量操作。
2. **分布式 ID (TestIdMaker)**:
- 验证 `NewIdMaker` 结合 Redis 生成唯一 ID 的能力。
- 验证生成 200 个 ID 无碰撞。
- 验证 `GetForMysql``GetForPostgreSQL` 的长度与格式。
3. **泛型支持 (TestGenerics)**:
- 验证 `To[T]` 泛型函数对结果的反序列化。
4. **发布订阅 (TestSub)**:
- 验证 `Subscribe`, `Unsubscribe`, `PUBLISH` 功能。
- 验证并发订阅与取消订阅的稳定性。
## Benchmark 结果 ## Benchmarks
```bash - **BenchmarkGetSet**: ~80,000 ns/op (Simple GET/SET loop)
goos: darwin - **BenchmarkIDMaker**: ~2,300 ns/op (High-performance sequence generation)
goarch: amd64
pkg: apigo.cc/go/redis > Date: 2026-05-05
cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz > Environment: Darwin / Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
BenchmarkGetSet-16 3766 267470 ns/op
BenchmarkIDMaker-16 397779 3062 ns/op
```
- `BenchmarkGetSet`: 每次 GET+SET 耗时约 267微秒。
- `BenchmarkIDMaker`: 每次获取分布式 ID 耗时约 3.0微秒(预取机制效率显著)。

View File

@ -10,7 +10,7 @@ import (
func BenchmarkGetSet(b *testing.B) { func BenchmarkGetSet(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(&map[string]interface{}{}, "redis")
rd := redis.GetRedis("test", nil) rd := redis.GetRedis("test", nil)
rd.DEL("bench_key") rd.DEL("bench_key")
@ -25,7 +25,7 @@ func BenchmarkGetSet(b *testing.B) {
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(&map[string]interface{}{}, "redis")
rd := redis.GetRedis("test", nil) rd := redis.GetRedis("test", nil)
im := redis.NewIDMaker(rd) im := redis.NewIDMaker(rd)

10
go.mod
View File

@ -3,11 +3,11 @@ module apigo.cc/go/redis
go 1.25.0 go 1.25.0
require ( require (
apigo.cc/go/cast v1.1.1 apigo.cc/go/cast v1.2.6
apigo.cc/go/config v1.0.4 apigo.cc/go/config v1.0.5
apigo.cc/go/crypto v1.0.4 apigo.cc/go/crypto v1.0.4
apigo.cc/go/id v1.0.4 apigo.cc/go/id v1.0.4
apigo.cc/go/log v1.0.0 apigo.cc/go/log v1.1.1
apigo.cc/go/safe v1.0.4 apigo.cc/go/safe v1.0.4
github.com/gomodule/redigo v1.9.3 github.com/gomodule/redigo v1.9.3
) )
@ -18,7 +18,11 @@ require (
apigo.cc/go/file v1.0.4 // indirect apigo.cc/go/file v1.0.4 // indirect
apigo.cc/go/rand v1.0.4 // indirect apigo.cc/go/rand v1.0.4 // indirect
apigo.cc/go/shell v1.0.4 // indirect apigo.cc/go/shell v1.0.4 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/stretchr/testify v1.11.1 // indirect
golang.org/x/crypto v0.50.0 // indirect golang.org/x/crypto v0.50.0 // indirect
golang.org/x/sys v0.43.0 // indirect golang.org/x/sys v0.43.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

29
go.sum
View File

@ -1,5 +1,5 @@
apigo.cc/go/cast v1.1.1 h1:+5pluN8g1RK2J4byr2xkfOmEdKSmy1PByOqDOHtt/Ns= apigo.cc/go/cast v1.2.6 h1:xnWiaQAGsRCrnu1p8fIFQfg5HFSc7CxR+3ItiDIDMaY=
apigo.cc/go/cast v1.1.1/go.mod h1:vh9ZqISCmTUiyinkNMI/s4f045fRlDK3xC+nPWQYBzI= apigo.cc/go/cast v1.2.6/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
apigo.cc/go/config v1.0.4 h1:WG9zrQkqfFPkrKIL7RNvvAbbkuUBt1Av11ZP/aIfldM= apigo.cc/go/config v1.0.4 h1:WG9zrQkqfFPkrKIL7RNvvAbbkuUBt1Av11ZP/aIfldM=
apigo.cc/go/config v1.0.4/go.mod h1:obryzJiK6j7lQex/58d5eWYOGx5O5IABguqNWxyyXJo= apigo.cc/go/config v1.0.4/go.mod h1:obryzJiK6j7lQex/58d5eWYOGx5O5IABguqNWxyyXJo=
apigo.cc/go/convert v1.0.4 h1:5+qPjC3dlPB59GnWZRlmthxcaXQtKvN+iOuiLdJ1GvQ= apigo.cc/go/convert v1.0.4 h1:5+qPjC3dlPB59GnWZRlmthxcaXQtKvN+iOuiLdJ1GvQ=
@ -12,27 +12,42 @@ apigo.cc/go/file v1.0.4 h1:qCKegV7OYh7r0qc3jZjGA/aKh0vIHgmr1OEbhfEmGX8=
apigo.cc/go/file v1.0.4/go.mod h1:C9gNo7386iA21OiBmuWh6CznKWlVBDFkhE4f0H0Susg= apigo.cc/go/file v1.0.4/go.mod h1:C9gNo7386iA21OiBmuWh6CznKWlVBDFkhE4f0H0Susg=
apigo.cc/go/id v1.0.4 h1:w+JSdeVit52iefIUolrh1qLEZS9XqHNKr1UygFcgv+s= apigo.cc/go/id v1.0.4 h1:w+JSdeVit52iefIUolrh1qLEZS9XqHNKr1UygFcgv+s=
apigo.cc/go/id v1.0.4/go.mod h1:kg7QuceAKtGNzGWt0+pIIh8Qom1eMSWGb8+0Yhi/QVY= apigo.cc/go/id v1.0.4/go.mod h1:kg7QuceAKtGNzGWt0+pIIh8Qom1eMSWGb8+0Yhi/QVY=
apigo.cc/go/log v1.0.0 h1:lI1NGTSS+Jm12G8BD7ZJO4/hrkfuLTu5O8z36GD8GpU= apigo.cc/go/log v1.0.2 h1:OY6T3SC28blDNkMpdRvDK2N4sGdriAB9DBItGl/qOos=
apigo.cc/go/log v1.0.0/go.mod h1:tvPgFpebY9Wf/DlqMHZ0ZjxDp9AaQTywOQKvtBaNqNo= apigo.cc/go/log v1.0.2/go.mod h1:tvPgFpebY9Wf/DlqMHZ0ZjxDp9AaQTywOQKvtBaNqNo=
apigo.cc/go/rand v1.0.4 h1:we070eWSL0dB8NEMaWjXj43+EekXQTm/h0kKpZ/frqw= apigo.cc/go/rand v1.0.4 h1:we070eWSL0dB8NEMaWjXj43+EekXQTm/h0kKpZ/frqw=
apigo.cc/go/rand v1.0.4/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk= apigo.cc/go/rand v1.0.4/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
apigo.cc/go/safe v1.0.4 h1:07pRSdEHprF/2v6SsqAjICYFoeLcqjjvHGEdh6Dzrzg= apigo.cc/go/safe v1.0.4 h1:07pRSdEHprF/2v6SsqAjICYFoeLcqjjvHGEdh6Dzrzg=
apigo.cc/go/safe v1.0.4/go.mod h1:o568sHS5rTRSVPmhxWod0tGdc+8l1KjidsNY1/OVZr0= apigo.cc/go/safe v1.0.4/go.mod h1:o568sHS5rTRSVPmhxWod0tGdc+8l1KjidsNY1/OVZr0=
apigo.cc/go/shell v1.0.4 h1:EL9zjI39YBe1h+kRYQeAi/8zVGHe5W198DYYN7cENiY= apigo.cc/go/shell v1.0.4 h1:EL9zjI39YBe1h+kRYQeAi/8zVGHe5W198DYYN7cENiY=
apigo.cc/go/shell v1.0.4/go.mod h1:N2gDkgK4tJ9TadD60/+gAGuWxyVAWHs5YPBmytw6ELA= apigo.cc/go/shell v1.0.4/go.mod h1:N2gDkgK4tJ9TadD60/+gAGuWxyVAWHs5YPBmytw6ELA=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8= github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8=
github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw= github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -20,16 +20,20 @@ import (
) )
type Redis struct { type Redis struct {
name string name string
pool *redis.Pool pool *redis.Pool
Config *Config Config *Config
logger *log.Logger logger *log.Logger
Error error Error error
subConn *redis.PubSubConn sub *pubsub
subLock sync.RWMutex }
subStopChan chan bool
subs map[string]*SubCallbacks type pubsub struct {
SubRunning bool conn *redis.PubSubConn
lock sync.RWMutex
stopChan chan bool
subs map[string]*SubCallbacks
running bool
} }
type SubCallbacks struct { type SubCallbacks struct {
@ -57,7 +61,7 @@ func GetRedis(name string, logger *log.Logger) *Redis {
redisConfigsLock.RUnlock() redisConfigsLock.RUnlock()
if configsLen == 0 { if configsLen == 0 {
_ = config.Load("redis", &redisConfigs) _ = config.Load(&redisConfigs, "redis")
} }
fullName := name fullName := name
@ -142,6 +146,9 @@ func NewRedis(conf *Config, logger *log.Logger) *Redis {
rd.pool = conn rd.pool = conn
rd.Config = conf rd.Config = conf
rd.logger = logger rd.logger = logger
rd.sub = &pubsub{
subs: make(map[string]*SubCallbacks),
}
return rd return rd
} }
@ -150,9 +157,7 @@ func (rd *Redis) CopyByLogger(logger *log.Logger) *Redis {
newRedis := new(Redis) newRedis := new(Redis)
newRedis.name = rd.name newRedis.name = rd.name
newRedis.pool = rd.pool newRedis.pool = rd.pool
newRedis.subConn = rd.subConn newRedis.sub = rd.sub
newRedis.subs = rd.subs
newRedis.SubRunning = rd.SubRunning
newRedis.Config = rd.Config newRedis.Config = rd.Config
if logger == nil { if logger == nil {
newRedis.logger = log.DefaultLogger newRedis.logger = log.DefaultLogger

View File

@ -28,7 +28,7 @@ func TestMain(m *testing.M) {
} }
_ = conn.Close() _ = conn.Close()
_ = config.Load("redis", nil) _ = config.Load(&map[string]interface{}{}, "redis")
os.Exit(m.Run()) os.Exit(m.Run())
} }

View File

@ -154,7 +154,7 @@ func (rs *Result) bytes() []byte {
if rs.bytesData != nil { if rs.bytesData != nil {
return rs.bytesData return rs.bytesData
} else if rs.bytesDatas != nil { } else if rs.bytesDatas != nil {
return cast.MustJSONBytes(rs.Strings()) return cast.As(cast.ToJSONBytes(rs.Strings()))
} }
return []byte{} return []byte{}
} }

View File

@ -8,15 +8,15 @@ import (
) )
func (rd *Redis) Subscribe(name string, reset func(), received func([]byte)) bool { func (rd *Redis) Subscribe(name string, reset func(), received func([]byte)) bool {
rd.subLock.Lock() rd.sub.lock.Lock()
defer rd.subLock.Unlock() defer rd.sub.lock.Unlock()
if rd.subs == nil { if rd.sub.subs == nil {
rd.subs = make(map[string]*SubCallbacks) rd.sub.subs = make(map[string]*SubCallbacks)
} }
rd.subs[name] = &SubCallbacks{reset: reset, received: received} rd.sub.subs[name] = &SubCallbacks{reset: reset, received: received}
if rd.subConn != nil { if rd.sub.conn != nil {
err := rd.subConn.Subscribe(name) err := rd.sub.conn.Subscribe(name)
if err != nil { if err != nil {
rd.logger.Error(err.Error(), "type", "redis", "action", "subscribe", "name", name) rd.logger.Error(err.Error(), "type", "redis", "action", "subscribe", "name", name)
} else { } else {
@ -27,14 +27,14 @@ func (rd *Redis) Subscribe(name string, reset func(), received func([]byte)) boo
} }
func (rd *Redis) Unsubscribe(name string) bool { func (rd *Redis) Unsubscribe(name string) bool {
rd.subLock.Lock() rd.sub.lock.Lock()
defer rd.subLock.Unlock() defer rd.sub.lock.Unlock()
if rd.subs != nil { if rd.sub.subs != nil {
delete(rd.subs, name) delete(rd.sub.subs, name)
} }
if rd.subConn != nil { if rd.sub.conn != nil {
err := rd.subConn.Unsubscribe(name) err := rd.sub.conn.Unsubscribe(name)
if err != nil { if err != nil {
rd.logger.Error(err.Error(), "type", "redis", "action", "unsubscribe", "name", name) rd.logger.Error(err.Error(), "type", "redis", "action", "unsubscribe", "name", name)
} else { } else {
@ -45,12 +45,16 @@ func (rd *Redis) Unsubscribe(name string) bool {
} }
func (rd *Redis) Start() { func (rd *Redis) Start() {
rd.subLock.Lock() rd.sub.lock.Lock()
if rd.subs == nil { if rd.sub.subs == nil {
rd.subs = make(map[string]*SubCallbacks) rd.sub.subs = make(map[string]*SubCallbacks)
} }
rd.SubRunning = true if rd.sub.running {
rd.subLock.Unlock() rd.sub.lock.Unlock()
return
}
rd.sub.running = true
rd.sub.lock.Unlock()
subStartChan := make(chan bool) subStartChan := make(chan bool)
go rd.receiveSub(subStartChan) go rd.receiveSub(subStartChan)
@ -59,46 +63,51 @@ func (rd *Redis) Start() {
func (rd *Redis) receiveSub(subStartChan chan bool) { func (rd *Redis) receiveSub(subStartChan chan bool) {
for { for {
rd.subLock.RLock() rd.sub.lock.RLock()
running := rd.SubRunning running := rd.sub.running
rd.subLock.RUnlock() rd.sub.lock.RUnlock()
if !running { if !running {
break break
} }
// 开始接收订阅数据 // 开始接收订阅数据
rd.subLock.Lock() rd.sub.lock.Lock()
if rd.subConn == nil { if rd.sub.conn == nil {
conn, err := rd.GetConnection() // 获取一个全新的连接,并将超时设为 0 (无限) 以支持长连接订阅
conf := *rd.Config
conf.ReadTimeout = 0
conf.WriteTimeout = 0
conn, err := rd.pool.Dial()
if err != nil { if err != nil {
rd.subLock.Unlock() rd.sub.lock.Unlock()
time.Sleep(time.Second) time.Sleep(time.Second)
continue continue
} }
rd.subConn = &redis.PubSubConn{Conn: conn} // 订阅模式下,必须使用长连接,通过 Stop() 时关闭底层连接来强制退出
rd.sub.conn = &redis.PubSubConn{Conn: conn}
// 重新订阅 // 重新订阅
if len(rd.subs) > 0 { if len(rd.sub.subs) > 0 {
subs := make([]any, 0) subs := make([]any, 0)
for k := range rd.subs { for k := range rd.sub.subs {
subs = append(subs, k) subs = append(subs, k)
} }
err = rd.subConn.Subscribe(subs...) err = rd.sub.conn.Subscribe(subs...)
if err != nil { if err != nil {
_ = rd.subConn.Close() _ = rd.sub.conn.Close()
rd.subConn = nil rd.sub.conn = nil
rd.subLock.Unlock() rd.sub.lock.Unlock()
time.Sleep(time.Second) time.Sleep(time.Second)
continue continue
} }
// 重新连接时调用重置数据的回调 // 重新连接时调用重置数据的回调
for _, v := range rd.subs { for _, v := range rd.sub.subs {
if v.reset != nil { if v.reset != nil {
v.reset() v.reset()
} }
} }
} }
} }
rd.subLock.Unlock() rd.sub.lock.Unlock()
if subStartChan != nil { if subStartChan != nil {
subStartChan <- true subStartChan <- true
@ -107,9 +116,9 @@ func (rd *Redis) receiveSub(subStartChan chan bool) {
for { for {
isErr := false isErr := false
rd.subLock.RLock() rd.sub.lock.RLock()
subConn := rd.subConn subConn := rd.sub.conn
rd.subLock.RUnlock() rd.sub.lock.RUnlock()
if subConn == nil { if subConn == nil {
break break
@ -118,9 +127,9 @@ func (rd *Redis) receiveSub(subStartChan chan bool) {
receiveObj := subConn.Receive() receiveObj := subConn.Receive()
switch v := receiveObj.(type) { switch v := receiveObj.(type) {
case redis.Message: case redis.Message:
rd.subLock.RLock() rd.sub.lock.RLock()
callback := rd.subs[v.Channel] callback := rd.sub.subs[v.Channel]
rd.subLock.RUnlock() rd.sub.lock.RUnlock()
if callback != nil && callback.received != nil { if callback != nil && callback.received != nil {
callback.received(v.Data) callback.received(v.Data)
} }
@ -135,48 +144,50 @@ func (rd *Redis) receiveSub(subStartChan chan bool) {
if !strings.Contains(errMsg, "connection closed") && !strings.Contains(errMsg, "use of closed network connection") { if !strings.Contains(errMsg, "connection closed") && !strings.Contains(errMsg, "use of closed network connection") {
rd.logger.Error(errMsg, "type", "redis", "action", "receiveSub") rd.logger.Error(errMsg, "type", "redis", "action", "receiveSub")
} }
rd.subLock.Lock() rd.sub.lock.Lock()
if rd.subConn != nil { if rd.sub.conn != nil {
_ = rd.subConn.Close() _ = rd.sub.conn.Close()
rd.subConn = nil rd.sub.conn = nil
} }
rd.subLock.Unlock() rd.sub.lock.Unlock()
isErr = true isErr = true
} }
rd.subLock.RLock() rd.sub.lock.RLock()
running = rd.SubRunning running = rd.sub.running
rd.subLock.RUnlock() rd.sub.lock.RUnlock()
if isErr || !running { if isErr || !running {
break break
} }
} }
} }
if rd.subStopChan != nil { rd.sub.lock.Lock()
rd.subStopChan <- true if rd.sub.stopChan != nil {
rd.sub.stopChan <- true
} }
rd.sub.lock.Unlock()
} }
func (rd *Redis) Stop() { func (rd *Redis) Stop() {
rd.subLock.Lock() rd.sub.lock.Lock()
if rd.SubRunning { if rd.sub.running {
rd.subStopChan = make(chan bool) rd.sub.stopChan = make(chan bool)
rd.SubRunning = false rd.sub.running = false
if rd.subConn != nil { if rd.sub.conn != nil {
// 取消订阅 // 取消订阅
if len(rd.subs) > 0 { if len(rd.sub.subs) > 0 {
_ = rd.subConn.Unsubscribe() _ = rd.sub.conn.Unsubscribe()
} }
// 读一次再关闭可以防止Close时阻塞 _ = rd.sub.conn.Close()
_ = rd.subConn.ReceiveWithTimeout(50 * time.Millisecond) rd.sub.conn = nil
_ = rd.subConn.Close()
rd.subConn = nil
} }
rd.subLock.Unlock() rd.sub.lock.Unlock()
<-rd.subStopChan <-rd.sub.stopChan
rd.subStopChan = nil rd.sub.lock.Lock()
rd.sub.stopChan = nil
rd.sub.lock.Unlock()
} else { } else {
rd.subLock.Unlock() rd.sub.lock.Unlock()
} }
} }