使用StackExchange.Redis扫描Redis键时KeyAsync循环为何挂起?
Redis键扫描挂起问题排查与修复方案
1. 确保Redis连接完全建立
ConnectionMultiplexer.Connect在AbortOnConnectFail=false时会立即返回实例,但此时连接可能尚未成功建立,后续操作会因等待连接就绪而挂起。
修改连接初始化逻辑,等待连接稳定后再返回:
builder.Services.AddSingleton<IConnectionMultiplexer>(async serviceProvider => { var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>(); const string redisHost = ... const int redisPort = ... const string redisPassword = ... var redisConfiguration = new ConfigurationOptions { EndPoints = { $"{redisHost}:{redisPort}" }, Password = redisPassword, Ssl = true, AbortOnConnectFail = false, ConnectTimeout = 15000, SyncTimeout = 15000, LoggerFactory = loggerFactory }; var multiplexer = await ConnectionMultiplexer.ConnectAsync(redisConfiguration); // 等待连接成功建立 await multiplexer.WaitUntilConnectedAsync(); return multiplexer; });
2. 修正主节点判断逻辑
server.IsReplica在部分Redis部署(如集群、哨兵模式)中可能判断不准确,导致跳过实际主节点。改用ServerType判断:
if (!server.IsConnected || server.ServerType != ServerType.Master) { continue; }
3. 优化KeysAsync调用参数
- 增大
pageSize提升扫描效率(默认值过小可能导致迭代缓慢) - 确保数据库编号与目标库一致
- 确认模式匹配语法符合Redis SCAN规则(
*为通配符,{hangfire}为哈希槽占位符,需与实际键格式匹配)
调整后的扫描代码:
await foreach (var key in server.KeysAsync( database: db.Database, pattern: pattern, pageSize: 1000).WithCancellation(cancellationToken)) { keys.Add(key); }
4. 排查CancellationToken状态
StartAsync的cancellationToken会在宿主服务停止时触发,若程序启动阶段出现异常导致宿主终止,会直接取消扫描操作。添加日志辅助排查:
// 在DataMigration中注入ILogger<DataMigration> logger.LogInformation("开始扫描Redis键,匹配模式:{Pattern}", pattern); await foreach (var key in server.KeysAsync(...)) { logger.LogDebug("找到键:{Key}", key); keys.Add(key); }
5. 升级StackExchange.Redis版本
旧版本的KeysAsync存在异步迭代挂起的已知bug,升级至最新稳定版(如2.7.x及以上)可解决此类问题。
完整修复后的DataMigration示例
public class DataMigration(IConnectionMultiplexer connection, ILogger<DataMigration> logger) : IHostedService { public async Task StartAsync(CancellationToken cancellationToken) { const string pattern = "{hangfire}:recurring-job:EmailNotificationJob:*"; List<RedisKey> keys = []; var db = connection.GetDatabase(); logger.LogInformation("启动Redis键扫描,模式:{Pattern}", pattern); foreach (var endpoint in connection.GetEndPoints()) { var server = connection.GetServer(endpoint); if (!server.IsConnected || server.ServerType != ServerType.Master) { logger.LogDebug("跳过端点 {Endpoint}:未连接或非主节点", endpoint); continue; } logger.LogDebug("扫描端点 {Endpoint} 的键", endpoint); await foreach (var key in server.KeysAsync( database: db.Database, pattern: pattern, pageSize: 1000).WithCancellation(cancellationToken)) { logger.LogDebug("发现键:{Key}", key); keys.Add(key); } } logger.LogInformation("共找到 {KeyCount} 个待更新的键", keys.Count); Console.WriteLine($"Found {keys.Count} keys to update."); } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; }
内容的提问来源于stack exchange,提问作者nop
相关产品推荐
相关产品推荐

