Redis 这个词很多人第一反应是缓存第二反应是面试八股。我第一次在实验环境里敲SET、GET的时候也觉得这东西简单得不像话——不就是个远程字典吗直到后来真拿它去给一个后台服务做支撑才发现问题根本不在命令会不会写而在于你把它放在了架构的哪个位置、给它配了什么参数、以及出问题的时候你有没有能力把原因定位出来。这篇内容想聊的就是这条线从在实验平台上把 Redis 跑通到用它真正构建出一个能扛事的支撑程序。不管你是刚接触 Redis、正在做课程实验还是已经写过几行业务代码但心里没底下面的内容应该都能拿到点东西——数据结构的选型逻辑、四类常见支撑程序的落地写法、实验环境里最容易卡住的几个坑以及让服务不半夜出事的工程细节。1. 先想清楚Redis在支撑程序里到底站哪个位置1.1 支撑程序为什么偏爱内存数据库先说个容易被忽略的前提Redis 快不只是因为数据在内存里。真正的差异在于它的访问模型。传统数据库一次查询要经过 SQL 解析、执行计划生成、磁盘页读取、事务日志落盘这一长串流程即使有缓存池一次访问的耗时也在零点几毫秒到几毫秒量级。而 Redis 单次命令的处理是在单线程事件循环里完成的命令进队列、查字典、写回、返回中间没有磁盘 IO、没有锁竞争、没有上下文切换官方压测数据是单实例十万级 QPS 起步。这个量级差意味着什么呢我打个比方数据库像一个有前台登记、有档案室、有审批流程的办事大厅你去查个东西要走完整套流程Redis 像一张摊在桌上的通讯录翻到那一页就是答案。所以支撑程序里凡是高频读、低价值密度、可以容忍丢失的数据都应该往 Redis 上放——会话信息、验证码、热点商品详情、页面片段、计数器、排行榜、临时锁。反过来凡是不能丢、要能追溯、要能复杂查询的老老实实留在磁盘数据库里。这里有个新手常犯的方向性错误把 Redis 当成更快的数据库来用把主数据全塞进去然后指望它承担唯一数据源。这在实验里没问题因为实验数据量小、也没人拔电源但在真实场景里内存是昂贵的Redis 的持久化是尽力而为而不是绝对可靠RDB 默认每 3600 秒才落一次盘中间宕机就是一段数据凭空消失。所以定位一定要摆正Redis 是支撑层不是主存储层。1.2 实验环境里第一件事确认你连的是哪个实例在实验平台上做 Redis 相关任务十有八九第一步不是写代码而是确认环境。我见过太多人代码逻辑写得完全正确结果卡在连接上一卡就是半小时。原因通常很朴素客户端连的是127.0.0.1:6379但实验容器里的 Redis 可能监听在别的端口或者根本没起来或者起了但开了保护模式拒绝外部连接。标准的自查顺序是这样的先用redis-cli -h host -p port ping确认服务活着返回PONG说明进程正常、网络通、认证没问题如果配了密码这一步会返回NOAUTH Authentication required那就是密码的事。如果这一步就报Connection refused说明进程没启动或者端口不对如果报Connection timed out那多半是网络或绑定地址的问题。这两个报错含义完全不同排查方向也不一样别混着猜。确认通了之后立刻看一眼这个实例的配置尤其是三个参数maxmemory内存上限设太小会直接写不进去、maxmemory-policy淘汰策略默认noeviction意味着内存满了就报错、databases库的数量默认 16 个。这几个值在实验环境里经常被平台预先设成很小的数字你代码里要是习惯了往 db 0 里猛塞数据很容易撞上OOM command not allowed when used memory maxmemory。这个报错我第一次见的时候还以为是代码 bug查了半天才发现是内存上限只有几十兆。提示进实验环境先跑一遍INFO server和CONFIG GET maxmemory把版本、内存、淘汰策略三个值记下来。这三条信息后面几乎所有异常排查都要用到先拿到手能省很多来回。1.3 五个数据结构对应五类支撑能力Redis 的入门门槛低但真正的分水岭在选对结构。同样一个需求用错结构性能可能差一个数量级代码复杂度也天差地别。我按实际用途梳理一下结构底层实现典型支撑场景常见误用StringSDS 动态字符串缓存 JSON、计数器、分布式锁用它存大对象导致大 keyHash压缩列表 / 哈希表对象字段局部更新、购物车字段数忽多忽少触发编码转换List快速列表简单队列、最新 N 条用它做随机访问O(n) 性能Set整数集合 / 哈希表去重、共同好友、抽奖元素数量失控内存暴涨ZSet跳表 哈希表排行榜、延时队列、滑动窗口限流排行榜同分排序踩坑这张表建议直接记在脑子里。后面所有的落地写法本质都是在这五种结构上做组合。比如统计七天活跃用户是 Set 的并集最近一小时访问次数是 ZSet 的范围计数每日签到是 Bitmap 的位操作。结构选对了代码往往只有几行结构选错了你可能要写几十行还不一定对。2. 数据结构选型同一个需求三种写法性能能差一个数量级2.1 String不只是存个字符串大多数人把 String 当成键值对存储来用其实它的能力边界比想象中宽。除了最基础的SET/GET它还有三个非常实用的变体SETNX不存在才设置是分布式锁的基础、SET key value EX seconds NX原子化的设置加过期一条命令顶三条、INCR/DECR原子自增计数器的核心。原子性这个词值得展开说一句。很多人写计数器是这么写的先GET拿到值代码里加一再SET回去。单机单线程测试时完全没问题但一旦有并发两个请求同时读到 100各自加一写回 101一次自增就凭空丢了。INCR之所以可靠是因为读、加、写三个动作在 Redis 内部是一个不可分割的操作中间不会插入别的命令。这就是为什么计数场景必须用INCR而不是读改写——不是性能问题是正确性问题。SET key value EX 60 NX这条组合命令也值得单独记一下。它等价于设置值、设过期、仅当不存在时生效三件事原子完成。写分布式锁、写只初始化一次的配置、写防重复提交的令牌全靠它。早期很多人用SETNX再单独EXPIRE这两条命令之间如果进程挂了就会留下一个永不过期的死锁键线上排查起来极其难受。2.2 Hash与JSON字符串的取舍存一个用户对象有两种主流写法一种是SET user:1001 {name:张三,age:28}把整个对象序列化成 JSON 塞进 String另一种是HSET user:1001 name 张三 age 28用 Hash 分字段存。怎么选关键看更新模式。如果这个对象每次都是整体读、整体写那 JSON 字符串更省事序列化反序列化一次搞定网络传输也只有一个 RTT。但如果业务里有大量只改某一个字段的操作——比如只更新最后登录时间、只改积分——那 Hash 明显更合适HSET user:1001 last_login 1720000000一条命令搞定不需要读出整个对象、改一个字段、再整体写回。后者的风险在于并发覆盖两个请求同时读、各自改不同字段、再写回后写的会把先写的字段冲掉。Hash 有个坑要注意Redis 对小的 Hash 会用压缩列表ziplist/listpack存储非常省内存一旦字段数或单个字段长度超过阈值hash-max-listpack-entries默认 128hash-max-listpack-value默认 64 字节就会转成真正的哈希表内存占用会跳一大截。所以如果你的对象字段特别多或者某个字段存的是长文本这个结构可能并不划算老老实实序列化成 String 反而更省。我一般的经验判断是字段数量稳定在十几个以内、且需要频繁局部更新用 Hash字段多、或者每次都是整体读写用 String 存 JSON。别为了看起来更规范而选 Hash内存是真的会多花。2.3 ZSet做排行榜的分页陷阱排行榜是 ZSet 最经典的场景ZINCRBY加分、ZREVRANGE取前 N、ZREVRANK查排名。写起来很顺手但有两个地方特别容易翻车。第一个是同分排序。ZSet 的排序规则是先按 score 排score 相同按 member 的字典序排。这在排行榜里往往不是你想要的效果——两个用户都是 1000 分谁排前面取决于用户 ID 的字符串大小看起来像是随机的。业界的通用解法是把时间戳编码进 score用score 分数 * 10^10 (某个递减的时间戳)这样同分时先到的人排前面。代价是 score 失去了可读性取出来之后要自己做一次解码。第二个是深分页。ZREVRANGE key 100000 100010这种操作Redis 内部要跳过前十万个元素虽然是 O(log N M) 的复杂度但跳表遍历本身在高并发下依然不便宜。真要做深分页通常的做法是限制用户只能看前 N 页或者用游标方式基于上一页最后一个元素的 score 继续往下取ZREVRANGEBYSCORE key (lastScore -inf LIMIT 0 10。# 加分 ZINCRBY rank:daily 10 user:1001 # 取前 10 名含分数 ZREVRANGE rank:daily 0 9 WITHSCORES # 查某人排名0 开始所以展示时要 1 ZREVRANK rank:daily user:1001 # 基于游标翻页取分数严格小于 950 的接下来 10 个 ZREVRANGEBYSCORE rank:daily (950 -inf WITHSCORES LIMIT 0 10还有一点排行榜要不要设置过期日榜就该在次日凌晨清掉或换 key用rank:daily:20240701这种带日期的 key 名靠 TTL 自动回收更省心别在一个固定 key 上反复DEL再重建那样在清理的瞬间读请求会全部打到空榜上。2.4 Set与Bitmap在去重和签到上的用法Set 的价值在于集合运算。SADD加元素、SISMEMBER判存在、SINTER求交集。最典型的支撑场景是共同关注——SINTER user:1001:follows user:1002:follows一条命令就能算出两个人的共同关注换成关系型数据库要写一堆 JOIN。去重场景也常用 Set比如同一篇文章同一用户只计一次阅读。但要注意 Set 的内存成本它存储的是完整元素一个元素哪怕只有十几个字节加上哈希表的开销实际占用往往要乘以 2 到 3。十万级元素还行百万级就该考虑 HyperLogLog 了——用 12KB 固定空间估算基数标准误差 0.81%。代价是它只能算有多少个不重复的不能判断某个元素在不在里面也不能取出元素。所以 UV 统计用它去重判断还是得 Set。签到、活跃标记这类是/否二值场景Bitmap 是最省的选择。原理很简单把每一天映射成一位用户 1001 号在 7 月 1 日签到就是往sign:1001这个 key 的第 0 位写 1。# 7 月 1 日签到偏移量按日期算 SETBIT sign:1001 0 1 # 7 月 5 日签到 SETBIT sign:1001 4 1 # 统计本月签到次数 BITCOUNT sign:1001 # 判断某天是否签到 GETBIT sign:1001 4一个用户一年的签到数据只占 365 位也就是 46 字节。同样的事情用 Set 存每个日期都是一个完整字符串元素一年下来轻松几百字节甚至更多。这种数据规模大、但每个值只有一个 bit 信息量的场景Bitmap 是降维打击。不过要提醒一句Bitmap 的偏移量是稀疏的如果用户 ID 直接当偏移量比如 ID 是 100 万那SETBIT会在瞬间分配 125KB 的空间这种用法是错误的——偏移量必须是连续的小整数通常是日期序号或者自增 ID 压缩后的值。3. 四类支撑程序的落地写法3.1 缓存旁路模式与三个经典失效场景缓存最主流的用法是 Cache-Aside也就是旁路缓存读的时候先查缓存没有再查数据库查到之后回填缓存写的时候先更新数据库再删除缓存。逻辑就这么简单但里面藏着三个绕不开的问题。第一个是缓存穿透查一个根本不存在的数据缓存里没有每次请求都打到数据库。恶意攻击或者代码 bug 都可能触发。两种解法一是把查不到这个结果也缓存起来值设为空字符串TTL 设短一点比如 60 秒二是用布隆过滤器在入口拦一道——它用很小的空间判断这个 key 一定不存在或可能存在能挡掉绝大多数无效查询。第二个是缓存击穿某个热点 key 刚好过期一瞬间大量请求同时涌向数据库。解法是互斥重建——只让一个请求去查库重建缓存其他请求短暂等待或者直接返回旧值。用 Redis 实现就是SET lock:rebuild:xxx 1 EX 10 NX抢到锁的那个去重建抢不到的睡个几十毫秒重试。第三个是缓存雪崩大批 key 在同一时刻集体过期。解法很朴素——给 TTL 加随机抖动。比如原本设置 3600 秒改成3600 random(0, 600)让过期时间打散到十分钟的区间里压力自然就分散了。这个改动只加了一行代码效果却非常明显。回填缓存时还有个细节先删缓存还是先更新数据库标准答案是先更新库、后删缓存而且删缓存失败要重试可以把删除操作丢进消息队列做补偿。为什么是删除而不是更新因为并发写的时候两个线程的更新顺序和落缓存顺序可能不一致容易写出脏数据删除则是幂等的下次读自然回填最新值。3.2 计数器与滑动窗口限流计数器看着简单坑在上面提过必须用INCR保证原子性。但加上过期时间之后事情就没那么单纯了。import redis r redis.Redis(host127.0.0.1, port6379, db0, decode_responsesTrue, socket_timeout2, socket_connect_timeout2) def incr_with_ttl(key: str, ttl: int 60) - int: 自增并保证首次设置过期时间用 Lua 保证原子 script local v redis.call(INCR, KEYS[1]) if v 1 then redis.call(EXPIRE, KEYS[1], ARGV[1]) end return v return r.eval(script, 1, key, ttl)为什么这里要用 Lua因为INCR和EXPIRE是两条命令如果进程在中间崩了就会留下一个永不过期的计数器越积越大。用 Lua 脚本包起来Redis 会把它当成一个整体执行中间不插入任何其他命令。这是 Redis 里保证多命令原子性的标准手段。固定窗口计数器有个缺陷窗口边界处的流量可能是上限的两倍。比如限制每分钟 100 次用户在 12:00:59 打 100 次12:01:00 再打 100 次两个窗口加起来 200 次但任意连续 60 秒内的实际峰值超标了。要更精确就得用滑动窗口用 ZSet 存每次请求的时间戳每次进来先ZREMRANGEBYSCORE清掉窗口外的再ZCARD数一下数量没超就ZADD进去。代价是内存占用随请求量线性增长所以一般只在敏感接口上用。-- 滑动窗口限流KEYS[1] 为窗口 keyARGV[1] 为窗口毫秒数ARGV[2] 为上限 local now redis.call(TIME) local ts tonumber(now[1]) * 1000 math.floor(tonumber(now[2]) / 1000) local window tonumber(ARGV[1]) redis.call(ZREMRANGEBYSCORE, KEYS[1], 0, ts - window) local count redis.call(ZCARD, KEYS[1]) if count tonumber(ARGV[2]) then redis.call(ZADD, KEYS[1], ts, ts .. - .. math.random(100000)) redis.call(PEXPIRE, KEYS[1], window) return 1 end return 0注意ZADD的 member 我用时间戳拼了个随机数因为 ZSet 的 member 必须唯一同一毫秒内的多次请求如果 member 相同会被覆盖导致计数偏少。3.3 分布式锁的正确姿势SET NX EX 唯一值校验分布式锁是 Redis 用得最多、也最容易写错的场景。错误写法五花八门正确的其实只有一套流程import uuid def acquire_lock(conn, lock_key: str, ttl_ms: int 30000) - str | None: token str(uuid.uuid4()) ok conn.set(lock_key, token, nxTrue, pxttl_ms) return token if ok else None def release_lock(conn, lock_key: str, token: str) - bool: script if redis.call(GET, KEYS[1]) ARGV[1] then return redis.call(DEL, KEYS[1]) else return 0 end return conn.eval(script, 1, lock_key, token) 1三个要点缺一不可。第一加锁必须是SET key value NX PX ttl一条命令。分两步写先SETNX再EXPIRE会有窗口期进程挂了就死锁。而且过期时间必须给不给就是给自己埋雷。第二value 必须是唯一标识。很多人 value 随便写个1。问题出在释放锁的时候如果线程 A 的业务执行超时了锁自动过期线程 B 拿到了锁这时候 A 执行完去删锁删掉的其实是 B 的锁。用唯一 token 加上比对再删的 Lua 脚本就能避免误删。第三锁的 TTL 要能覆盖业务耗时或者做续期。TTL 设短了业务没跑完锁就没了等于没锁设长了万一持有者挂了别人要干等。工程上的做法是另起一个看门狗线程每隔 TTL/3 去检查一次锁还在不在自己手里在就延长。不过这会让实现复杂度上一个大台阶简单业务场景下把 TTL 设成业务最大耗时的 3 倍左右通常够用了。还有个场景值得提如果只是防重复提交、防重复初始化不需要严格的互斥语义那就别上分布式锁用SET NX EX做个一次性令牌就行简单得多。锁是重资产别滥用。3.4 List和Stream做轻量队列的边界用 List 做队列是最省事的生产端LPUSH消费端BRPOPBRPOP是阻塞版本队列空的时候会挂起等待不占 CPU。# 生产 LPUSH queue:tasks {id:1,type:resize} # 消费阻塞 5 秒取不到返回 nil BRPOP queue:tasks 5但 List 有个致命短板消息出队即消失。消费者BRPOP拿到消息之后如果进程崩了这条消息就永久丢了没有任何重试机制。要解决这个得用BRPOPLPUSH或者新版本的BLMOVE把消息从待处理队列原子地转移到处理中队列业务处理成功后再从处理中队列删掉。这样万一消费者挂了消息还在处理中队列里可以起个巡检任务把超时的消息挪回主队列。# BLMOVE从 source 弹出放到 dest处理完再 LREM 删除 BLMOVE queue:tasks queue:processing LEFT RIGHT 5但如果你的需求已经涉及消费组消息确认多消费者负载均衡那 List 就不够用了该上 Stream。Stream 是 Redis 5.0 引入的设计上就是照着轻量消息队列做的XADD生产、XREADGROUP按消费组读、XACK确认、XPENDING查未确认。它的可靠性比 List 高一个档次但复杂度也上来了运维成本不小。我的判断标准很粗暴任务丢了会不会有严重后果丢一两条日志、丢一两次统计List 足够丢订单、丢支付回调那就别在 Redis 上硬扛了老老实实上专业的消息中间件。Redis 的队列适合轻、快、可以接受少量丢失的场景超过这个范围就是在给自己找事。4. 实验环境里最容易卡住的几处按链路排查4.1 连接被拒和连接超时是两回事看到报错别急着改代码先把报错类型分清楚。拒绝连接和连接超时指向的是完全不同的两个环节。Connection refused意味着 TCP 握手被明确拒绝通常有三种可能Redis 进程没起来、端口不对、或者监听的地址和你连的地址不匹配。排查路径先ps -ef | grep redis看进程在不在不在就手动起一个在的话看启动日志里的监听端口和客户端配置比对。还有一个隐蔽情况——Redis 默认的bind 127.0.0.1只允许本机连接容器里跨机器访问就需要改成bind 0.0.0.0或者加上具体网卡地址。Connection timed out则是握手包压根没得到响应多半是网络不通或者被拦截。这时候ping host先看通不通telnet host port看端口开没开再往下就是看安全组、防火墙这些了。还有一种特殊报错连接成功了但一执行命令就返回NOAUTH Authentication required。这就是配了密码没认证AUTH yourpassword一下即可或者客户端配置里补上password参数。另一种是DENIED Redis is running in protected mode这是保护模式只允许本机无密码访问改配置或者加密码都能解决。下面这张表我建议背下来能省大量试错时间报错信息根因处理动作Connection refused进程未启动 / 端口错 / bind 限制查进程、对端口、改 bindConnection timed out网络不通 / 端口未放行ping、telnet、查防火墙NOAUTH Authentication required开启了密码认证客户端补 password 或 AUTHDENIED protected mode保护模式限制外来访问设密码或调整配置OOM command not allowed达到 maxmemory 且无淘汰策略调大内存或改淘汰策略4.2 数据写进去了又没了这个现象在实验环境里出现频率相当高而且新手容易怀疑是 Redis 本身有问题。实际上绝大多数情况是下面四种之一。第一种key 被别的地方删了或者覆盖了。实验里经常多人共用同一个实例不同题目用同样的 key 名互相踩踏。解决办法是给 key 加个自己的前缀比如exp1:user:1001。第二种过期时间被意外设置。有时候是从上一题复制过来的代码里面带着EXPIRE没删干净数据写进去几十秒就没了。可以用TTL key确认一下剩余时间返回 -1 表示永不过期-2 表示 key 已经不存在了。第三种写到了不同的库。Redis 默认 16 个库SELECT 1和SELECT 0是两个完全隔离的空间。客户端连的是 db 0命令行里敲过SELECT 2然后就发现数据不见了。这个坑我自己踩过排查了十分钟才反应过来。第四种实例重启了但没开持久化。数据全在内存里进程一重启就清零。实验环境里容器被回收重建是很常见的这时候数据丢失是必然的只能靠落盘解决——但这通常不是题目考察的点遇到这种情况应该确认是不是环境本身被重置了。4.3 评测脚本对 key 命名和类型的隐性要求在实验平台上做题最冤枉的失分是逻辑全对但评测不过。多数情况下问题出在格式契约上而不是功能上。评测脚本通常是这样工作的跑一遍你的程序然后用TYPE key检查类型是不是 String 而不是 Hash用GET key取值比对内容用TTL key检查过期时间是不是在预期区间用LLEN检查队列长度。这意味着几个细节必须严格对齐key 名一个字符都不能差大小写敏感、数据类型必须完全一致用HSET存的东西GET出来是空的、值的格式要能对上数字别存成字符串带引号的形式、JSON 字段顺序可能也要注意。我的建议是写完先自己在命令行里把评测脚本关心的 key 挨个TYPE一遍、TTL一遍、取值一遍比盲目提交省时间。另外记得清理测试数据——如果上一轮实验留下的数据还在LLEN出来的长度会翻倍评测自然不通过。实验开始前跑一次FLUSHDB是个好习惯当然只在你自己的库里。5. 让支撑程序稳住的几个工程细节5.1 连接池和超时参数别用默认值初学者最常见的写法是每个请求new一个 Redis 连接用完关掉。这个模式在压测下会直接把 Redis 打满——每次连接都要走 TCP 三次握手、认证、分配资源开销比执行命令本身大得多。标准做法是连接池进程启动时建好池子每个请求借一个连接、用完归还。import redis pool redis.ConnectionPool( host127.0.0.1, port6379, db0, max_connections50, # 按并发量估别贪大 socket_timeout2, # 单次读写超时防止卡死 socket_connect_timeout1, # 建连超时快速失败 health_check_interval30, # 定期探活避免拿到已断的连接 retry_on_timeoutFalse, # 超时是否重试写操作慎开 decode_responsesTrue, ) r redis.Redis(connection_poolpool)几个参数值得单独说。max_connections不是越大越好它应该和应用的并发线程数匹配设成 500 但实际只有 20 个线程就是浪费而设得比并发数小就会出现借不到连接的排队等待。socket_timeout一定要设Redis 是单线程的万一有慢命令阻塞了事件循环没有超时的客户端会一直挂在那里。retry_on_timeout建议关掉因为对非幂等的写操作自动重试可能导致重复执行宁可让上层报错处理。还有一点在容器环境里health_check_interval特别重要。网络抖动或者中间设备静默断开长连接时客户端手里的连接看起来还活着实际上发出去的命令全都没响应表现出来的现象就是偶尔卡几秒然后超时。定期探活能把这个窗口压到最小。5.2 大key和热key是性能问题的两大源头大 key 指的是单个 key 的 value 过大——String 超过 10KB或者集合类元素数量超过几千个。它的危害是多方面的读写时占用带宽、删除时阻塞主线程DEL一个百万元素的 Set 可能要几百毫秒这期间所有命令都在排队、持久化时拖慢 fork。检测手段是用redis-cli --bigkeys它会扫描全库并按类型列出最大的 key。删除大 key 不要用DEL用UNLINK——它把 key 从键空间摘掉真正的内存回收交给后台线程异步做不会阻塞主线程。对于超大集合还可以用SCAN配合分批SREM一点点拆解掉。热 key 则是另一个方向某个 key 被极高频率访问把单实例的 CPU 打满。典型的是大促时的爆款商品详情。应对手段有三种本地缓存兜一层牺牲一点一致性换性能、把 key 复制成多个副本随机选一个读比如hot:item:1001:0到hot:item:1001:9、或者做读写分离。选哪种取决于你的数据更新频率——几乎不变的数据用本地缓存最划算。5.3 持久化和淘汰策略两个必须做决定的配置持久化有 RDB 和 AOF 两种也可以混合使用。RDB 是定时快照文件小、恢复快但两次快照之间的数据会丢AOF 是追加写命令日志默认everysec每秒刷一次盘最多丢一秒数据但文件大、恢复慢。生产环境一般开混合持久化aof-use-rdb-preamble yes兼顾两者。要提醒的是 RDB 的fork操作。快照时会 fork 一个子进程在 fork 的瞬间主进程会短暂阻塞数据量越大阻塞越久。十几个 G 的实例 fork 一次可能卡几百毫秒这个时间对高并发服务来说是能感知的。所以要么控制单实例的数据量要么把快照时间安排在低峰期。淘汰策略的选择直接关系到内存满了会怎样。可选值有八种实际常用的就三个策略行为适用场景noeviction内存满时写入报错数据绝对不能丢宁可拒绝服务allkeys-lru从所有 key 中淘汰最近最少用的纯缓存场景最常用volatile-lru只淘汰设置了过期时间的 key缓存和持久数据混存在一个实例allkeys-lfu按访问频率淘汰有明显热点LRU 容易误伤纯缓存用途选allkeys-lru基本不会错。如果实例里既有缓存数据又有不能丢的数据那要么分开用两个实例要么用volatile-lru并确保所有缓存 key 都设了过期时间——最怕的是有人忘了设 TTL缓存数据变成了永久数据把内存一点点吃干。5.4 慢查询和日常监控问题要在发生前发现Redis 自带慢查询日志slowlog-log-slower-than单位是微秒默认 10000 也就是 10 毫秒。建议把它调到 5000 甚至更低因为 Redis 的单命令正常执行都在微秒级超过 5 毫秒就已经很可疑了。用SLOWLOG GET 10能看最近十条慢命令一眼就能看出是哪个 key 出了问题。日常监控我一般盯四个指标used_memory看内存增长趋势、connected_clients看连接数是否异常、instantaneous_ops_per_sec看 QPS 突增突降、keyspace_hits / keyspace_misses算缓存命中率。命中率是个特别敏感的指标正常缓存场景应该在 90% 以上一旦掉到 70% 以下说明缓存要么在大量过期要么 key 设计有问题需要立刻查。还有个容易忽略的点不要在生产上用KEYS *。它是 O(n) 的全量扫描几百万个 key 会直接把主线程卡住几秒钟这期间所有请求全部堆积。要遍历就用SCAN它用游标分批返回每次只扫一小部分对主线程的影响可控。SCAN的代价是可能返回重复元素因为扩容缩容期间槽位会变所以业务上要能容忍重复。真正跑过一段时间之后会发现Redis 的问题几乎从来不是命令写错了而是结构选错了参数没配容量没规划。入门阶段把这些底层逻辑捋清楚后面遇到再复杂的需求也不过是在这几种结构上做组合。我个人在实际操作中的体会是每次上线新的 Redis 用法之前先在测试环境用接近真实的数据量压一遍重点看内存增长曲线和慢查询日志。很多时候小数据量下跑得飞起的写法数据量上到十万级就开始暴露问题——比如一个HGETALL拿回来几千个字段、一个SMEMBERS返回几万个元素这些在实验里完全无感到线上就是事故。另外凡是写进缓存的 key一律强制要求设置过期时间这条规矩看起来死板但它能挡掉绝大多数内存莫名其妙涨满的麻烦。