ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ASP.NET Core 玩转 Redis 缓存:从接入到分布式锁实战指南

ASP.NET Core 玩转 Redis 缓存:从接入到分布式锁实战指南 我做了这么多年 .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将来就算换缓存组件业务代码改动也很小。缺点是它只提供了最基本的Get、Set、Remove等方法复杂的数据类型和操作需要自己补充。第三层是把缓存封装成通用的服务比如自定义ICacheService内部组合 StackExchange.Redis 或者 IDistributedCache外部暴露带泛型的异步方法。我在团队里通常推荐这种方案因为可以在封装层统一处理序列化、缓存穿透保护、过期时间随机化、监控日志等横切关注点业务代码只需要调用_cache.GetOrSetAsyncT(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.AddSingletonIConnectionMultiplexer(sp { var configuration builder.Configuration.GetConnectionString(Redis); return ConnectionMultiplexer.Connect(configuration); });连接字符串推荐用配置项来管理例如Redis: localhost:6379,passwordyourpassword,defaultDatabase0,connectTimeout5000,syncTimeout5000,abortConnectfalse这里有几个参数值得仔细说明。abortConnectfalse很关键它表示即使启动时 Redis 连不上也不会立刻抛异常而是让连接在后台重试。很多线上事故就是配置中心先启动、Redis 后启动结果应用直接挂了。connectTimeout和syncTimeout要合理设置别太大也别太小我一般设置在 3 到 5 秒。defaultDatabase用来区分不同的业务库建议按业务模块分库避免一个 Redis 实例里 key 太乱。还有一个容易忽略的点StackExchange.Redis 默认是支持多路复用的同一个连接可以并发执行命令所以完全不需要为每个请求创建新连接。但也正因为这样如果某个操作卡住了可能会阻塞同一条连接上的其他命令。建议在某些慢操作上使用单独的ConnectionMultiplexer实例或者在代码里设置合理的同步超时时间。2.2 序列化方案的选择Redis 本身只能存储字符串和字节数组所以对象必须序列化后才能写入。这个环节的坑实在太多了。最常见的坑是使用默认的 JSON 序列化结果中文字符被转成了\uXXXX编码日志里看还好但不同语言客户端之间互相操作时容易出问题。后来我统一使用 System.Text.Json 并设置Encoder JavaScriptEncoder.UnsafeRelaxedJsonEscaping解决这个问题。另一个坑是类型信息丢失。直接序列化一个Product对象反序列化时还要手动指定类型如果存的是一个多态对象或者接口类型JSON 反序列化会直接失败。方案有两个一是在序列化时附带类型信息比如使用JsonSerializerOptions的TypeInfoResolver二是在自定义缓存服务封装层读写都使用泛型方法确保类型是在编译期就确定好的。如果对性能要求很高可以考虑使用 MessagePack 或者 protobuf 这类二进制序列化方案。我实测下来MessagePack 比 JSON 能快 3 到 5 倍存储空间也小很多。但对大部分业务系统来说JSON 序列化就够了不必过度优化。真正值得优化的是减少序列化次数也就是把大的缓存对象拆成更小的维度避免为了更新一个字段而把整个大对象读出来再写进去。我以前维护过一个商品中心服务商品详情被设计成一个大 JSON 包一次缓存写入大约 200KB。这个设计在初期还好到了大促流量进来就发现两个问题一是网络传输时间变长二是单个 key 的更新时间变长。后来我把商品详情按维度拆分基础信息、价格、库存、活动信息分别存到不同的 key 里热点数据的命中率提升了大对象的序列化开销也降下来了。2.3 键名设计与命名空间规划Redis 是扁平化的键值结构没有表、库的概念除了数据库编号所以键名的规划直接决定了后续的可维护性。我见过最惨的代码是直接用实体名加主键做 key比如user_123等业务多了以后Redis 里变成了一锅粥连删除都不知道删哪些。我习惯用命名空间加冒号分隔的方式比如user:info:123、product: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 TaskUser GetUserAsync(int userId) { string key $user:info:{userId}; var cached await _cache.GetStringAsync(key); if (!string.IsNullOrEmpty(cached)) { return JsonSerializer.DeserializeUser(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拿到字节数组自己反序列化因为GetStringAsync和SetStringAsync内部已经帮我们处理了 UTF-8 编码的转换代码更简洁。如果数据是 byte[]再用GetAsync。不过序列化仍然是我们自己控制的这点要明确。3.2 封装通用缓存服务GetOrSet 模式的实现实际项目里直接在每个业务代码里手写先查缓存、没有再查库、再写缓存这一套逻辑代码会非常冗长。我一般会封装一个通用的ICacheService对外暴露GetOrSetAsyncT方法把缓存操作的细节收敛到一个地方。这个模式的实现大致如下public class CacheService : ICacheService { private readonly IDistributedCache _cache; private readonly ILoggerCacheService _logger; public async TaskT GetOrSetAsyncT(string key, FuncTaskT factory, TimeSpan? expiry null, CancellationToken cancellationToken default) { var cached await _cache.GetAsync(key, cancellationToken); if (cached ! null) { return _deserializer.DeserializeT(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 命令。官方库提供了现成的方法LockTake和LockRelease可以这样用public class RedisLock : IDistributedLock { private readonly IConnectionMultiplexer _connection; private static readonly Random Random new Random(); public async Taskbool TryLockAsync(string resource, string token, TimeSpan expiry) { var db _connection.GetDatabase(); return await db.LockTakeAsync(resource, token, expiry); } public async Taskbool 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 TaskT GetOrSetWithLockAsyncT(string key, FuncTaskT factory, TimeSpan expiry) { var cached await _cache.GetAsync(key); if (cached ! null) { return _deserializer.DeserializeT(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.DeserializeT(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.conf里bind和protected-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_processedredis-cli DBSIZE查看当前库的 key 数量判断缓存规模redis-cli SCAN 0 MATCH user:* COUNT 100遍历 key批量查找某个前缀的 key替代 KEYSredis-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 这条路上少走一些弯路把这些经验直接用到你的项目里。
返回列表