资讯动态

Redis核心技术与实战:从数据结构、缓存治理到分布式锁与连接超时排查

发布时间:2026/10/26 15:54:32 来源:尧图企业网站定制
简介这份 Redis 核心原理与实战讲解资料包面向后端开发、运维及需要系统掌握 Redis 的进阶学习者按开篇、基础篇、实践篇分篇整理章节脉络清晰。基础篇覆盖键值数据库基本架构、常用数据结构、单线程高性能 IO 模型、AOF 日志与 RDB 快照、主从库数据同步、哨兵机制、哨兵集群和切片集群并含第19讲课后思考题答案与常见问题答疑实践篇具体讲解 String 类型的“万金油”误用、海量 keys 统计、GEO 自定义数据类型、时间序列保存、消息队列方案、异步阻塞规避、CPU 结构影响、响应延迟波动排查等真实场景。内容以课程讲解为主压缩包约 856MB目录结构利于按专题查阅目前已有 48 人学习。整体可帮助开发者打通原理与实战既适合面试前快速复习也适合线上 Redis 变慢等问题的排障参考。1. Redis核心技术与实战.zip别让 “ping 通” 变成你唯一的里程碑很多后端和运维拿到 Redis 相关的实战资料包后第一反应是照着敲redis-cli ping看到 PONG 就关掉终端觉得已经会了。真正的问题从第二天开始暴露高峰时段缓存 key 突然成片消失接口偶发Redis command timed out两个实例同时拿到同一把分布式锁日志里全是全量复制的同步记录。Redis核心技术与实战这类主题真正想解决的不是 “怎么装”而是 “装完之后参数怎么定、故障怎么排”。这套思路适合正在用 Redis 做缓存、消息队列或分布式锁的中级开发者也适合需要扛线上事故的运维。下面按一条我实践中最常用的排查链展开数据结构、缓存治理、持久化、主从、分布式锁再到连接超时。2. 从数据类型开始用对 Redis六类对象的实战行为与内存评估2.1 string/hash/set/zset 在真实请求里的取舍两个反直觉的例子很多教程把“String 存 JSON、Hash 存对象”讲成固定口诀真实请求里往往反直觉。第一个例子是用户信息缓存。用 Hash 的主要原因不是省内存而是支持局部更新只改昵称时HINCRBY或HSET一个字段不需要像 String 那样经历完整读改写。高并发下 String 的 read-modify-write 会因为三个步骤之间的时间差造成丢更新。但 Hash 有边界field 数量少于 128、单字段长度小于 64 字节时内部使用 ziplist 紧凑编码超过阈值自动转成 hashtable省内存优势消失。所以在选型前一定要看内部编码redis-cli OBJECT ENCODING user:1001 # 小对象返回 ziplist说明紧凑编码生效 # 返回 hashtable 说明字段数或长度已突破阈值逻辑说明OBJECT ENCODING返回的是 Redis 内部编码方式它不是线上排错的万能命令却是判断类型选型是否仍然成立的最快手段。如果发现本来期望 ziplist 的 key 变成了 hashtable就要回头查业务是不是在一个 key 里塞了太多字段这是最常见的 Hash 使用膨胀场景。第二个例子是排行榜。ZSET 的ZADD和ZINCRBY天然原子适合做热榜、积分榜。需要注意 score 是 double 类型超过 2^53 的整数会精度丢失如果同时要做“按时间排序”和“按分数排序”两个 ZSET 之间的数据一致性必须靠事务或补偿任务兜底。我一般会允许缓存层有几秒漂移因为排行榜场景对一致性要求没那么苛刻。import redis r redis.Redis(host127.0.0.1, port6379, decode_responsesTrue) r.zadd(ranking:2024, {order_1001: 200.5, order_1002: 180.0}) top r.zrevrange(ranking:2024, 0, 9, withscoresTrue) print(top)参数说明0, 9是索引区间表示取前 10 条withscoresTrue让返回结果带分数。注意这里用的是zrevrange而不是zrange因为排行榜一般按分数从高到低取两者方向相反写错就会把排序结果颠倒。2.2 过期策略与内存淘汰maxmemory-policy 五个选项与一片 key 消失的事后复盘Redis 内存不够时靠 maxmemory-policy 决定淘汰谁这里有五个常见选项每个的适用场景和风险差异很大整理成下面这张表策略淘汰范围优点风险noeviction不淘汰数据只读不丢写入直接报错allkeys-lru所有 key命中率整体较好无过期时间的低频 key 也可能被淘汰allkeys-lfu所有 key 按访问频率冷热分离效果更细腻需要 Redis 4.0 以上CPU 开销略高volatile-lru只淘汰设置了过期时间的 key不碰长期 key样本池可能正好全是快过期的 keyvolatile-ttl只淘汰剩余 TTL 最短的 key优先保新鲜数据大量长 TTL key 时几乎无淘汰效果一个常见的“现场”是这样的业务方给所有缓存 key 统一设置了 24 小时过期maxmemory-policy 配了 volatile-lru结果一到晚间高峰命中率突然从 95% 掉到 60%一批明明没过期的 key 也消失了。查监控才发现volatile-lru 的淘汰池只从“设了过期时间的 key”里采样而这些 key 恰好分布在未来几小时内Redis 只负责从采样样本里挑最久没用的不会区分它还剩多久过期。这个坑的解法有两条要么改用 allkeys-lru让淘汰池包含所有 key要么把“永不淘汰的配置类数据”与“可淘汰的缓存数据”放到不同实例或不同逻辑库再分别设置淘汰策略。查看当前实例内存状态时我习惯先跑下面这组命令redis-cli config get maxmemory redis-cli config get maxmemory-policy redis-cli info memory | grep -E used_memory_human|maxmemory_human|mem_fragmentation_ratio参数说明maxmemory是 Redis 可用的内存上限maxmemory-policy是达到上限后的行为mem_fragmentation_ratio是内存碎片率。这个值长期大于 1.5、且确认没有大 key 时通常是频繁更新大对象或大量短生命周期 key 导致的碎片可以在低峰期用MEMORY PURGE尝试整理如果整理效果差就只能通过重启实例重排内存。2.3 用一个 1GB 以下实例验证选型redis-benchmark 与 INFO 的两次输出对比不要在没压测的情况下直接拍板用哪种类型。即使是一个 1GB 的小实例也能通过 redis-benchmark 快速得到一组可对比的数字。下面是我常用的两组命令redis-benchmark -h 127.0.0.1 -p 6379 -t set,get -n 100000 -c 50 -d 256 redis-benchmark -h 127.0.0.1 -p 6379 -t hset -n 100000 -c 50 -d 256逻辑说明-n 100000表示发送 10 万次请求-c 50是并发连接数-d 256是每个 value 的大小。第一组压 String第二组压 Hash 的写入。真正值得看的不是 requests per second 有多高而是能不能在同样的并发和 value 大小下保持接近的延迟如果 Hash 的 p99 明显高于 String要怀疑字段数量是否已经突破紧凑编码阈值而不是急着怪 Redis。压测前后各取一次内存形成对比redis-cli info memory | grep used_memory_human redis-cli -h 127.0.0.1 -p 6379 info stats | grep -E instantaneous_ops_per_sec参数说明used_memory_human是当前内存使用量instantaneous_ops_per_sec是实时吞吐。常见误用是只用info memory看总内存不看碎片率和瞬时吞吐结果压测时内存看着正常一上生产就频繁淘汰。小实例压测的价值在于用最便宜的方式提前暴露类型选型和内存参数的问题而不是等线上事故来验收。3. 缓存治理的三种事故现场穿透、击穿、雪崩的止血顺序3.1 缓存穿透参数化 key 设计、空值缓存与布隆过滤器的最小实现缓存穿透的用户画像很典型同一个不存在的 ID 被反复请求缓存查不到DB 也查不到于是每次请求都穿透到数据库。最直接的办法是把“查不到”也缓存起来// 查询用户若 DB 返回 null则写一个短 TTL 的占位符 Object value redisTemplate.opsForValue().get(user: userId); if (value null) { User user userMapper.selectById(userId); if (user null) { redisTemplate.opsForValue().set(user: userId, , 60, TimeUnit.SECONDS); return Result.empty(); } redisTemplate.opsForValue().set(user: userId, JSON.toJSONString(user), 1800, TimeUnit.SECONDS); }逻辑说明空值也缓存 60 秒可以挡住同一个不存在 ID 的重复穿透。这里的参数60不能设太长因为如果数据源后续真写入了该 ID空值缓存会掩盖新数据。另一个问题是攻击者可以构造大量不同 ID空值缓存会膨胀内存因此要配合“参数化 key 设计”也就是 key 只用业务真正存在的 ID 维度而不是把用户传入的任意参数直接拼进 key。更进一步的做法是布隆过滤器在缓存之前先做一次存在性判断// 启动时加载存量 ID 到布隆过滤器误判率设为 1% BloomFilterString userIdFilter BloomFilter.create( Funnels.stringFunnel(StandardCharsets.UTF_8), 1_000_000, 0.01); if (!userIdFilter.mightContain(userId)) { return Result.empty(); }参数说明1_000_000是预估元素数量0.01是误判率。误判率越低位数组越大内存占用越高一般业务取 1% 就够。布隆过滤器只能挡掉大多数不存在的 key少量误判仍会穿透到 DB所以空值缓存和布隆过滤器是从两个层面叠加的而不是二选一。3.2 缓存击穿互斥锁重建与逻辑过期哪个更稳缓存击穿是某一个热点 key 在过期瞬间大量请求同时打到底层存储。最常见的热点重建方案是互斥锁// 尝试获取重建锁只允许一个线程去回源 Boolean got redisTemplate.opsForValue() .setIfAbsent(lock:hotkey, token, 3, TimeUnit.SECONDS); if (Boolean.TRUE.equals(got)) { try { String value loadFromDb(); redisTemplate.opsForValue().set(hotkey, value, 3600, TimeUnit.SECONDS); } finally { // 用 Lua 脚本比对 token 后删除防止误删新锁 releaseLock(lock:hotkey, token); } }逻辑说明setIfAbsent等价于SET key value NX EX 3只有第一个请求能拿到锁并回源其余请求拿不到锁就走了。这里的token是当前线程生成的唯一标记删除锁时要比对 token否则可能出现 A 线程的锁已过期、B 线程刚重建锁A 的 finally 把 B 的锁误删。锁过期时间 3 秒只是兜底如果回源超过 3 秒锁提前失效就会放行第二个线程这时要评估锁时间是否应该覆盖回源最慢耗时或改用逻辑过期方案。逻辑过期的思路完全不同不把 Redis 里的 key 真正过期而是给缓存对象加一个 expireAt 时间戳读请求发现过期后只允许一个线程去回源其余请求直接返回旧数据。好处是读请求永远不阻塞坏处是拿到的是“已经过期一会儿”的数据。选择哪个方案取决于业务能不能接受短暂旧数据能接受就用逻辑过期不能接受就上互斥锁并加大锁过期余量。3.3 雪崩防线的三个动作过期时间加盐、快速失败与半开降级雪崩是大量 key 在同一时间点集中失效后端瞬间被打满。第一道防线是让过期时间随机化int baseTtl 3600; int ttl baseTtl ThreadLocalRandom.current().nextInt(600); redisTemplate.opsForValue().set(cache: key, value, ttl, TimeUnit.SECONDS);参数说明baseTtl是基础过期时间nextInt(600)在 1 小时内引入随机抖动让 key 的失效时间点散开。这里常见误用是把所有 key 的过期时间都设成整点附近结果是每小时整点都会出现一次小规模雪崩。加盐之后虽然做不到完全均匀但已经足以避免“全军同时过期”。第二道防线是快速失败。后端服务应当为 Redis 操作设置足够短的超时时间并让调用方在超时后直接走降级逻辑而不是反复重试。第三道防线是半开降级把对 Redis 的读写开关做成可配置项当检测到延迟升高或错误率上升时先切到“只读缓存不写缓存”模式让后端压力先降下来等观察窗口通过后再恢复全量读写。这样能把雪崩的影响面控制在缓存层而不是一路蔓延到数据库。4. 持久化与主从复制RDB/AOF 怎么搭、增量同步积压点在哪里看4.1 RDB 与 AOF参数对照表和不同故障等级下的选择Redis 持久化只有两种主流方案RDB 做全量快照AOF 做命令追加。很多人纠结选哪个其实取决于你能容忍丢多少数据、恢复时间要求多快维度RDBAOF触发方式save 规则、手动 BGSAVE每写一条命令按策略写盘数据粒度全量二进制快照追加写命令恢复速度快直接载入快照慢要重放全部命令丢数据风险可能丢最后一次快照之后的数据everysec 丢 1 秒always 不丢但慢适用场景冷备、缓存可重建存放不可轻易重建的数据RDB 的典型配置是save 900 1意思是 900 秒内至少有 1 个 key 变化就触发快照AOF 的典型配置是appendfsync everysec每秒刷一次盘。生产环境我很少二选一更多是混合持久化aof-use-rdb-preamble yes让 AOF 文件头部先放一份 RDB 快照再追加增量命令。这样重启时先加载 RDB再重放少量增量命令恢复速度和丢数据风险同时被控制住。很多人把 AOF 打成“AOP”查日志时对着appendonly找不到配置其实 Redis 配置项叫appendonly检查时直接看这项redis-cli config get appendonly redis-cli config get appendfsync redis-cli config get aof-use-rdb-preamble参数说明appendonly控制是否开启 AOFappendfsync决定刷盘策略aof-use-rdb-preamble在 Redis 4.0 之后可用开启后 AOF 文件头部会内嵌 RDB 快照。这里的坑是如果业务写入量极大always策略会让每条命令都同步刷盘延迟明显变高everysec是大多数线上场景的折中点。4.2 主从复制三个阶段的关联与 docker 安装 Redis 主从的最小配置主从复制可以拆成三个阶段全量同步、增量传播、心跳保活。全量同步发生在从节点刚接入或复制积压缓冲不足以覆盖断线期间写入时增量传播是正常状态下主节点把写命令推给从节点心跳则是主从双方周期性互联确认对方还活着。在 docker 环境下搭主从最容易犯的错误是把从节点指向127.0.0.1。容器里127.0.0.1指向容器自身从节点根本连不上主节点。正确做法是用服务名或容器 IPservices: redis-master: image: redis:7-alpine container_name: redis-master command: [redis-server, --appendonly, yes] ports: [6379:6379] redis-slave: image: redis:7-alpine container_name: redis-slave command: [ redis-server, --replicaof, redis-master, 6379, --appendonly, yes ] depends_on: - redis-master逻辑说明从节点的--replicaof redis-master 6379会把主节点的主机名解析为 docker 网络里的服务地址depends_on保证 master 先启动但依赖它不等于等待它 ready所以从节点可能比主节点先进入运行状态此时从节点日志会出现连接拒绝这是正常现象它会自动重试。验证主从关系是否建立用这个命令redis-cli -h 127.0.0.1 -p 6379 info replication # 关注 role:slave、master_link_status:up、slave_read_repl_offset参数说明role显示当前节点角色master_link_status是主从链路状态只有up才能正常复制slave_read_repl_offset和master_repl_offset的差值代表从节点落后主节点的字节数。这个差值如果持续增长说明从节点消费速度跟不上主节点写入速度需要检查从节点所在宿主机的 IO 和网络带宽。4.3 从一个 Redis 日志时间线定位全量复制与增量复制的切换点主从不稳定时日志是最好的时间线还原工具。典型的全量复制日志长这样# Full resync requested by replica # Background RDB transfer started # SYNC sent 2048 MB to replica # MASTER - REPLICA sync: Finished with success第一行意味着从节点触发了全量复制原因通常有两个刚接入的新从节点或断线时间超过了repl-backlog-size能覆盖的范围。中间的 “Background RDB transfer started” 说明主节点正在后台生成快照并传输最后的 Finished 表示这次全量同步完成。如果日志里频繁出现 Full resync不要急着怀疑网络。先看积压缓冲区redis-cli config get repl-backlog-size redis-cli info replication | grep repl_backlog参数说明repl-backlog-size是主节点为从节点保留的增量缓冲区默认 1MB。判断标准是从节点断线期间主节点写入的数据量如果超过这个值重连后必然全量复制。建议把缓冲区大小配置为“断线重连窗口内最大写入量”的两倍以上例如高峰 1 分钟写入 5MB就给repl-backlog-size 16mb避免一断线就全量复制。全量复制本身不是错但它会占用主节点磁盘 IO 和网络带宽高峰时段触发会让主节点延迟明显抬升。5. Redis 分布式锁避坑记录从 setnx 到 Redisson 的四个现场5.1 现场一锁提前过期任务还没跑完第二个客户端就进了临界区现象A 客户端用SET key token NX EX 10拿到锁但业务任务实际耗时 15 秒锁在第 10 秒过期B 客户端立刻拿到锁进入临界区两个客户端同时执行互斥逻辑。原因给锁设置过期时间时只考虑了“正常耗时”没有为慢请求、GC 停顿、网络抖动留出余量。解决把锁过期时间从 10 秒提到 30 秒再用 Lua 脚本保证“检查 token删除锁”的原子性-- KEYS[1] 是锁 keyARGV[1] 是当前线程的 token if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end逻辑说明删除锁前必须比对 token防止 A 线程业务执行完把 B 线程刚重建的锁删掉。这条 Lua 脚本的意义在于 Redis 会原子执行它不会在 get 和 del 之间插入其他命令。但手动延长过期时间只是权宜之计。更稳的做法是用 Redisson 的看门狗机制持有锁期间Redisson 会每隔锁过期时间的 1/3 自动续期只有客户端还活着就持续续期客户端崩溃时锁自然过期。这个机制解决的就是“锁时间不够任务跑”的经典痛点。5.2 现场二主从切换期间 “锁”被复制到另一台机器两个客户端同时持锁现象A 客户端在 master 上写入锁master 还没把这条命令同步给 slave 就发生故障哨兵把 slave 提升为 masterB 客户端在新的 master 上SET NX成功两把“同一把锁”同时存在。原因Redis 主从复制是异步的锁的写入不保证多副本强一致。解决这个场景在单机房 Redis 部署里无解因为主从架构天然牺牲了强一致换可用性。常见出路有三个一是采用 Redisson 的 RedLock 算法向多个独立 Redis 节点同时申请锁多数派成功才算拿到锁二是对锁的持有方做唯一性约束让业务本身具备幂等性比如把 token 写进数据库或消息里即使锁被穿透也能靠幂等兜底三是评估业务是否真的不能接受“短暂双持”很多内部任务允许一定概率的重入用业务幂等换锁的可用性成本往往更低。5.3 现场三setnx 成功后客户端抛异常锁被永久占有现象客户端执行SET key token NX EX 10成功后在业务逻辑里抛了未捕获异常代码没有走删除锁的 finally 分支10 秒过期时锁已经释放但异常发生得太快过期时间被忽略故障恢复后锁一直留在 Redis 里。原因setnx成功后开发者忘记在 finally 里删锁或者删除锁的代码只写在成功路径上。解决把“获取锁-执行业务-释放锁”写进 try-finally并且把删除锁的 Lua 脚本封装成公共方法核心原则是“无论业务成功还是失败锁必须释放释放锁必须校验 token锁必须有兜底过期时间”。5.4 排查顺序从 Redis 层面观察锁竞争现场线上锁冲突需要证据而不是靠猜。我一般按下面这个顺序取现场# 1. 看当前存在的锁和剩余 TTL redis-cli --scan --pattern lock:* # 2. 对可疑 key 查看剩余过期时间 redis-cli ttl lock:order:1001 # 3. 观察一段时间内客户端命令序列 redis-cli monitor参数说明--scan配合 pattern 可以避免KEYS lock:*一次性阻塞 Redis 大实例ttl用于判断锁是否已经过期并确认是否被错误持有monitor会实时输出所有命令可以用来观察锁的竞争窗口但生产高峰期不要长时间开着monitor它会把实例的吞吐打到明显下降。实际操作时线上几乎不可能抓到完整竞争窗口所以我更推荐把锁的获取耗时和持有耗时打印到业务日志里配合 Redis 侧MONITOR做交叉验证。日志里如果出现“获取锁等待超过 1 秒”的频率上升说明锁的临界区耗时已经影响到了下游大量请求这会进一步放大主从切换下的双持风险。6. Redis Command Timed Out 排查线Lettuce 线程模型与连接参数的最后落脚点Redis 客户端最常见的报错之一是Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException。这个报错的直接含义是命令发出后在配置的超时时间内没有拿到响应。很多人的第一反应是加长超时时间但这通常治标不治本。Lettuce 是线程安全的默认用一个共享连接处理所有命令配合 io threads 做网络读写。如果应用侧大量执行阻塞命令比如慢查询、大 key 操作或KEYS全表扫描会占住这个共享连接的读写线程后面的正常命令全部排队最终超过 commandTimeout。另一个常见原因是连接池配置过小压测时 QPS 上涨活跃连接不够用新请求在池里等待等待时间也被计入超时。Spring Boot 下常见的调整方向是这样一组参数spring: data: redis: timeout: 3s connect-timeout: 2s lettuce: pool: max-active: 16 max-idle: 8 min-idle: 2参数说明timeout是命令的读超时connect-timeout是建立连接的超时max-active是连接池最大连接数max-idle和min-idle控制空闲连接的上下限。调参时要注意timeout不是越大越好线上 3 秒已经是比较保守的上限真正要查的是有没有慢命令占住线程。排查路径是先在 Redis 侧看慢日志redis-cli slowlog get 10 redis-cli slowlog len通过slowlog返回的命令耗时和参数判断是哪些 key 或命令长期占用线程。如果慢日志没有异常再检查连接池是否被打满连接池的监控指标在 Lettuce 里可以通过 Micrometer 暴露 active/idle/pending 连接数而 Redis 侧的INFO clients可以看到当前连接数和阻塞客户端数量。这两个数据一对比基本就能定位是连接池太小还是 Redis 端有命令卡住。我现在遇到这一类连接超时已经不会先去改超时时间而是先把慢日志、连接池指标、Redis 端INFO clients三个数据抓全再决定是加连接、减慢命令还是切客户端线程模型。这个排查顺序帮我在好几次事故里少走了弯路希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑