重构 PubSub 状态管理,修复并发竞态风险(by AI)
This commit is contained in:
parent
28308c2f58
commit
3b32a0efb7
@ -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
46
TEST.md
@ -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微秒(预取机制效率显著)。
|
|
||||||
|
|||||||
@ -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
10
go.mod
@ -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
29
go.sum
@ -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=
|
||||||
|
|||||||
21
redis.go
21
redis.go
@ -25,11 +25,15 @@ type Redis struct {
|
|||||||
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
|
|
||||||
|
type pubsub struct {
|
||||||
|
conn *redis.PubSubConn
|
||||||
|
lock sync.RWMutex
|
||||||
|
stopChan chan bool
|
||||||
subs map[string]*SubCallbacks
|
subs map[string]*SubCallbacks
|
||||||
SubRunning bool
|
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
|
||||||
|
|||||||
@ -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())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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{}
|
||||||
}
|
}
|
||||||
|
|||||||
141
subscribe.go
141
subscribe.go
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user