mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-10 00:33:28 +08:00
fix(redis): 避免 Redis 搜索因稀疏 SCAN MATCH 返回空结果 (#746)
fixes #738 Redis SCAN MATCH 可能连续返回空批次,但后续 cursor 页仍然存在匹配 key。 此前搜索模式最多扫描 16 轮,可能导致首次点击搜索无数据,而点击“加载更多”后才出现结果。 移除搜索模式的固定轮数限制,改由已有的搜索时间上限和目标数量限制兜底。 新增回归测试,验证搜索可以继续越过 16 个空匹配页并返回后续结果。
This commit is contained in:
@@ -47,7 +47,6 @@ const (
|
||||
redisScanMaxDuration = 12 * time.Second
|
||||
redisSearchMaxTargetCount int64 = 1000
|
||||
redisSearchMaxStepCount int64 = 1000
|
||||
redisSearchMaxRounds = 16
|
||||
redisSearchMaxDuration = 3 * time.Second
|
||||
)
|
||||
|
||||
@@ -552,7 +551,9 @@ func (r *RedisClientImpl) ScanKeys(pattern string, cursor uint64, count int64) (
|
||||
if scanStepCount > redisSearchMaxStepCount {
|
||||
scanStepCount = redisSearchMaxStepCount
|
||||
}
|
||||
maxRounds = redisSearchMaxRounds
|
||||
// SCAN MATCH 可能连续返回空批次,但后续 cursor 页仍然存在匹配 key。
|
||||
// 搜索模式不使用固定轮数限制,改由 maxDuration 和 targetCount 兜底。
|
||||
maxRounds = 0
|
||||
maxDuration = redisSearchMaxDuration
|
||||
}
|
||||
|
||||
@@ -604,7 +605,7 @@ func (r *RedisClientImpl) ScanKeys(pattern string, cursor uint64, count int64) (
|
||||
|
||||
nodeCursor = nextCursor
|
||||
round++
|
||||
if nodeCursor == 0 || round >= maxRounds {
|
||||
if nodeCursor == 0 || (maxRounds > 0 && round >= maxRounds) {
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -653,7 +654,7 @@ func (r *RedisClientImpl) ScanKeys(pattern string, cursor uint64, count int64) (
|
||||
|
||||
currentCursor = nextCursor
|
||||
round++
|
||||
if currentCursor == 0 || round >= maxRounds {
|
||||
if currentCursor == 0 || (maxRounds > 0 && round >= maxRounds) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
goredis "github.com/redis/go-redis/v9"
|
||||
)
|
||||
@@ -741,6 +742,81 @@ func TestListRemoveUsesLRemForOneMatchingValue(t *testing.T) {
|
||||
t.Fatalf("expected LREM command, got %v", commands)
|
||||
}
|
||||
|
||||
func TestRedisSearchScanContinuesPastEmptyMatchedPages(t *testing.T) {
|
||||
var mu sync.Mutex
|
||||
scanCalls := 0
|
||||
const searchPattern = "*[lL][aA][tT][eE]*"
|
||||
|
||||
redisScanResponse := func(cursor string, keys ...string) string {
|
||||
var builder strings.Builder
|
||||
builder.WriteString("*2\r\n")
|
||||
builder.WriteString(redisBulkString(cursor))
|
||||
builder.WriteString(fmt.Sprintf("*%d\r\n", len(keys)))
|
||||
for _, key := range keys {
|
||||
builder.WriteString(redisBulkString(key))
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
addr := startRedisProtocolTestServer(t, func(args []string) string {
|
||||
command := strings.ToUpper(strings.TrimSpace(args[0]))
|
||||
switch command {
|
||||
case "HELLO":
|
||||
return "-ERR unknown command 'HELLO'\r\n"
|
||||
case "CLIENT":
|
||||
return "-ERR unknown subcommand\r\n"
|
||||
case "SCAN":
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
scanCalls++
|
||||
for i := 0; i+1 < len(args); i++ {
|
||||
if strings.EqualFold(args[i], "MATCH") && args[i+1] != searchPattern {
|
||||
t.Fatalf("expected SCAN MATCH %q, got command %v", searchPattern, args)
|
||||
}
|
||||
}
|
||||
if scanCalls <= 16 {
|
||||
return redisScanResponse(strconv.Itoa(scanCalls))
|
||||
}
|
||||
return redisScanResponse("0", "late:user:1")
|
||||
case "TYPE":
|
||||
return "+string\r\n"
|
||||
case "TTL":
|
||||
return ":-1\r\n"
|
||||
}
|
||||
return "+OK\r\n"
|
||||
})
|
||||
|
||||
rawClient := goredis.NewClient(&goredis.Options{
|
||||
Addr: addr,
|
||||
Protocol: 2,
|
||||
})
|
||||
client := &RedisClientImpl{
|
||||
client: rawClient,
|
||||
singleClient: rawClient,
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
result, err := client.ScanKeys(searchPattern, 0, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("ScanKeys returned error: %v", err)
|
||||
}
|
||||
if result == nil || len(result.Keys) != 1 {
|
||||
t.Fatalf("expected one searched key after empty pages, got %#v", result)
|
||||
}
|
||||
if result.Keys[0].Key != "late:user:1" {
|
||||
t.Fatalf("expected late:user:1, got %#v", result.Keys[0])
|
||||
}
|
||||
if result.Cursor != "0" {
|
||||
t.Fatalf("expected completed cursor, got %q", result.Cursor)
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if scanCalls <= 16 {
|
||||
t.Fatalf("expected ScanKeys to continue past 16 empty search pages, got %d calls", scanCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedisSelectDBReconnectsWithSentinelConfig(t *testing.T) {
|
||||
oldConnect := redisDBSwitchConnect
|
||||
defer func() {
|
||||
|
||||
Reference in New Issue
Block a user