Loading

【WebWiki】10 · Redis

04 Redis

WebWiki · 缓存篇

1. 数据类型与典型场景

类型 底层结构 典型场景
String SDS 缓存 JSON、计数器(INCR)、分布式锁(SET NX EX)
Hash ziplist / hashtable 对象缓存(按字段读写)、购物车 HINCRBY
List quicklist 消息队列(轻量)、最新列表 LPUSH+LRANGE
Set intset / hashtable 标签、共同关注 SINTER、抽奖 SRANDMEMBER
ZSet 跳表+dict 排行榜 ZRANGE、延时队列(score=执行时间)
Bitmap String 位图 签到 SETBIT、日活统计
HyperLogLog 概率结构 亿级 UV 去重计数(0.81% 误差)
Stream Radix 树 消息队列(消费组、ACK,比 List 专业)
# 排行榜三连
ZINCRBY leaderboard:2026-09 1 "user:1001"
ZREVRANGE leaderboard:2026-09 0 9 WITHSCORES      # Top10
ZREVRANK leaderboard:2026-09 "user:1001"          # 我的排名

为什么 Redis 这么快(复习):纯内存操作、单线程避免锁/上下文切换、基于 epoll 的 I/O 多路复用、高效数据结构(下面第 2 节细讲)。6.0 引入多线程 I/O(仅网络读写多线程,命令执行仍单线程),进一步榨干多核网卡。

2. 五大数据类型的底层编码(面试核心)

Redis 的"类型"是给用户看的,底层由 encoding(编码) 决定真正怎么存。一条 TYPE 显示 hash,底层可能是 ziplist(小数据)也可能是 hashtable(大数据)。用 OBJECT ENCODING key 查看。

2.1、SDS(Simple Dynamic String,字符串底层)

Redis 没用 C 原生字符串(以 \0 结尾的 char*),而是自研 SDS:

struct sdshdr {
    int len;     // 已用长度,O(1) 取长度
    int alloc;   // 分配容量
    int flags;   // 头部类型
    char buf[];  // 字节数组,二进制安全
};

相比 C 字符串的优势:

  • O(1) 求长度:C 串要遍历到 \0,SDS 直接读 len;
  • 二进制安全:能存图片/Protocol Buffer 等含 \0 的数据,不靠 \0 判结尾;
  • 杜绝缓冲区溢出:sdscat 先检查空间,不够自动扩容;
  • 预分配 + 惰性释放:扩容翻倍(<1MB)或 +1MB(≥1MB),缩容不立即回收,减少内存重分配。

注意:Redis 的 String 不仅能存字符串,还存整数(共享 REDIS_SHARED_INTEGERS 小整数对象池)、字节数组。当值是可以用 long 表示的整数时,底层用 int 编码,INCR 直接改整数无需解析。

2.2、ziplist 与 listpack(紧凑列表)

  • ziplist:一段连续内存压缩存储,省内存,适合小数据。每个 entry 记录"前一节点长度"(用于反向遍历),但级联更新是痛点:中间插入一个大节点导致"前一节点长度"从 1 字节变 5 字节,可能触发后续一连串重编码。
  • listpack(7.0 起逐步替代 ziplist):改进设计,每个 entry 只记录自身长度,避免级联更新;Hash/ZSet/List 的小数据底层编码从 ziplist 迁移到 listpack。

当 Hash/List/ZSet 元素少且体积小(阈值可配:hash-max-listpack-entries、hash-max-listpack-value 等)时,用 listpack;超过阈值自动转成 hashtable / skiplist + dict。

# 查看底层编码
127.0.0.1:6379> HSET cart:u1 sku1 2
127.0.0.1:6379> OBJECT ENCODING cart:u1
"listpack"          # 元素少→listpack
# 塞入大量/大字段后再次查看会变成 "hashtable"

2.3、quicklist(List 底层)

Redis 3.2+ 的 List 底层是 quicklist = 链表 + ziplist/listpack:宏观是双向链表,每个节点是一个小的 ziplist(控制每块大小 list-max-listpack-size),兼顾"节点插入删除 O(1)"和"内存紧凑"。老版本 List 早期用 ziplist+linkedlist,后统一为 quicklist。

2.4、intset(Set 小整数集合)

当 Set 全是整数且元素数不多(set-max-intset-entries)时,底层是 intset(有序整数数组,查找二分 O(log n));出现非整数或大元素则升级为 hashtable。

2.5、skiplist + dict(ZSet 底层,最重要)

ZSet(有序集合)底层用跳表(skiplist)+ 字典(dict)两张结构,各取所长:

  • dict:member → score 映射,O(1) 查某成员分数(ZSCORE)。
  • skiplist:按 score 排序的多层链表,支持范围查询、排名(ZRANGE/ZRANK)O(log n)。

为什么用两个而不是只用一个?只靠 dict 无法按 score 排序取范围;只靠跳表查单点分数要 O(log n) 且占内存多(跳表每个节点要存 member+score+多层指针)。两个结构共享 member/score 对象,不重复存值,省内存又全能。

跳表原理:每个节点随机层数(幂次概率,越高层越稀),形成"高速公路"。查找时从最高层往右、往下,平均 O(log n),实现比平衡树简单、范围遍历友好。

层 3:  head ──────────────────────────► 节点10
层 2:  head ───► 节点3 ─────────────► 节点10
层 1:  head ─► 节点1 ─► 节点3 ─► 节点6 ─► 节点10
       (按 score 有序,每层是 next 指针)

2.6、dict 与渐进式 rehash(核心中的核心)

Hash、Set(hashtable)、ZSet(dict 部分) 都用哈希表 dict:

dict
 ├ ht[0]            # 当前哈希表
 ├ ht[1]            # rehash 时的目标表
 ├ rehashidx        # rehash 进度(-1 表示不在进行)
 └ 两个 ht 各是:数组 + 链地址法解决冲突

扩容/缩容时机:

  • 负载因子 used / size ≥ 1 且无 BGSAVE/BGREWRITEAOF(或 ≥ 5 强制)则扩容,size 取 ≥ used*2 的最小 2^n;
  • 负载因子 < 0.1 缩容。

为什么"渐进式 rehash"而不是一次性搬? 如果一次性把 ht[0] 几百万 key 搬到 ht[1],会长时间阻塞(Redis 单线程!)。所以 Redis 采用渐进式:

  1. 分配 ht[1],置 rehashidx = 0;
  2. 此后每次增删改查 dict 时,顺手把 ht[0] 第 rehashidx 个桶的全部 key 搬到 ht[1],rehashidx++;
  3. 还靠 serverCron 定时任务分批搬(每毫秒搬若干桶),防止长时间无访问导致搬迁卡住;
  4. 搬迁完成,ht[0] 释放,指针互换,rehashidx = -1。

搬迁期间查找"两个表都查"(先 ht[0] 再 ht[1]),新增只写 ht[1],保证 ht[0] 只减不增、最终搬完。

127.0.0.1:6379> DEBUG OBJECT myhash   # 观察内部(含编码、长度等,调试用)
127.0.0.1:6379> OBJECT ENCODING myhash
"hashtable"

编码小结:小数据省内存(listpack/intset/ziplist),大数据上性能结构(hashtable/skiplist),阈值可配、自动转换。理解编码能解释"为什么同样命令内存占用差很多""大 key 为什么慢"。

3. 持久化

方式 原理 优缺点
RDB 定时 fork 子进程全量快照 文件小恢复快;可能丢最后几分钟
AOF 追加写命令(everysec 折中) 丢数据少;文件大、重写有开销
混合持久化(4.0+) RDB 头 + AOF 增量 生产推荐 aof-use-rdb-preamble yes

3.1、RDB 机制细节

  • 触发:save(同步,阻塞,别用)、bgsave(异步 fork 子进程)、配置 save 900 1(900 秒内 1 次改动)、主从全量同步时、执行 flushall/shutdown(带 save) 时。
  • fork + 写时复制(COW):bgsave 时父进程 fork 出子进程共享内存页;子进程把内存数据序列化进 dump.rdb;期间父进程写某页,该页被 COW 复制一份,子进程读旧页——所以快照是 fork 那一刻的一致性视图,且内存占用峰值可能是 2 倍(大量写入时)。
  • 优点:二进制压缩、恢复极快、适合备份/灾难恢复、fork 后主进程几乎无感。
  • 缺点:两次快照间宕机丢数据(取决于 save 间隔);fork 大内存实例有停顿(页表复制,几十 GB 内存 fork 可能百毫秒级)。

3.2、AOF 机制细节

  • 原理:把每条写命令以 Redis 协议文本追加到 appendonly.aof。重启用 redis-check-aof 把文件读回来重放。
  • 刷盘策略 appendfsync:
    • always:每条命令 fsync,最安全、最慢;
    • everysec:默认,每秒 fsync 一次,最多丢 1 秒,性能与安全折中(推荐);
    • no:交给 OS 刷,最快、可能丢更多。
  • AOF 重写(Rewrite):AOF 只追加会越来越大(如 INCR 100 次存 100 行)。BGREWRITEAOF fork 子进程,根据内存现状生成"最小命令集"(如 SET cnt 100),新 AOF 体积小。重写期间新命令同时写"旧 AOF"和"重写缓冲",重写完合并。
  • 混合持久化(4.0+):aof-use-rdb-preamble yes 时,AOF 重写文件以 RDB 格式开头 + 增量 AOF 命令,重启先快速加载 RDB 再重放少量 AOF,兼顾速度与少丢数据。生产首选。
# 配置示例(redis.conf)
save 900 1
save 300 100
save 60 10000
appendonly yes
appendfsync everysec
aof-use-rdb-preamble yes     # 混合持久化
auto-aof-rewrite-percentage 100   # 体积翻倍触发重写
auto-aof-rewrite-min-size 64mb

选型:纯缓存(可丢)可只开 RDB;需持久化(如会话、排行榜)开 AOF + 混合。金融级再加主从/集群多副本。

4. 过期与淘汰

  • 过期策略:惰性删除(访问时检查)+ 定期删除(抽样清理);
  • 内存满时按 maxmemory-policy 淘汰:
策略 说明
noeviction(默认) 写报错
allkeys-lru 全部键 LRU 淘汰(纯缓存推荐)
volatile-lru / volatile-ttl 只淘汰带过期时间的键(有持久数据时用)
allkeys-lfu(4.0+) 访问频率优先

深入:过期删除为什么是"惰性 + 定期"而非定时?

  • 定时删除(每 key 一个 timer)太耗 CPU,key 多时不可行。
  • 惰性删除:读/写某 key 时先 expireIfNeeded,过期则删。缺点:永不访问的过期 key 占内存(内存泄漏隐患)。
  • 定期删除:serverCron 每秒跑 10 次(默认),每次随机抽 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP(默认20) 个带过期时间的 key 清理,超时(默认 25ms)即停,循环到比例 < 25% 才收手。平衡 CPU 与内存。

LRU vs LFU(4.0+ 推荐 LFU):

  • LRU(最近最少用):近似 LRU(Redis 不维护完整链表,用 24bit 时间戳 + 随机采样 N 个淘汰最旧),仅看"多久没访问",易被一次性全扫的冷数据污染。
  • LFU(最不经常用):记访问频率(lfu_log_factor 控制计数增长、lfu_decay_time 控制衰减),能识别"长期高频"vs"偶尔爆发",缓存命中率更高。生产纯缓存建议 allkeys-lfu。
# 内存与淘汰配置
maxmemory 4gb
maxmemory-policy allkeys-lfu
# 采样精度(LRU/LFU 近似度)
maxmemory-samples 10

5. 缓存读写模式与三大问题

Cache-Aside 模式

// Cache-Aside 读路径
public ProductVO getProduct(Long id) {
    String key = "product:" + id;
    String json = redis.get(key);
    if (json != null) return JSON.parseObject(json, ProductVO.class);

    // miss:加互斥锁防击穿(只放一个线程回源)
    String lockKey = "lock:" + key;
    if (redis.setIfAbsent(lockKey, "1", Duration.ofSeconds(3))) {
        try {
            Product p = productMapper.selectById(id);
            if (p == null) {
                redis.set(key, "", Duration.ofSeconds(60));   // 空值缓存防穿透
                return null;
            }
            redis.set(key, JSON.toJSONString(p),
                      Duration.ofMinutes(30).plusSeconds(ThreadLocalRandom.current().nextLong(60))); // TTL 加随机防雪崩
            return ProductVO.from(p);
        } finally {
            redis.delete(lockKey);
        }
    }
    Thread.sleep(50);
    return getProduct(id);   // 自旋重试
}
问题 现象 对策
缓存穿透 查询不存在的数据,全打到 DB 空值缓存、布隆过滤器、参数校验
缓存击穿 热点 key 过期瞬间大量回源 互斥锁重建、热点 key 永不过期+后台刷新
缓存雪崩 大量 key 同时过期 / Redis 宕机 TTL 加随机、多级缓存、集群高可用、限流降级

写路径:先更新数据库,再删除缓存(Cache-Aside 标准写法);要求更高时用 Canal 订阅 binlog 异步删,实现最终一致。

三大问题深入(第 11 篇有落地命令,这里讲原理)

5.1、缓存穿透(查不存在的数据)

  • 成因:恶意/异常请求查 DB 和缓存都没有的 key(如 id=-1、id=超长随机),缓存不命中 → 每次打到 DB,缓存形同虚设。
  • 对策 1 空值缓存:DB 也没查到,缓存一个短 TTL 的空对象(如 ""/NULL 标记),拦住重复穿透。代价:占用一点内存、与 DB 有短暂不一致(空值过期前若数据补上,需主动删/等 TTL)。
  • 对策 2 布隆过滤器(见 5.4):在缓存前加一层,所有合法 key 预先放进 BF,查前先问 BF;BF 说"不存在"直接拒绝,绝不透传到 DB。BF 说"可能存在"才放行(有误判但可接受)。
  • 对策 3 参数校验/限流:id 范围、格式、频率校验,配合熔断降级。

5.2、缓存击穿(热点 key 突失效)

  • 成因:某个极热点 key(如首页爆款、热搜)在过期瞬间,海量并发同时 miss,瞬间全部回源 DB,DB 被打垮。
  • 对策 1 互斥锁重建:如上面代码,只放一个线程去 DB 加载,其他线程等/重试(本地锁或 Redis SET NX 分布式锁)。
  • 对策 2 热点永不过期 + 后台刷新:逻辑过期(值里带 expireTime,读时不依赖 Redis TTL,另起线程/定时刷新),或物理不过期、由后台任务主动更新。代价:实现复杂、需保证最终一致。
  • 对策 3 提前预热:大促前把热点数据主动灌缓存。

5.3、缓存雪崩(大量 key 同时失效 / Redis 挂)

  • 成因:大量 key 设了相同 TTL(如都 1 小时),到点集体失效,瞬时全打 DB;或 Redis 集群整体宕机。
  • 对策:
    • TTL 加随机抖动:base + random(0, base*0.3),错开失效时间(代码已体现);
    • 多级缓存:本地缓存(Caffeine)→ Redis → DB,Redis 挂还有本地兜底;
    • 高可用:主从 + 哨兵 / Cluster,避免单点(见第 6 节);
    • 限流降级:熔断、排队、返回默认,保护 DB;
    • Redis 持久化 + 快速恢复:宕机后尽快拉起并预热带。

5.4、布隆过滤器(Bloom Filter)原理与落地

布隆过滤器是"空间极小、查询极快、可能误判(false positive)、绝不漏判(false negative)"的概率型集合。

  • 原理:一个 m 位 bit 数组 + k 个独立哈希函数。
    • 加入元素 x:k 个哈希算 k 个位置,全部置 1;
    • 查询元素 x:k 个位置全为 1 → "可能存在"(若有任一位 0 → "一定不存在");
    • 误判来源:不同元素哈希位重叠,导致"查一个没加过的,结果位全 1"→ 误判存在,但绝不会把加过的判成不存在。
  • 落地(Redis + Redisson 的 RBloomFilter):
// 初始化布隆过滤器(预计元素 1 亿,误判率 1%)
RBloomFilter<String> bf = redisson.getBloomFilter("user:id:bf");
bf.tryInit(100_000_000L, 0.01);

// 数据初始化/新增时把合法 id 加入
bf.add(String.valueOf(userId));

// 查询前先问布隆过滤器
public User getById(Long id) {
    if (!bf.contains(String.valueOf(id))) {
        return null;                          // 一定不存在,直接返回,不查 DB
    }
    // 可能存在 → 走缓存/DB(此处仍可能误判,再查 DB 兜底)
    return userService.queryFromDb(id);
}
  • 注意:布隆过滤器不支持删除(清位会影响别的元素);需要删除用 Counting Bloom Filter / Cuckoo Filter。误判率与 m/k 相关:元素越多、误判率越高,初始化要估准容量。

6. 分布式锁

// Redisson:看门狗自动续期,可重入
RLock lock = redisson.getLock("order:pay:" + orderId);
try {
    if (lock.tryLock(3, 30, TimeUnit.SECONDS)) {   // 最多等 3s,持锁 30s
        // 业务处理
    }
} finally {
    if (lock.isHeldByCurrentThread()) lock.unlock();
}

要点:加锁必须带唯一 value + 过期时间(防死锁与误删);释放用 Lua 保证"判断+删除"原子性;Redisson 已封装好上述一切。

手写分布式锁的正确姿势(理解原理,别只会调 Redisson)

6.1、SET NX EX 加锁

# 原子加锁:不存在才设、同时给过期时间(单条命令,避免 SETNX + EXPIRE 之间宕机导致死锁)
SET lock:order:1001 <唯一token> NX EX 30
# 唯一 token 用 UUID,保证"只能删自己加的锁"
  • 为什么必须 NX + EX 一条命令:分开 SETNX 再 EXPIRE,若中间进程挂了,锁永不过期 → 死锁。合到一条命令才原子。
  • 为什么 value 要唯一:避免 A 线程锁过期、B 拿到锁后,A 执行完误删 B 的锁。释放前先校验 value 是自己的。

6.2、Lua 脚本释放锁(保证原子)

-- 释放锁:只有 value 匹配才删(GET + DEL 原子执行)
if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end
# 调用(EVAL script numkeys key value)
redis-cli --eval unlock.lua lock:order:1001 , <唯一token>

不用 Lua 的话,GET 判等再 DEL 两步之间有竞态(判完过期、被别人拿到、你却删了别人的)。Lua 在 Redis 中单线程原子执行,无此问题。

6.3、锁续期(看门狗)

业务执行可能超过锁 TTL(如 30s 没跑完),锁过期被别人抢走 → 并发。Redisson 的"看门狗":加锁后起守护线程,每 TTL/3 续期一次(重置 EX 为 30s),直到 unlock。手写版可用"定时任务续期"或"Redisson 直接用"。

6.4、RedLock(红锁)与争议

Redis 作者提出多节点 RedLock(多数节点加锁成功才算拿到锁),应对单实例故障。但分布式时钟、GC 停顿等仍可能出问题,Martin Kleppmann 曾发文质疑。工程取舍:绝大多数场景"单 Redis 实例 + 过期 + 续期"足够;强一致需求考虑 ZooKeeper/etcd 的 CP 锁,或业务层幂等兜底(锁只是"尽量串行",最终靠幂等/版本号兜底)。

7. 高可用架构

主从复制:主写从读,异步复制(可能丢少量数据)
哨兵 Sentinel:监控 + 自动故障转移(至少 3 个哨兵节点),客户端走哨兵发现主
Cluster 集群:16384 个 slot 按 CRC16(key) 分片,数据分布到多主,横向扩容
方案 容量 适用
主从+哨兵 单机内存上限 中小规模、结构简单
Cluster 水平扩展 大数据量、高写入

Cluster 注意事项:多 key 命令要求同 slot(用 hash tag {user1001}.cart);批量操作用 mget 需分组。

7.1、主从复制原理

  • 全量同步:从节点首次连主,发 PSYNC;主 bgsave 生成 RDB 发给从,从清空载入;期间主写命令进复制缓冲(repl_backlog),RDB 传完后再补发缓冲中的增量命令。
  • 增量同步(命令传播):全量后,主每执行写命令都异步发给从;从断线重连若 repl_backlog 还在窗口内,直接增量续传(PSYNC 带 offset),否则再全量。
  • 异步复制:主不等从落盘即返回客户端,主从有复制延迟(从 info replication 的 master_repl_offset vs 从 slave_repl_offset 差)。故障切换可能丢少量未同步数据。
  • 读写分离:从只读(replica-read-only yes),分担读;但从数据延迟——写后立即读从可能读不到(读写分离的一致性坑),重要读走主或加重试。

7.2、哨兵(Sentinel)原理

  • 监控:哨兵定时 PING 主从,超时(主观下线 sdown)标记;多个哨兵确认主不可达达 quorum → 客观下线(odown)。
  • 选举与故障转移:哨兵间用 Raft 类协商选"领头哨兵",领头从从节点里挑一个(优先级/复制偏移最新/runid 小)提升为主(SLAVEOF NO ONE),其他从改认新主,通知客户端。
  • 部署:至少 3 个哨兵节点(奇数,避免脑裂),且分散不同机器。客户端通过哨兵获取当前主节点地址,主切换自动感知。
  • 缺点:仍受单机内存上限;在线扩容麻烦(Cluster 才解决)。

7.3、Cluster 槽位机制(分片核心)

  • 16384 个 slot:Redis Cluster 把 key 空间分成 16384 个哈希槽。每个主节点负责一部分 slot(如 3 主各 ~5461 个)。
  • 定位 key:slot = CRC16(key) & 16383(哈希算法);客户端算 slot 直连对应节点。命令 CLUSTER KEYSLOT key 查看。
  • 请求路由:客户端连任意节点,若 key 的 slot 不归该节点,返回 MOVED <slot> <node> 重定向;迁移中返回 ASK 临时重定向。智能客户端( Lettuce/JedisCluster/Redisson)缓存 slot→node 映射,少走重定向。
  • 扩容/缩容:用 redis-cli --cluster reshard 把部分 slot 从旧节点迁到新节点(迁移中 key 用 ASK 引流),在线完成,无需停服。
  • 多 key 约束:跨 slot 的命令(如 MGET、事务)默认报错。解决:
    • Hash Tag:用 {...} 包裹决定 slot 的部分,{user1001}.cart 和 {user1001}.order 同 slot,可一起操作;
    • 批量命令按 slot 分组后分别发。
  • 高可用:每个主配若干从,主挂由 Cluster 自动故障转移(类似哨兵但内建)。
  • 限制:不支持多 key 跨 slot 事务、Lua 脚本里 key 必须同 slot(用 hash tag);最大 1000 节点左右;DB 只有 0(Cluster 禁用 select)。
# 创建集群(3 主 3 从)
redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 \
  127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 --cluster-replicas 1

# 查看 slot 分布
redis-cli -p 7000 cluster slots
# 迁移 slot(reshard 交互/参数)
redis-cli --cluster reshard 127.0.0.1:7000 --cluster-from <nodeA> \
  --cluster-to <nodeB> --cluster-slots 1000 --cluster-yes

8. Spring Boot 整合

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory cf) {
        RedisTemplate<String, Object> tpl = new RedisTemplate<>();
        tpl.setConnectionFactory(cf);
        tpl.setKeySerializer(new StringRedisSerializer());
        tpl.setHashKeySerializer(new StringRedisSerializer());
        tpl.setValueSerializer(new GenericJackson2JsonRedisSerializer());   // JSON 存储
        tpl.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        return tpl;
    }
}

建议再包一层 CacheService(封装 getOrLoad、TTL 随机、key 规范 业务:对象:id),避免业务代码直接拼 Redis 命令。

整合踩坑

  • key 序列化:用 StringRedisSerializer 让 key 可读;value 用 JSON(注意反序列化类型、循环引用、LocalDateTime 需自定义序列化器)。别用默认 JdkSerializationRedisSerializer(二进制、人看不懂、跨语言不兼容)。
  • 连接池:生产用 Lettuce(spring-boot-starter-data-redis 默认)+ 配置 lettuce.pool,高并发避免每次新建连接。
  • 事务/管道:RedisTemplate.executePipelined(...) 批处理;多命令合并减少 RTT(详见 11 篇 pipeline)。
  • 发布订阅 / Stream:RedisMessageListenerContainer 监听频道;StreamMessageListenerContainer 消费 Stream 消费组。
  • 缓存注解:@Cacheable / @CachePut / @CacheEvict 配 RedisCacheManager,注意 缓存穿透——@Cacheable 不缓存 null 默认(可 cacheNullValues=true)防穿透但会存空。

9. 小结

主题 要点
底层编码 SDS、listpack、intset、quicklist、skiplist+dict、dict 渐进式 rehash
持久化 RDB 快照快、AOF 安全、混合持久化首选
过期淘汰 惰性+定期删除;LRU/LFU 按场景选
三大问题 穿透(空值/BF)、击穿(互斥锁)、雪崩(TTL随机+高可用)
分布式锁 SET NX EX + 唯一value + Lua 释放 + 看门狗续期
高可用 主从复制、哨兵选主、Cluster 16384 slot 分片
整合 序列化用 String+JSON、连接池、CacheService 封装

本篇讲原理与架构,第 11 篇《Redis 整理笔记》补充命令实战、应用场景、Pipeline/Lua、BigKey 排查。

posted @ 2026-09-19 05:40  Stellan7  阅读(11)  评论(0)    收藏  举报