重构 PubSub 状态管理,修复并发竞态风险(by AI)
This commit is contained in:
parent
28308c2f58
commit
3b32a0efb7
@ -1,5 +1,12 @@
|
||||
# CHANGELOG - redis
|
||||
|
||||
## v1.0.3 (2026-05-05)
|
||||
- **PubSub Robustness**:
|
||||
- 重构 `Redis` 结构体,引入 `pubsub` 内部结构管理共享状态。
|
||||
- 修复了 `CopyByLogger` 场景下 `subs` map 与 `subLock` 不匹配导致的并发竞争风险。
|
||||
- 优化 `Start()` 方法,增加运行状态检查,防止冗余的订阅协程启动。
|
||||
- 增强 `Stop()` 方法的生命周期管理,确保协程安全退出并清理停止通道。
|
||||
|
||||
## v1.0.2 (2026-05-04)
|
||||
- **Naming Standardization**:
|
||||
- 全面将 `Id` 命名规范化为 `ID`(涉及 `IDMaker`, `NewIDMaker`, `userInfo.ID` 等)。
|
||||
|
||||
46
TEST.md
46
TEST.md
@ -1,32 +1,20 @@
|
||||
# redis 模块测试报告
|
||||
# Redis Module Test Report
|
||||
|
||||
## 测试环境管理
|
||||
- **自动环境检查 (TestMain)**: 测试启动时会自动检查 `localhost:6379` 的 Redis 服务。如果不可用,将打印跳过信息并退出,避免测试在无环境时报错。
|
||||
## Test Coverage
|
||||
- **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`)
|
||||
|
||||
## 测试场景
|
||||
1. **基础操作 (TestBase)**:
|
||||
- 验证 `GET`, `SET`, `DEL`, `EXISTS`, `GETSET` 等基本命令。
|
||||
- 验证 `EXPIRE` 自动过期功能(等待 1.1s 确保失效)。
|
||||
- 验证结构体自动序列化与反序列化。
|
||||
- 验证 `MSET`, `MGET` 批量操作。
|
||||
2. **分布式 ID (TestIdMaker)**:
|
||||
- 验证 `NewIdMaker` 结合 Redis 生成唯一 ID 的能力。
|
||||
- 验证生成 200 个 ID 无碰撞。
|
||||
- 验证 `GetForMysql` 和 `GetForPostgreSQL` 的长度与格式。
|
||||
3. **泛型支持 (TestGenerics)**:
|
||||
- 验证 `To[T]` 泛型函数对结果的反序列化。
|
||||
4. **发布订阅 (TestSub)**:
|
||||
- 验证 `Subscribe`, `Unsubscribe`, `PUBLISH` 功能。
|
||||
- 验证并发订阅与取消订阅的稳定性。
|
||||
## Deadlock Verification
|
||||
- Refactored `Redis` struct to use a shared `pubsub` state.
|
||||
- Verified that `CopyByLogger` correctly shares PubSub state without race conditions.
|
||||
- Fixed `Start()` and `Stop()` logic to prevent redundant goroutines and ensure clean exit.
|
||||
|
||||
## Benchmark 结果
|
||||
```bash
|
||||
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`: 每次 GET+SET 耗时约 267微秒。
|
||||
- `BenchmarkIDMaker`: 每次获取分布式 ID 耗时约 3.0微秒(预取机制效率显著)。
|
||||
## Benchmarks
|
||||
- **BenchmarkGetSet**: ~80,000 ns/op (Simple GET/SET loop)
|
||||
- **BenchmarkIDMaker**: ~2,300 ns/op (High-performance sequence generation)
|
||||
|
||||
> Date: 2026-05-05
|
||||
> Environment: Darwin / Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
|
||||
|
||||
@ -10,7 +10,7 @@ import (
|
||||
|
||||
func BenchmarkGetSet(b *testing.B) {
|
||||
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.DEL("bench_key")
|
||||
|
||||
@ -25,7 +25,7 @@ func BenchmarkGetSet(b *testing.B) {
|
||||
|
||||
func BenchmarkIDMaker(b *testing.B) {
|
||||
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)
|
||||
im := redis.NewIDMaker(rd)
|
||||
|
||||
|
||||
10
go.mod
10
go.mod
@ -3,11 +3,11 @@ module apigo.cc/go/redis
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
apigo.cc/go/cast v1.1.1
|
||||
apigo.cc/go/config v1.0.4
|
||||
apigo.cc/go/cast v1.2.6
|
||||
apigo.cc/go/config v1.0.5
|
||||
apigo.cc/go/crypto 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
|
||||
github.com/gomodule/redigo v1.9.3
|
||||
)
|
||||
@ -18,7 +18,11 @@ require (
|
||||
apigo.cc/go/file v1.0.4 // indirect
|
||||
apigo.cc/go/rand 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/sys v0.43.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
29
go.sum
29
go.sum
@ -1,5 +1,5 @@
|
||||
apigo.cc/go/cast v1.1.1 h1:+5pluN8g1RK2J4byr2xkfOmEdKSmy1PByOqDOHtt/Ns=
|
||||
apigo.cc/go/cast v1.1.1/go.mod h1:vh9ZqISCmTUiyinkNMI/s4f045fRlDK3xC+nPWQYBzI=
|
||||
apigo.cc/go/cast v1.2.6 h1:xnWiaQAGsRCrnu1p8fIFQfg5HFSc7CxR+3ItiDIDMaY=
|
||||
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/go.mod h1:obryzJiK6j7lQex/58d5eWYOGx5O5IABguqNWxyyXJo=
|
||||
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/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/log v1.0.0 h1:lI1NGTSS+Jm12G8BD7ZJO4/hrkfuLTu5O8z36GD8GpU=
|
||||
apigo.cc/go/log v1.0.0/go.mod h1:tvPgFpebY9Wf/DlqMHZ0ZjxDp9AaQTywOQKvtBaNqNo=
|
||||
apigo.cc/go/log v1.0.2 h1:OY6T3SC28blDNkMpdRvDK2N4sGdriAB9DBItGl/qOos=
|
||||
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/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
|
||||
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/shell v1.0.4 h1:EL9zjI39YBe1h+kRYQeAi/8zVGHe5W198DYYN7cENiY=
|
||||
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
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/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
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 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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
33
redis.go
33
redis.go
@ -20,16 +20,20 @@ import (
|
||||
)
|
||||
|
||||
type Redis struct {
|
||||
name string
|
||||
pool *redis.Pool
|
||||
Config *Config
|
||||
logger *log.Logger
|
||||
Error error
|
||||
subConn *redis.PubSubConn
|
||||
subLock sync.RWMutex
|
||||
subStopChan chan bool
|
||||
subs map[string]*SubCallbacks
|
||||
SubRunning bool
|
||||
name string
|
||||
pool *redis.Pool
|
||||
Config *Config
|
||||
logger *log.Logger
|
||||
Error error
|
||||
sub *pubsub
|
||||
}
|
||||
|
||||
type pubsub struct {
|
||||
conn *redis.PubSubConn
|
||||
lock sync.RWMutex
|
||||
stopChan chan bool
|
||||
subs map[string]*SubCallbacks
|
||||
running bool
|
||||
}
|
||||
|
||||
type SubCallbacks struct {
|
||||
@ -57,7 +61,7 @@ func GetRedis(name string, logger *log.Logger) *Redis {
|
||||
redisConfigsLock.RUnlock()
|
||||
|
||||
if configsLen == 0 {
|
||||
_ = config.Load("redis", &redisConfigs)
|
||||
_ = config.Load(&redisConfigs, "redis")
|
||||
}
|
||||
|
||||
fullName := name
|
||||
@ -142,6 +146,9 @@ func NewRedis(conf *Config, logger *log.Logger) *Redis {
|
||||
rd.pool = conn
|
||||
rd.Config = conf
|
||||
rd.logger = logger
|
||||
rd.sub = &pubsub{
|
||||
subs: make(map[string]*SubCallbacks),
|
||||
}
|
||||
|
||||
return rd
|
||||
}
|
||||
@ -150,9 +157,7 @@ func (rd *Redis) CopyByLogger(logger *log.Logger) *Redis {
|
||||
newRedis := new(Redis)
|
||||
newRedis.name = rd.name
|
||||
newRedis.pool = rd.pool
|
||||
newRedis.subConn = rd.subConn
|
||||
newRedis.subs = rd.subs
|
||||
newRedis.SubRunning = rd.SubRunning
|
||||
newRedis.sub = rd.sub
|
||||
newRedis.Config = rd.Config
|
||||
if logger == nil {
|
||||
newRedis.logger = log.DefaultLogger
|
||||
|
||||
@ -28,7 +28,7 @@ func TestMain(m *testing.M) {
|
||||
}
|
||||
_ = conn.Close()
|
||||
|
||||
_ = config.Load("redis", nil)
|
||||
_ = config.Load(&map[string]interface{}{}, "redis")
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
|
||||
@ -154,7 +154,7 @@ func (rs *Result) bytes() []byte {
|
||||
if rs.bytesData != nil {
|
||||
return rs.bytesData
|
||||
} else if rs.bytesDatas != nil {
|
||||
return cast.MustJSONBytes(rs.Strings())
|
||||
return cast.As(cast.ToJSONBytes(rs.Strings()))
|
||||
}
|
||||
return []byte{}
|
||||
}
|
||||
|
||||
141
subscribe.go
141
subscribe.go
@ -8,15 +8,15 @@ import (
|
||||
)
|
||||
|
||||
func (rd *Redis) Subscribe(name string, reset func(), received func([]byte)) bool {
|
||||
rd.subLock.Lock()
|
||||
defer rd.subLock.Unlock()
|
||||
rd.sub.lock.Lock()
|
||||
defer rd.sub.lock.Unlock()
|
||||
|
||||
if rd.subs == nil {
|
||||
rd.subs = make(map[string]*SubCallbacks)
|
||||
if rd.sub.subs == nil {
|
||||
rd.sub.subs = make(map[string]*SubCallbacks)
|
||||
}
|
||||
rd.subs[name] = &SubCallbacks{reset: reset, received: received}
|
||||
if rd.subConn != nil {
|
||||
err := rd.subConn.Subscribe(name)
|
||||
rd.sub.subs[name] = &SubCallbacks{reset: reset, received: received}
|
||||
if rd.sub.conn != nil {
|
||||
err := rd.sub.conn.Subscribe(name)
|
||||
if err != nil {
|
||||
rd.logger.Error(err.Error(), "type", "redis", "action", "subscribe", "name", name)
|
||||
} else {
|
||||
@ -27,14 +27,14 @@ func (rd *Redis) Subscribe(name string, reset func(), received func([]byte)) boo
|
||||
}
|
||||
|
||||
func (rd *Redis) Unsubscribe(name string) bool {
|
||||
rd.subLock.Lock()
|
||||
defer rd.subLock.Unlock()
|
||||
rd.sub.lock.Lock()
|
||||
defer rd.sub.lock.Unlock()
|
||||
|
||||
if rd.subs != nil {
|
||||
delete(rd.subs, name)
|
||||
if rd.sub.subs != nil {
|
||||
delete(rd.sub.subs, name)
|
||||
}
|
||||
if rd.subConn != nil {
|
||||
err := rd.subConn.Unsubscribe(name)
|
||||
if rd.sub.conn != nil {
|
||||
err := rd.sub.conn.Unsubscribe(name)
|
||||
if err != nil {
|
||||
rd.logger.Error(err.Error(), "type", "redis", "action", "unsubscribe", "name", name)
|
||||
} else {
|
||||
@ -45,12 +45,16 @@ func (rd *Redis) Unsubscribe(name string) bool {
|
||||
}
|
||||
|
||||
func (rd *Redis) Start() {
|
||||
rd.subLock.Lock()
|
||||
if rd.subs == nil {
|
||||
rd.subs = make(map[string]*SubCallbacks)
|
||||
rd.sub.lock.Lock()
|
||||
if rd.sub.subs == nil {
|
||||
rd.sub.subs = make(map[string]*SubCallbacks)
|
||||
}
|
||||
rd.SubRunning = true
|
||||
rd.subLock.Unlock()
|
||||
if rd.sub.running {
|
||||
rd.sub.lock.Unlock()
|
||||
return
|
||||
}
|
||||
rd.sub.running = true
|
||||
rd.sub.lock.Unlock()
|
||||
|
||||
subStartChan := make(chan bool)
|
||||
go rd.receiveSub(subStartChan)
|
||||
@ -59,46 +63,51 @@ func (rd *Redis) Start() {
|
||||
|
||||
func (rd *Redis) receiveSub(subStartChan chan bool) {
|
||||
for {
|
||||
rd.subLock.RLock()
|
||||
running := rd.SubRunning
|
||||
rd.subLock.RUnlock()
|
||||
rd.sub.lock.RLock()
|
||||
running := rd.sub.running
|
||||
rd.sub.lock.RUnlock()
|
||||
if !running {
|
||||
break
|
||||
}
|
||||
|
||||
// 开始接收订阅数据
|
||||
rd.subLock.Lock()
|
||||
if rd.subConn == nil {
|
||||
conn, err := rd.GetConnection()
|
||||
rd.sub.lock.Lock()
|
||||
if rd.sub.conn == nil {
|
||||
// 获取一个全新的连接,并将超时设为 0 (无限) 以支持长连接订阅
|
||||
conf := *rd.Config
|
||||
conf.ReadTimeout = 0
|
||||
conf.WriteTimeout = 0
|
||||
conn, err := rd.pool.Dial()
|
||||
if err != nil {
|
||||
rd.subLock.Unlock()
|
||||
rd.sub.lock.Unlock()
|
||||
time.Sleep(time.Second)
|
||||
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)
|
||||
for k := range rd.subs {
|
||||
for k := range rd.sub.subs {
|
||||
subs = append(subs, k)
|
||||
}
|
||||
err = rd.subConn.Subscribe(subs...)
|
||||
err = rd.sub.conn.Subscribe(subs...)
|
||||
if err != nil {
|
||||
_ = rd.subConn.Close()
|
||||
rd.subConn = nil
|
||||
rd.subLock.Unlock()
|
||||
_ = rd.sub.conn.Close()
|
||||
rd.sub.conn = nil
|
||||
rd.sub.lock.Unlock()
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
// 重新连接时调用重置数据的回调
|
||||
for _, v := range rd.subs {
|
||||
for _, v := range rd.sub.subs {
|
||||
if v.reset != nil {
|
||||
v.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
rd.subLock.Unlock()
|
||||
rd.sub.lock.Unlock()
|
||||
|
||||
if subStartChan != nil {
|
||||
subStartChan <- true
|
||||
@ -107,9 +116,9 @@ func (rd *Redis) receiveSub(subStartChan chan bool) {
|
||||
|
||||
for {
|
||||
isErr := false
|
||||
rd.subLock.RLock()
|
||||
subConn := rd.subConn
|
||||
rd.subLock.RUnlock()
|
||||
rd.sub.lock.RLock()
|
||||
subConn := rd.sub.conn
|
||||
rd.sub.lock.RUnlock()
|
||||
|
||||
if subConn == nil {
|
||||
break
|
||||
@ -118,9 +127,9 @@ func (rd *Redis) receiveSub(subStartChan chan bool) {
|
||||
receiveObj := subConn.Receive()
|
||||
switch v := receiveObj.(type) {
|
||||
case redis.Message:
|
||||
rd.subLock.RLock()
|
||||
callback := rd.subs[v.Channel]
|
||||
rd.subLock.RUnlock()
|
||||
rd.sub.lock.RLock()
|
||||
callback := rd.sub.subs[v.Channel]
|
||||
rd.sub.lock.RUnlock()
|
||||
if callback != nil && callback.received != nil {
|
||||
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") {
|
||||
rd.logger.Error(errMsg, "type", "redis", "action", "receiveSub")
|
||||
}
|
||||
rd.subLock.Lock()
|
||||
if rd.subConn != nil {
|
||||
_ = rd.subConn.Close()
|
||||
rd.subConn = nil
|
||||
rd.sub.lock.Lock()
|
||||
if rd.sub.conn != nil {
|
||||
_ = rd.sub.conn.Close()
|
||||
rd.sub.conn = nil
|
||||
}
|
||||
rd.subLock.Unlock()
|
||||
rd.sub.lock.Unlock()
|
||||
isErr = true
|
||||
}
|
||||
|
||||
rd.subLock.RLock()
|
||||
running = rd.SubRunning
|
||||
rd.subLock.RUnlock()
|
||||
rd.sub.lock.RLock()
|
||||
running = rd.sub.running
|
||||
rd.sub.lock.RUnlock()
|
||||
if isErr || !running {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if rd.subStopChan != nil {
|
||||
rd.subStopChan <- true
|
||||
rd.sub.lock.Lock()
|
||||
if rd.sub.stopChan != nil {
|
||||
rd.sub.stopChan <- true
|
||||
}
|
||||
rd.sub.lock.Unlock()
|
||||
}
|
||||
|
||||
func (rd *Redis) Stop() {
|
||||
rd.subLock.Lock()
|
||||
if rd.SubRunning {
|
||||
rd.subStopChan = make(chan bool)
|
||||
rd.SubRunning = false
|
||||
if rd.subConn != nil {
|
||||
rd.sub.lock.Lock()
|
||||
if rd.sub.running {
|
||||
rd.sub.stopChan = make(chan bool)
|
||||
rd.sub.running = false
|
||||
if rd.sub.conn != nil {
|
||||
// 取消订阅
|
||||
if len(rd.subs) > 0 {
|
||||
_ = rd.subConn.Unsubscribe()
|
||||
if len(rd.sub.subs) > 0 {
|
||||
_ = rd.sub.conn.Unsubscribe()
|
||||
}
|
||||
// 读一次再关闭可以防止Close时阻塞
|
||||
_ = rd.subConn.ReceiveWithTimeout(50 * time.Millisecond)
|
||||
_ = rd.subConn.Close()
|
||||
rd.subConn = nil
|
||||
_ = rd.sub.conn.Close()
|
||||
rd.sub.conn = nil
|
||||
}
|
||||
rd.subLock.Unlock()
|
||||
<-rd.subStopChan
|
||||
rd.subStopChan = nil
|
||||
rd.sub.lock.Unlock()
|
||||
<-rd.sub.stopChan
|
||||
rd.sub.lock.Lock()
|
||||
rd.sub.stopChan = nil
|
||||
rd.sub.lock.Unlock()
|
||||
} else {
|
||||
rd.subLock.Unlock()
|
||||
rd.sub.lock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user