Redis连接失败主因是ConnectionMultiplexer未单例化,应全局初始化并配置abortConnect=false、超时参数;MemoryCache与Redis语义不同,需分层使用;序列化推荐Protobuf避免循环引用;缓存击穿需Redis层加锁+空值缓存。

Redis 连接失败但本地 MemoryCache 正常?检查 ConnectionMultiplexer 初始化方式
分布式缓存失效的第一反应,往往是 Redis 连不上,而不是代码逻辑错了。C# 里 ConnectionMultiplexer 是线程安全的单例,但很多人在每次用缓存时都 new 一个——这会导致连接爆炸、超时、Socket 异常,甚至让 Redis 服务端拒绝新连接。
正确做法是全局只初始化一次,并设置合理的重连策略:
- 用
Lazy或 DI 容器注册为 Singleton - 必须配置
abortConnect=false,否则首次连接失败就直接抛异常 - 加上
connectTimeout=5000和syncTimeout=2000,避免阻塞主线程 - 别在
appsettings.json里硬写密码,用ConfigurationOptions.Parse()+ 环境变量注入更安全
示例片段:
var options = ConfigurationOptions.Parse("localhost:6379");
options.AbortOnConnectFail = false;
options.ConnectTimeout = 5000;
options.SyncTimeout = 2000;
_connection = ConnectionMultiplexer.Connect(options);
MemoryCache 和 Redis 数据不一致?别直接套用同一套 ICache 接口
.NET 的 MemoryCache 和 StackExchange.Redis 的操作语义根本不同:前者支持绝对过期、滑动过期、回调通知;后者只认 TTL(秒级),且不支持 Key 失效回调。强行用统一抽象包装,大概率导致缓存雪崩或脏读。
真实项目里更稳妥的做法是分层使用:
- 高频读 + 低一致性要求 → 直接走
MemoryCache(比如配置项、枚举字典) - 跨进程共享 + 强一致性要求 → 走 Redis,但手动控制
EXPIRE时间,别依赖SetAsync(key, value, TimeSpan)的自动 TTL(它底层调的是SETEX,精度有限) - 需要两级缓存协同?用
TryGetValue先查内存,没命中再查 Redis,查到后同步写回内存(注意加锁防重复加载)
关键点:MemoryCache 的 SlidingExpiration 在 Redis 里没有对应物,别试图“模拟”。
序列化报错 System.Text.Json.JsonException: A possible object cycle was detected?别默认用 System.Text.Json
StackExchange.Redis 默认用 System.Text.Json 序列化对象,但它对循环引用、接口类型、无参构造函数缺失非常敏感。而你业务模型里很可能有导航属性、DTO 继承链、或者用了 record 但没标 [JsonConstructor]。
比改模型更现实的解法是换序列化器:
- 用
Newtonsoft.Json:加PreserveReferencesHandling.Objects解决循环引用 - 或直接存
byte[],自己用Protobuf-net序列化(体积小、性能高、无反射开销) - 千万别在 Redis 中存
DateTimeKind.Unspecified—— 不同服务器时区可能让DateTime解析错乱
示例(Protobuf):
var bytes = Serializer.Serialize<mydto>(dto); db.StringSet(key, bytes); // 不走 JSON</mydto>
缓存穿透/击穿没防护?光靠 GetOrCreateAsync 不够
MemoryCache.GetOrCreateAsync 只解决本地并发加载,对 Redis 层完全无效。如果大量请求同时打中一个已过期的热点 Key(比如商品详情),所有请求都会穿透到 DB,这就是典型的缓存击穿。
必须在 Redis 层加保护:
- 用
SET key value EX 60 NX(即StringSet(key, value, expiry, When.NotExists))抢锁,抢到的去查 DB,没抢到的等待或重试 - 空值也缓存(比如
"user:123:empty"),并设较短 TTL(如 2 分钟),防止恶意查询不存在 ID - 不要用
GetDatabase().KeyExists(key)做预检——它和后续StringGet之间存在竞态窗口
最易忽略的一点:Redis 锁必须带唯一 client ID 和自动续期(用 Lua 脚本保证原子性),否则容易死锁。











