ASP.NET Core 玩转 Redis 缓存:从接入到分布式锁实战指南
2026/9/17 8:44:46 网站建设 项目流程

我做了这么多年 .NET 后端,说实话,真正把 ASP.NET Core 的缓存玩明白,是从我把 Redis 从"能用"折腾到"好用"那个阶段开始的。很多朋友一提到 Redis 就说"我知道,就是个缓存嘛",但真到了线上,数据不一致、缓存穿透打垮数据库、序列化乱码、连接池耗尽……这些坑我基本都踩过一遍。这篇就来系统聊聊在 ASP.NET Core 里使用 Redis 缓存这件事,从基础接入到缓存策略设计,再到分布式锁、一致性方案和问题排查,把我实际项目里的经验和教训一次性说清楚。

1. 整体设计与思路拆解

1.1 缓存的本质与 Redis 的定位

先想清楚一个问题:缓存到底解决什么?我见过太多人把缓存当成"数据库的遮羞布",哪里慢就往前面套一层,结果该慢的还是慢,还多出一堆数据不一致的烂摊子。缓存的核心价值只有两个:一是扛高并发读,把热点数据从数据库的磁盘 I/O 里解放出来;二是降低响应延迟,让数据在内存附近就能被拿到。

Redis 为什么能在缓存方案里成为事实标准?因为它是内存数据库,单线程模型配合 IO 多路复用,读写性能能到十万级 QPS。而且它支持 String、Hash、List、Set、Sorted Set 等丰富的数据类型,这意味着它不只是"键值对",还能做限流、排行榜、分布式锁、消息队列等更多事情。在 ASP.NET Core 的开发场景里,Redis 可以作为进程内 IMemoryCache 之外的分布式缓存方案,解决多实例部署时缓存不一致的问题。

我在设计缓存方案时,通常会先问三个问题:第一,这个数据是不是热点数据,被读取的频率够不够高;第二,这个数据能不能容忍短时间的不一致;第三,如果缓存挂了,数据库能不能扛住。这三个问题的答案决定了缓存的可选范围。不满足条件的场景,硬上缓存反而会变成新的瓶颈。

1.2 在 ASP.NET Core 中接入 Redis 的主流方式

目前 .NET 生态里接入 Redis 的方式,主要分成三层,每一层的封装程度和灵活性都不一样。

第一层是直接用官方推荐的 StackExchange.Redis 客户端库,直接用 ConnectionMultiplexer 连接 Redis 服务器,自己管理序列化、键名规则、过期策略。这是最底层、最灵活的方式,适合需要精细控制缓存行为的场景,比如做分布式锁、实现自定义缓存客户端。

第二层是使用 ASP.NET Core 内置的IDistributedCache抽象接口,配合AddStackExchangeRedisCache扩展方法,把 Redis 当作分布式缓存的实现。这种方式的好处是抽象度高,代码不依赖 Redis 特有的 API,将来就算换缓存组件,业务代码改动也很小。缺点是它只提供了最基本的GetSetRemove等方法,复杂的数据类型和操作需要自己补充。

第三层是把缓存封装成通用的服务,比如自定义ICacheService,内部组合 StackExchange.Redis 或者 IDistributedCache,外部暴露带泛型的异步方法。我在团队里通常推荐这种方案,因为可以在封装层统一处理序列化、缓存穿透保护、过期时间随机化、监控日志等横切关注点,业务代码只需要调用_cache.GetOrSetAsync<T>("key", factory)这样一行代码就够了。

1.3 技术选型背后的考量

在 ASP.NET Core 项目里,很多团队初期会直接用内存缓存 IMemoryCache,因为配置简单、性能极快。但一旦部署到多台服务器,每台机器的缓存各自为政,就会出现同一份数据在 A 机器是旧值、在 B 机器是新值的情况,用户请求打到不同实例上看到的结果不一样。Redis 作为集中式缓存可以天然解决这个问题。

另一个容易踩坑的点是缓存粒度设计。Redis 的 key 数量不是无限的,内存也不是白给的。我把缓存分为三类:第一类是静态或者低频变化的数据,比如配置信息、字典表,这类数据缓存时间长、命中率高;第二类是强一致要求稍低的热点数据,比如商品详情、用户基础信息,这类数据要设置合理的过期时间,同时要处理缓存击穿的风险;第三类是强一致要求极高的数据,比如订单金额、库存数量,这类数据不要轻易放缓存,顶多做本地短期缓存。

2. 核心细节解析与实操要点

2.1 连接字符串与 ConnectionMultiplexer 的正确姿势

StackExchange.Redis 里面最核心的类就是ConnectionMultiplexer,很多人第一次用的时候都会犯一个错误:每次操作都 new 一个连接,结果 Redis 服务器连接数飙升,性能反而下降。ConnectionMultiplexer的设计意图就是全局复用,官方文档明确说了要把它设计成单例对象。

在 ASP.NET Core 里正确的做法是注册为单例服务:

builder.Services.AddSingleton<IConnectionMultiplexer>(sp => { var configuration = builder.Configuration.GetConnectionString("Redis"); return ConnectionMultiplexer.Connect(configuration); });

连接字符串推荐用配置项来管理,例如:

Redis: "localhost:6379,password=yourpassword,defaultDatabase=0,connectTimeout=5000,syncTimeout=5000,abortConnect=false"

这里有几个参数值得仔细说明。abortConnect=false很关键,它表示即使启动时 Redis 连不上,也不会立刻抛异常,而是让连接在后台重试。很多线上事故就是配置中心先启动、Redis 后启动,结果应用直接挂了。connectTimeoutsyncTimeout要合理设置,别太大也别太小,我一般设置在 3 到 5 秒。defaultDatabase用来区分不同的业务库,建议按业务模块分库,避免一个 Redis 实例里 key 太乱。

还有一个容易忽略的点:StackExchange.Redis 默认是支持多路复用的,同一个连接可以并发执行命令,所以完全不需要为每个请求创建新连接。但也正因为这样,如果某个操作卡住了,可能会阻塞同一条连接上的其他命令。建议在某些慢操作上使用单独的ConnectionMultiplexer实例,或者在代码里设置合理的同步超时时间。

2.2 序列化方案的选择

Redis 本身只能存储字符串和字节数组,所以对象必须序列化后才能写入。这个环节的坑实在太多了。

最常见的坑是使用默认的 JSON 序列化,结果中文字符被转成了\uXXXX编码,日志里看还好,但不同语言客户端之间互相操作时容易出问题。后来我统一使用 System.Text.Json 并设置Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping解决这个问题。

另一个坑是类型信息丢失。直接序列化一个Product对象,反序列化时还要手动指定类型;如果存的是一个多态对象或者接口类型,JSON 反序列化会直接失败。方案有两个:一是在序列化时附带类型信息,比如使用JsonSerializerOptionsTypeInfoResolver;二是在自定义缓存服务封装层,读写都使用泛型方法,确保类型是在编译期就确定好的。

如果对性能要求很高,可以考虑使用 MessagePack 或者 protobuf 这类二进制序列化方案。我实测下来,MessagePack 比 JSON 能快 3 到 5 倍,存储空间也小很多。但对大部分业务系统来说,JSON 序列化就够了,不必过度优化。真正值得优化的是减少序列化次数,也就是把大的缓存对象拆成更小的维度,避免为了更新一个字段而把整个大对象读出来再写进去。

我以前维护过一个商品中心服务,商品详情被设计成一个大 JSON 包,一次缓存写入大约 200KB。这个设计在初期还好,到了大促流量进来就发现两个问题:一是网络传输时间变长,二是单个 key 的更新时间变长。后来我把商品详情按维度拆分,基础信息、价格、库存、活动信息分别存到不同的 key 里,热点数据的命中率提升了,大对象的序列化开销也降下来了。

2.3 键名设计与命名空间规划

Redis 是扁平化的键值结构,没有表、库的概念(除了数据库编号),所以键名的规划直接决定了后续的可维护性。我见过最惨的代码是直接用实体名加主键做 key,比如user_123,等业务多了以后,Redis 里变成了一锅粥,连删除都不知道删哪些。

我习惯用命名空间加冒号分隔的方式,比如user:info:123product:detail:456。这样有几个好处:一是可读性强,从 key 就能看出业务归属;二是可以用 SCAN 命令按模式批量处理;三是后续要做多租户隔离时,前缀天然可以作为分片依据。

键名设计还有一个容易被忽略的点:键名长度会影响内存占用。Redis 的内存瓶颈通常不在于数据量,而在于键的开销。一个几百字节的键名在几百万 key 的情况下会占用不少内存。所以要控制键名长度,不建议用超长的拼接字符串作为键。

2.4 过期时间与内存淘汰策略

Redis 的过期时间设置是有讲究的。太短,缓存命中率低,数据库压力大;太长,数据更新不及时,可能给用户展示过期信息。

我常用的策略是:基础数据类缓存 30 到 60 分钟;热点数据且能容忍 5 分钟延迟的,设置 10 到 15 分钟;强一致要求高的数据,尽量不缓存或者只做 30 秒以内的短期缓存。这是"绝对过期时间"的用法。

但在实际项目里,我更常用的是滑动过期,也叫滑动窗口过期。比如用户登录会话,希望用户一直在操作就一直有效,超过 30 分钟不操作就自动失效。StackExchange.Redis 提供的KeyExpire配合SlidingExpiration可以实现类似效果。不过要注意,每次读取缓存时刷新过期时间也会带来额外的写操作,如果缓存命中率极高,这个开销可能是不可忽略的。

内存淘汰策略一般不用应用层开发操心,但运维和架构层面要关注 Redis 的maxmemory设置和maxmemory-policy。我建议设置allkeys-lru或者volatile-ttl,并保证 Redis 实例内存不超过物理内存的 70%,预留足够的内存给系统本身和碎片消耗。

3. 实操过程与核心环节实现

3.1 使用 IDistributedCache 实现基础缓存读写

IDistributedCache 是 ASP.NET Core 内置的分布式缓存抽象,接 Redis 非常方便。首先安装包:

dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis

然后在 Program.cs 里注册服务:

builder.Services.AddStackExchangeRedisCache(options => { options.Configuration = builder.Configuration.GetConnectionString("Redis"); options.InstanceName = "SampleApp:"; });

InstanceName是个特别好的功能,它会给所有 key 自动加前缀。比如设置了SampleApp:,那么调用SetAsync("user:123", ...)时,实际的 Redis key 是SampleApp:user:123。这样部署多个应用共用同一个 Redis 实例时就不会冲突,清理某个应用的缓存也特别方便,直接按前缀删除。

使用方式也直观:

public class UserService { private readonly IDistributedCache _cache; private readonly IUserRepository _repository; public UserService(IDistributedCache cache, IUserRepository repository) { _cache = cache; _repository = repository; } public async Task<User> GetUserAsync(int userId) { string key = $"user:info:{userId}"; var cached = await _cache.GetStringAsync(key); if (!string.IsNullOrEmpty(cached)) { return JsonSerializer.Deserialize<User>(cached); } var user = await _repository.GetByIdAsync(userId); if (user != null) { string json = JsonSerializer.Serialize(user); await _cache.SetStringAsync(key, json, new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) }); } return user; } }

这里会有人问:为什么不直接用GetAsync拿到字节数组自己反序列化?因为GetStringAsyncSetStringAsync内部已经帮我们处理了 UTF-8 编码的转换,代码更简洁。如果数据是 byte[],再用GetAsync。不过序列化仍然是我们自己控制的,这点要明确。

3.2 封装通用缓存服务:GetOrSet 模式的实现

实际项目里,直接在每个业务代码里手写"先查缓存、没有再查库、再写缓存"这一套逻辑,代码会非常冗长。我一般会封装一个通用的ICacheService,对外暴露GetOrSetAsync<T>方法,把缓存操作的细节收敛到一个地方。

这个模式的实现大致如下:

public class CacheService : ICacheService { private readonly IDistributedCache _cache; private readonly ILogger<CacheService> _logger; public async Task<T> GetOrSetAsync<T>(string key, Func<Task<T>> factory, TimeSpan? expiry = null, CancellationToken cancellationToken = default) { var cached = await _cache.GetAsync(key, cancellationToken); if (cached != null) { return _deserializer.Deserialize<T>(cached); } var value = await factory(); if (value == null) { return default; } var options = new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = expiry ?? TimeSpan.FromMinutes(10) }; await _cache.SetAsync(key, _serializer.Serialize(value), options, cancellationToken); return value; } }

这个模式把"缓存未命中时加载数据"的逻辑以委托的形式传进来,业务代码只需要关心如何从数据库获取数据,不用关心缓存细节。后面如果要加入缓存穿透保护、缓存击穿锁、异常降级,只需要在这个封装层修改,所有业务调用方自动生效,维护成本低很多。

我还会在封装层再加一个开关:当 Redis 出现异常时,直接降级走数据库,不让缓存故障拖垮正常业务。分布式缓存最大的困境是 "缓存可用性 = 系统可用性" 的隐含假设,一旦 Redis 不可用,所有经过缓存的接口就都不可用了。为了打破这个假设,在封装层捕获连接异常并降级是非常有必要的。

3.3 缓存穿透、击穿、雪崩的应对方案

这三个问题是缓存面试必问的话题,也是线上事故的重灾区。

缓存穿透是说查询一个根本不存在的数据,缓存里没有,数据库里也没有,每次请求都打到数据库。比如恶意攻击者伪造不存在的用户 ID,如果接口没有做参数校验,大量请求会直接穿透缓存打挂数据库。解决方案有三种:一是参数校验,明显不合法的请求直接拒绝;二是空值缓存,把查不到的数据也缓存起来,设置较短的过期时间(比如 3 到 5 分钟),这样同一个"不存在的数据"也不会反复穿透;三是布隆过滤器,在缓存前面加一层过滤器,快速判断某个 key 是否存在。我推荐先做参数校验和空值缓存,布隆过滤器适合 key 数量特别大、内存不足的场景。

缓存击穿是说某个热点 key 过期的一瞬间,大量并发请求同时发现缓存没命中,同时去数据库查询。这相当于把缓存过期时间变成了数据库的集中受攻击时间点。应对方案有互斥锁和逻辑过期。互斥锁的意思是:发现缓存没命中时,先尝试获取一个分布式锁,只有拿到锁的线程才能去查数据库,其他线程等待锁释放后重新查缓存。逻辑过期则是把"物理过期时间"写进缓存的数据里,程序在读取时判断是否逻辑过期,如果过期就异步去刷新缓存,这种方式可以做到非常平滑,但实现复杂度略高。

缓存雪崩是指大面积 key 在同一时间段集中过期,或者 Redis 实例宕机,导致大量请求直接打到数据库。应对方案也很明确:一是过期时间加随机因子,让过期时间尽量分散,比如在 5 分钟基础上加 0 到 60 秒的随机值;二是多级缓存,本地缓存 + Redis 缓存组合,即使 Redis 挂了,本地缓存还能撑一阵;三是 Redis 集群高可用,用主从复制加哨兵或者 Cluster 集群,避免单点故障;四是服务降级和熔断,在数据库压力过大时直接返回降级数据。

这里我分享一个实际的生产配置模板,是我在项目里一直沿用的:

public static class CacheKeyPolicy { public static TimeSpan GetRandomizedExpiry(TimeSpan baseExpiry) { var random = Random.Shared.Next(30, 90); return baseExpiry.Add(TimeSpan.FromSeconds(random)); } }

然后在写入缓存时统一使用GetRandomizedExpiry(TimeSpan.FromMinutes(30)),从源头降低集中过期的概率。

3.4 分布式锁:用 Redis 实现互斥

分布式锁是 Redis 最常见的进阶用法。最常见的场景是:多个实例同时处理某个定时任务,只有一台能执行;或者缓存击穿时,只有一个线程能去查数据库。

用 StackExchange.Redis 实现分布式锁,核心就是 SET NX EX 命令。官方库提供了现成的方法LockTakeLockRelease,可以这样用:

public class RedisLock : IDistributedLock { private readonly IConnectionMultiplexer _connection; private static readonly Random Random = new Random(); public async Task<bool> TryLockAsync(string resource, string token, TimeSpan expiry) { var db = _connection.GetDatabase(); return await db.LockTakeAsync(resource, token, expiry); } public async Task<bool> ReleaseLockAsync(string resource, string token) { var db = _connection.GetDatabase(); return await db.LockReleaseAsync(resource, token); } }

这里有个很关键的细节:token必须是随机的、每次调用唯一的标识。为什么?因为锁的释放必须由持有者自己执行,不能一个线程把另一个线程的锁给释放了。我用的 token 是Guid.NewGuid().ToString(),释放时将 token 一并传过去,LockRelease内部会校验 token 匹配才删除 key。

使用姿势如下:

var token = Guid.NewGuid().ToString(); var isLocked = await _lock.TryLockAsync("job:fetchData", token, TimeSpan.FromSeconds(10)); if (!isLocked) { return; // 另一个实例正在执行 } try { await FetchDataFromDatabaseAsync(); } finally { await _lock.ReleaseLockAsync("job:fetchData", token); }

细节上有几个要注意的点:锁的过期时间要根据业务执行时间合理估算,太短会导致业务没执行完锁就自动释放了,其他实例已经开始执行;太长会导致持有锁的实例崩溃后,锁迟迟不释放。我一般设置为预计执行时间的 3 倍,例如预计执行 2 秒,过期时间设 10 秒。另外一定要使用finally释放锁,防止异常导致锁不释放。

还有一个进阶用法:在缓存击穿场景里,互斥锁要配合"二次检查缓存"的逻辑。也就是拿到锁之后,先不要立刻去数据库,而是再查一次缓存,因为在你等待锁的期间,第一个拿到锁的线程可能已经把缓存填好了。

public async Task<T> GetOrSetWithLockAsync<T>(string key, Func<Task<T>> factory, TimeSpan expiry) { var cached = await _cache.GetAsync(key); if (cached != null) { return _deserializer.Deserialize<T>(cached); } var token = Guid.NewGuid().ToString(); bool lockAcquired = await _lock.TryLockAsync($"lock:{key}", token, TimeSpan.FromSeconds(5)); if (!lockAcquired) { await Task.Delay(50); return await GetOrSetWithLockAsync(key, factory, expiry); // 递归等待,生产环境建议用循环 } try { cached = await _cache.GetAsync(key); if (cached != null) { return _deserializer.Deserialize<T>(cached); } var data = await factory(); await _cache.SetAsync(key, _serializer.Serialize(data), new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = expiry }); return data; } finally { await _lock.ReleaseLockAsync($"lock:{key}", token); } }

这段代码我已经在多个项目里验证过效果,能有效防止缓存击穿引起的数据库压力飙升。需要留意的是,条件允许的话尽量用循环而不是递归,避免递归层级过深导致栈溢出。

4. 缓存与数据库的一致性方案

4.1 Cache Aside Pattern 的坑

讲到缓存一致性,就必须讲 Cache Aside Pattern,也就是读缓存、未命中读数据库、回写缓存,写数据时更新数据库并删除缓存。这个模式简单、易用,网上到处都是。但真正严格的工程实践里,它有几个隐患。

把删除缓存放在"更新数据库成功之后",看起来正确,但如果删除缓存失败,就会出现缓存里是旧值、数据库是新值的情况。我遇到过一次事故:更新用户昵称后,Redis 里旧昵称缓存一直没删掉,用户自助修改后页面显示的还是旧昵称,过了通行时间才恢复。原因就是删除操作超时了,代码里也没有失败重试。

解决这个问题有几种方案。最简单的是:把删除失败的消息丢到消息队列,由消费者异步删除。复杂一点的是:引入监听数据库变更的技术,比如用 Canal 监听 MySQL 的 binlog,当数据变更时自动同步失效缓存。在 .NET 领域,如果你不想引入额外的中间件,我强烈建议在封装层实现"更新数据库 + 删除缓存"的本地事务性操作,至少做到删除失败时记录日志并及时重试。

4.2 延迟双删的取舍

延迟双删是个民间说法,核心逻辑是:更新数据库后,先删除一级缓存,休眠一小段时间,再删除二级缓存。为什么要这样?因为在并发场景下,可能出现两个请求交错的局面:请求 A 读缓存发现过期,去数据库查旧数据;请求 B 更新数据库成新数据,删除缓存;请求 A 拿到旧数据回写缓存。此时缓存里就是旧数据了。

延迟双删通过第二次删除来"兜底",把那个写入旧数据的缓存清理掉。但这个方法看起来聪明,实际操作中也有不少麻烦。第一,休眠时间不好确定,网络延迟、线程调度、数据库复制延迟都会影响最佳等待时间的判断;第二,如果第二次删除也失败,问题还是存在;第三,这个方案在读写并发非常高的场景下,仍然不能保证绝对一致。

我在实践中会做取舍:对于强一致性要求不高但对用户感知影响明显的场景,比如商品昵称、用户头像,用延迟双删并在删除失败时记录日志监控;对于强一致性要求极高的场景,比如支付结果、订单状态,不使用缓存或者只用极短过期时间的缓存,从架构上规避一致性问题。有一句话说得很好:缓存一致性问题的根本解决方式,是少用缓存。

4.3 订阅发布与逻辑过期方案

如果缓存的一致性要求较高,但你又不想引入外部消息队列,Redis 自带的发布订阅功能是一个轻量级的替代方案。

思路是这样的:当数据发生变化时,除了更新数据库,还会调用 Redis 的Publish发布一条消息,消息内容是要失效的缓存 key。应用里每个实例都Subscribe了同一个频道,收到消息后尝试删除对应的本地缓存和分布式缓存。这样比延迟双删可靠得多,因为发布消息是实时到达的,而且每个实例都会执行删除操作。

但要注意,Redis 发布订阅是"即发即弃"的,如果某个实例恰好断线重连,消息就会丢失。所以这个方法适合作为提高一致性成功率的辅助手段,不能完全依赖它。对于可以容忍最终一致性的系统,组合"删除缓存 + 消息队列重试"方案会稳妥得多。

逻辑过期方案是另一种思路:缓存永远不过期,但值里带着一个"过期时间戳",比如{"data":..., "expireAt": 1735689600}。读取时发现当前时间超过了expireAt,就返回旧数据,同时在后台异步刷新缓存。这样做的好处是极限情况下用户体验不会突然变差,数据库的压力也是平滑的,不会在某个时间点集中爆发。代价是实现复杂度上升,而且会有短时间的旧数据暴露,适合可以接受微秒级不一致的场景。

5. 常见问题与排查技巧实录

5.1 Redis 连接问题排查

在实际部署中,最常见的三大类问题分别是:连接被拒绝、连接数超限、命令阻塞。

连接被拒绝的常规原因是防火墙没有放通 Redis 端口,或者是 Redis 配置了bind 127.0.0.1,只能本机访问。此时排查顺序是先telnet测试端口通不通,再看 Redis 的redis.confbindprotected-mode的配置。生产环境建议绑定内网 IP 并设置强密码,不要暴露到公网。

连接数超限是个隐蔽问题,Redis 默认的最大连接数是 10000(通过maxclients配置)。当ConnectionMultiplexer被误用、每次请求都创建新实例时,连接数很快就满了。还有一种情况是连接池设置不当,或者某个慢操作导致连接被长期占用。排查时用redis-cli INFO clients查看当前连接的客户端数量,再用CLIENT LIST分析每个连接是从哪里来的。解决思路就是先把连接实例改成全局单例,再看是否有异常代码泄漏了连接。

命令阻塞是更难排查的一种情况。Redis 是单线程模型,某些慢命令(比如KEYS *、大 key 的删除、超大集合的查询)会阻塞整个实例的处理。我在一次生产事故里发现,运维同学在排查问题时直接执行了KEYS *,结果线上 Redis 瞬间阻塞了十几秒,所有接口的超时率飙升。所以查 key 一定要用SCAN命令,删除大 key 要拆分成小批量删除。

5.2 可视化管理工具怎么选

之前做技术分享时,经常有人问 Redis 用什么客户端工具。我的观点是:redis-cli 是底线,可视化工具是效率手段。

我日常使用的工具是 Another Redis Desktop Manager,开源、跨平台、支持 Windows、macOS 和 Linux,操作体验流畅,可以浏览 key、执行命令、查看内存分析。另一个选项是 Redis Insight,官方出品的可视化工具,界面更好看,带有内存分析和慢日志查看功能,更专业。如果只是需要简单的测试,也可以直接在线使用一些 Web 版的 Redis 客户端。

使用可视化工具时,建议开启 SSH 隧道或者配置好访问控制,不要把带密码的管理端暴露在公网,防止 Redis 被爆破或恶意攻击。

5.3 redis-cli 常用监控命令清单

下面这些命令是我在上线排查时最常用到的,整理成了一张速查表:

命令用途使用场景
redis-cli PING测试连通性确认 Redis 是否在线
redis-cli INFO查看各项运行指标关注 connected_clients、used_memory、total_commands_processed
redis-cli DBSIZE查看当前库的 key 数量判断缓存规模
redis-cli SCAN 0 MATCH user:* COUNT 100遍历 key批量查找某个前缀的 key,替代 KEYS
redis-cli TTL key查看 key 剩余过期时间排查缓存是否即将过期
redis-cli SLOWLOG GET 10查看慢命令日志排查命令阻塞
redis-cli MONITOR实时打印所有命令开发环境定位问题,生产慎用
redis-cli --stat实时输出统计信息粗粒度观察 QPS 和内存变化

生产环境要特别慎重使用MONITOR命令,因为它会输出所有命令,瞬间产生大量日志,本身就会影响 Redis 性能。

5.4 开发环境用 Docker 部署 Redis

在本地开发和测试环节,我通常直接用 Docker 启动一个 Redis,比在 Windows 上手工安装要方便得多,也能避免系统兼容性问题。

使用 Docker Compose 是标准做法,一个简单的配置:

services: redis: image: redis:7-alpine container_name: local-redis ports: - "6379:6379" command: redis-server --appendonly yes --requirepass localpass volumes: - redis-data:/data volumes: redis-data:

--appendonly yes开启 AOF 持久化,保证重启之后数据不丢。--requirepass localpass设置访问密码,虽然不是生产环境,但养成带密码的习惯是好事。启动后,可以用docker exec -it local-redis redis-cli -a localpass PING验证连接。

如果要模拟生产环境的主从架构,可以在 Compose 里定义多个 Redis 服务,通过主从复制参数关联起来。这个在测试分布式锁、缓存读写分离时特别有用。

5.5 缓存性能优化的经验总结

最后聊几个我在性能调优时总结出来的经验。

第一个经验是合并请求。如果某个页面需要同时展示用户信息、订单汇总、消息未读数,不要在业务代码里三次访问 Redis,尽量用 Pipeline 或者 Lua 脚本一次拿到。StackExchange.Redis 提供了CreateBatch或者直接用 pipeline 的能力,多次 Redis 操作可以一次往返完成,能显著降低网络开销。我把一个首页接口从 11 次 Redis 往返减少到 2 次,接口耗时从 80 毫秒降到了 25 毫秒。

第二个经验是控制 key 的粒度。热点数据按照业务维度拆分比大对象缓存更划算,读多个维度时用批量获取即可。但粒度太细也有问题,会增加 key 的数量和缓存维护的成本,所以要在"缓存体积"和"缓存数量"之间找到一个平衡点。

第三个经验是监控警报不可少。没有监控的缓存系统就像没有仪表盘的飞机,我只能通过 Redis 自身的INFO指标配合应用层埋点来观察缓存命中、缓存耗时、序列化耗时、Redis 异常数。当缓存命中率持续低于 60% 时,就说明缓存设计可能有问题,需要尽早排查。

我在实际维护中发现,缓存这个问题,看起来入门门槛很低,但真正做细之后,每一步都藏着深刻的设计选择。有些坑,没有经历过真的很难体会。希望这篇文章能帮你在 ASP.NET Core 与 Redis 这条路上少走一些弯路,把这些经验直接用到你的项目里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询