← 返回文章列表

Redis核心技术解析:从持久化到分布式锁

深入解析Redis持久化机制、缓存三大问题(穿透/击穿/雪崩)、高可用架构、双写一致性以及布隆过滤器和分布式锁的实战应用。

29 分钟阅读
字号

Redis核心技术解析:从持久化到分布式锁

Redis作为互联网架构中不可或缺的组件,几乎出现在了所有高并发系统的设计中。但很多开发者对Redis的理解仅停留在"缓存"层面,忽略了其背后的技术深度。本文将系统梳理Redis的核心技术栈,帮你建立完整的知识体系。

一、持久化机制:RDB与AOF

Redis提供了两种主要的持久化机制,各有优劣,适用于不同场景。

1.1 RDB(快照)

RDB会在指定的时间间隔内生成内存数据的快照,保存到磁盘上的一个二进制文件(dump.rdb)。

触发方式有两种

# 1. 配置文件触发(时间+次数双条件,满足任一即触发)
save 900 1      # 900秒内至少1次写操作
save 300 10     # 300秒内至少10次写操作  
save 60 10000   # 60秒内至少10000次写操作
 
# 2. 手动触发
BGSAVE  # 后台异步执行,不阻塞主线程
SAVE    # 同步执行,会阻塞(生产环境禁用)

RDB的优缺点

优点缺点
全量备份,文件小,适合备份可能丢失最后一次快照后的数据
恢复速度快fork进程有短暂阻塞
适合灾难恢复

1.2 AOF(日志)

AOF会记录每次写操作的命令,Redis重启时通过重放这些命令来恢复数据。

# 三种同步策略
appendfsync always     # 每个写命令都fsync,数据最安全(不推荐,性能差)
appendfsync everysec    # 每秒fsync一次(默认,推荐)
appendfsync no         # 由OS决定何时fsync(最快,但不安全)

AOF的重写机制

随着命令不断写入AOF,文件会越来越大。AOF重写会压缩文件,只保留最终的命令:

# 手动触发
BGREWRITEAOF
 
# 自动触发(根据文件大小)
auto-aof-rewrite-percentage 100  # 文件大小达到上次重写的200%时触发
auto-aof-rewrite-min-size 64mb  # 文件至少达到64MB才触发

AOF的优缺点

优点缺点
数据安全性高(everysec模式最多丢1秒数据)文件比RDB大
写是追加操作,性能较好恢复速度比RDB慢

1.3 两者结合使用

# 生产环境的最佳实践
appendonly yes                    # 开启AOF
appendfsync everysec              # 每秒同步
rdbchecksum yes                   # 开启RDB校验
dbfilename dump.rdb               # RDB文件名
dir /var/lib/redis               # 数据目录

Redis重启时优先加载AOF(数据更完整),如果AOF不存在才加载RDB。


二、缓存三大问题:穿透、击穿、雪崩

这三个问题是缓存体系在不同维度上的失效,理解它们的区别和解决方案是面试和实战的重中之重。

2.1 缓存穿透

定义:查询不存在的数据,导致请求直接打到数据库上。

场景举例:恶意用户不断查询ID=-1、ID=不存在的数据。

危害:大量不存在的数据请求打到DB,DB压力剧增。

解决方案

方案1:参数校验

def get_product(product_id):
    # 最前置的防护,拦截明显非法参数
    if product_id <= 0:
        return None
    # 继续业务逻辑...

方案2:空值缓存

def get_product(product_id):
    cache_key = f"product:{product_id}"
    
    # 先查缓存
    result = redis.get(cache_key)
    if result is not None:
        if result == "NULL":  # 空值标记
            return None
        return json.loads(result)
    
    # 查数据库
    product = db.query(f"SELECT * FROM products WHERE id={product_id}")
    
    if product:
        redis.setex(cache_key, 3600, json.dumps(product))
    else:
        # 关键:空值也要缓存,设置较短过期时间
        redis.setex(cache_key, 60, "NULL")
    
    return product

方案3:布隆过滤器

这是最优雅的方案,后文会详细展开。

2.2 缓存击穿

定义:某个热点Key过期的瞬间,大量请求击穿到数据库。

场景举例:商品详情页,某爆款商品突然有大量并发请求,而这个商品的缓存恰好过期。

危害:大量请求同时打DB,形成瞬间峰值,可能导致数据库崩溃。

解决方案

方案1:分布式锁

def get_product(product_id):
    cache_key = f"product:{product_id}"
    lock_key = f"lock:product:{product_id}"
    
    # 先查缓存
    result = redis.get(cache_key)
    if result:
        return json.loads(result)
    
    # 获取锁,只允许一个线程查DB
    lock = redis.pubsub("channel:{lock_key}")
    
    if redis.set(lock_key, "1", nx=True, ex=10):
        # 成功获取锁,查DB
        product = db.query(f"SELECT * FROM products WHERE id={product_id}")
        redis.setex(cache_key, 3600, json.dumps(product))
        redis.delete(lock_key)
        return product
    else:
        # 没拿到锁,短暂等待后重试
        import time
        time.sleep(0.1)
        return get_product(product_id)

方案2:热点数据永不过期

# 更新策略改为主动更新,不依赖过期时间
def update_product_cache(product_id):
    # 监听数据变更事件,主动更新缓存
    product = db.query(f"SELECT * FROM products WHERE id={product_id}")
    redis.set(f"product:{product_id}", json.dumps(product))

2.3 缓存雪崩

定义:大量Key同时过期,或者Redis服务宕机,导致所有请求打到数据库。

场景举例

  • 大量缓存设置了相同的过期时间(整点过期)
  • Redis主节点宕机,从节点顶上但也没有缓存

解决方案

方案1:过期时间差异化

# 不要统一设置3600秒
# 错误
redis.setex("product:{id}", 3600, data)
 
# 正确:加随机偏移
expire = 3600 + random.randint(0, 300)  # 1小时~1小时05分随机
redis.setex("product:{id}", expire, data)

方案2:Redis高可用

主从 + 哨兵/集群
确保单节点宕机不影响服务

方案3:服务降级/限流

# 缓存雪崩时,保护数据库
def get_product_with_fallback(product_id):
    try:
        return get_product(product_id)
    except:
        # 降级:直接查DB(牺牲性能保证可用)
        return db.query(f"SELECT * FROM products WHERE id={product_id}")

2.4 三者对比

问题本质解决方案
穿透查无此数据布隆过滤器、空值缓存
击穿一个热点失效分布式锁、永不过期
雪崩大量/全部失效过期差异化、高可用、限流

三、Redis高可用架构

Redis的高可用架构经历了三个阶段的演进:主从复制 → 哨兵模式 → 集群模式。

3.1 主从复制

┌─────────┐         ┌─────────┐
│  Master │ ──────→ │ Slave1  │  主负责写,从负责读
└─────────┘         └─────────┘
       │                   │
       └───────→ Slave2 ───┘

配置方法

# 在从节点配置
replicaof 192.168.1.100 6379
 
# 或者在配置文件
replicaof <master-ip> <master-port>
replica-read-only yes  # 从节点只读

复制原理

1. 全量同步:Slave连接Master,发送PSYNC命令
2. Master fork子进程生成RDB快照,发送给Slave
3. Slave加载RDB,完成全量复制
4. 增量同步:Master后续的写命令通过replication buffer发送给Slave

存在的问题

  • 主从延迟
  • 主节点宕机需要手动切换,不具备高可用
  • 所有从节点数据相同,内存利用率低

3.2 哨兵模式

在主从基础上增加哨兵进程,监控主从节点的健康状态,自动完成故障切换。

┌─────────┐
│ Sentinel│ ←── 监控 + 自动切换
└────┬────┘

┌────┴────┐         ┌─────────┐
│  Master │ ──────→ │ Slave1  │
└─────────┘         └─────────┘

                    ┌──────┴──────┐
                    │  Sentinel   │
                    └─────────────┘

配置

# sentinel.conf
sentinel monitor mymaster 192.168.1.100 6379 2
#                   集群名    主节点IP  端口  投票阈值
 
sentinel down-after-milliseconds mymaster 30000
# 主观下线时间(30秒没响应认为下线)
 
sentinel failover-timeout mymaster 180000
# 故障转移超时时间

自动故障切换流程

1. 主观下线:Sentinel检测到Master无响应
2. 客观下线:多个Sentinel达成共识
3. 选举Leader:由Sentinel投票选出一个Leader
4. 故障转移:Leader选择一个最优Slave晋升为Master
5. 通知应用:更新配置,主从关系变更

解决的问题:主节点宕机可自动切换,具备基本高可用。

遗留问题

  • 每台机器数据一样,内存利用率低
  • 不支持在线扩容
  • 写操作仍然只有单点

3.3 集群模式(Redis Cluster)

Redis 3.0引入,实现了分布式存储,每个节点存储不同的数据。

┌─────────────────────────────────────────────────┐
│                   Redis Cluster                  │
│                                                  │
│    Slot 0-5460      Slot 5461-10922   Slot 10923-16383  │
│  ┌───────────┐    ┌───────────┐    ┌───────────┐  │
│  │  Master1  │    │  Master2  │    │  Master3  │  │
│  │  (主)     │    │  (主)     │    │  (主)     │  │
│  └─────┬─────┘    └─────┬─────┘    └─────┬─────┘  │
│        │                │                │        │
│  ┌─────┴─────┐    ┌─────┴─────┐    ┌─────┴─────┐  │
│  │  Slave1   │    │  Slave2   │    │  Slave3   │  │
│  │  (从)     │    │  (从)     │    │  (从)     │  │
│  └───────────┘    └───────────┘    └───────────┘  │
└─────────────────────────────────────────────────┘

数据分片原理

Redis集群没有使用一致性哈希,而是引入了**哈希槽(Hash Slot)**的概念。

  • 共16384个槽位
  • 每个Key通过CRC16校验后对16384取模,决定放置哪个槽
  • 每个Master节点负责一部分哈希槽
# 计算key应该落在哪个槽
CRC16("user:10086") % 16384 = 5662  # 槽5662
 
# 查看集群槽信息
redis-cli cluster slots

为什么是16384个槽?

这是Redis作者Antirez权衡后的选择:

  • 心跳包需要携带所有槽位信息
  • 每个槽用1bit标识,16384个槽 = 2KB
  • 如果用65536个槽,心跳包变成8KB,太大了
  • 16384是2的14次方,方便位运算

集群的特点

特性说明
数据分片每个节点存储不同的数据
高可用每个分片都是主从结构
自动故障转移内置哨兵逻辑
在线扩容支持reshard数据迁移
客户端路由客户端访问正确的节点

四、一致性哈希

在讨论Redis集群时,不可避免地要提到一致性哈希算法,理解它有助于理解Redis集群的分片策略。

4.1 传统哈希的问题

# 传统取模哈希
hash(key) % node_count
 
# 问题:节点数量变化时,所有映射关系都会改变
# 例如:3个节点变成4个节点
# 几乎所有key的映射都会变化,数据大量失效

4.2 一致性哈希的原理

将服务器节点和数据Key都映射到一个环形哈希空间上:
 
        0

        │          节点A(0°)
        │           ╱
  270°──┼─────────╱────────────────── 90°
        │        ╱       ↑
        │       ╱        │  数据Key落在这个区间
        │      ╱         │  就路由到节点A
        │     ╱          │
        │    ╱           │
  180°──┼──╱────────────┴───────────────
        │        节点B(180°)


        360°/0°

数据定位规则:Key顺时针找到的第一个节点,就是它所属的节点。

4.3 虚拟节点

物理节点只有3个,但数据分布可能极不均匀:
- 节点A可能承载60%的数据
- 节点B、C各承载20%
 
解决:引入虚拟节点
 
物理节点:A、B、C
虚拟节点:A#1、A#2、A#3、B#1、B#2、B#3、C#1、C#2、C#3
 
虚拟节点映射到环上:
- A#1(30°), A#2(120°), A#3(250°)
- B#1(80°), B#2(150°), B#3(300°)
- ...
 
数据key落在节点A#2和B#1之间 → 路由到B#1(实际节点B)

虚拟节点的作用:让数据在物理节点上分布得更均匀。

4.4 Redis Cluster vs 一致性哈希

特性Redis Cluster(哈希槽)一致性哈希
槽数量16384(固定)2^32(理论上无限)
扩容手动重新分配槽自动映射
实现复杂度中等较高
数据迁移按槽迁移按key迁移
应用Redis官方集群方案很多中间件使用

五、Redis为什么这么快

这是面试中的高频问题,Redis之所以能达到每秒十万级QPS,原因主要有以下几点。

5.1 纯内存操作

Redis的所有数据都存储在内存中,内存的访问延迟在纳秒级别:

# 内存 vs 磁盘延迟对比
内存访问:~100纳秒
SSD磁盘:~100微秒
机械硬盘:~10毫秒
 
内存比磁盘快 100-100000

5.2 高效的数据结构

Redis定义了多种高效的数据结构,每种操作都是O(1)或O(k):

# O(1) 操作
SET key value              # 写入
GET key                    # 读取
HSET key field value       # 哈希写入
HGET key field             # 哈希读取
 
# O(k) 操作(k是参与操作的元素数量)
LPUSH key v1 v2 v3         # 列表头插,k=3
LRANGE key 0 9             # 范围查询,k=10
 
# O(n) 操作要慎用
KEYS pattern               # 全量扫描,禁止在生产使用
SCAN cursor                # 渐进式扫描,安全
FLUSHALL                   # 清空数据库,禁止随便用

5.3 单线程 + IO多路复用

Redis 6.0之前是单线程模型,6.0之后部分多线程化。

单线程模型的优势:
- 没有线程切换开销
- 没有锁竞争
- 原子操作天然保证
 
IO多路复用(epoll/kqueue/select):
- 一个线程处理多个客户端连接
- "事件发生时通知我"而非"主动轮询"
- epoll在Linux下效率极高
 
Redis 6.0多线程IO:
- 主线程处理命令执行
- 辅助线程处理socket读写、解析
- 对客户端来说仍然是单线程感知

5.4 简洁高效的设计

- 内部编码优化:
  - 小列表用ziplist(内存紧凑)
  - 小集合用intset(整数数组)
  
- 文本协议(RESP):
  - 简单,易解析
  - 序列化/反序列化开销低

六、MySQL与Redis双写一致性

这是分布式系统中极具挑战的问题。CAP理论告诉我们:在分布式系统中,无法同时满足一致性、可用性和分区容忍性。对于高可用的分布式系统,分区容忍性是必须满足的,所以只能在可用性和一致性之间权衡。

6.1 四种同步方案

方案1:先更新数据库,再删除缓存

T1: 线程A更新数据库:UPDATE → 新值
T2: 线程A删除缓存:DEL cache
T3: 线程B查询:cache miss → 读DB → 新值 → 写入缓存
 
结果:最终一致 ✓

问题场景(需要"读写并发"才会出现):

T0: 初始状态,缓存miss
 
T1: 线程A查询:cache miss → 读DB → 旧值(还没写缓存)
T2: 线程B更新DB → 新值
T3: 线程B删除缓存
T4: 线程A把旧值写入缓存  ← 覆盖了!
 
结果:缓存旧值,DB新值,不一致

这个场景的概率很低:必须是cache miss的读,且必须在更新前开始读取,并在删除之后才写完。

方案2:先删除缓存,再更新数据库

T1: 线程A删除缓存
T2: 线程B查询:cache miss → 读DB → 旧值
T3: 线程A更新数据库:新值
T4: 线程B把旧值写入缓存  ← 必然发生!
 
结果:必然不一致

需要用"延迟双删"弥补

def update_user(user_id, age):
    # 第一步:删缓存
    redis.delete(f"user:{user_id}")
    
    # 第二步:更新数据库
    db.execute(f"UPDATE users SET age={age} WHERE id={user_id}")
    
    # 第三步:延迟再删(等并发读完成)
    import time
    time.sleep(0.5)  # 等待500ms
    redis.delete(f"user:{user_id}")

方案3:删除缓存 + MQ重试

删除缓存可能失败(网络抖动、Redis暂时不可用),需要MQ保证最终删除成功:

# 应用层
def update_and_notify(user_id, age):
    db.execute(f"UPDATE users SET age={age} WHERE id={user_id}")
    # 发MQ消息,不同步删除
    mq.send("cache:delete", {"key": f"user:{user_id}", "retry": 0})
 
# MQ消费者
def consume(msg):
    key = msg["key"]
    if redis.delete(key):
        return  # 成功,ACK
    # 失败,重试
    if msg["retry"] < 3:
        msg["retry"] += 1
        mq.send("cache:delete", msg)
    else:
        log.error(f"删除缓存失败: {key}")

方案4:Canal订阅Binlog(最可靠)

MySQL → Binlog → Canal → MQ → Worker → 删除缓存
 
优点:
- 应用层代码几乎不用改
- 数据同步是准实时的
- 删除失败有MQ兜底重试

6.2 一致性方案对比

方案一致性复杂度性能影响推荐场景
先更DB再删缓存最终一致无额外延迟一般业务
先删缓存再双删最终一致一次额外延迟读多写少
先更DB+MQ重试准强一致核心业务
Canal订阅Binlog准强一致极高要求

6.3 为什么是删除而不是更新缓存

场景分析

问题:多表数据聚合后缓存
 
线程A:更新用户表年龄 age=30
线程B:更新订单表 count=100
 
如果用"更新缓存":
T1: 线程A读用户:{age:25},准备写缓存
T2: 线程B读订单:{count:100},写缓存 → {age:25, count:100}
T3: 线程A写缓存:{age:30, count:旧值} ← 覆盖了订单数!
 
用"删除缓存":
T1: 线程A更新用户表
T2: 线程A删除聚合缓存
T3: 下次查询重新聚合所有数据
 
删除比更新更安全,更简单的语义

七、布隆过滤器

布隆过滤器是解决缓存穿透的利器,理解它有助于应对高并发系统设计。

7.1 什么是布隆过滤器

布隆过滤器是一种空间效率极高的概率型数据结构,核心特点:

  • 不存在不误判:如果布隆过滤器说"不存在",那一定不存在
  • 存在可能误判:如果布隆过滤器说"存在",可能不存在(误判率可控制)
经典比喻:100亿个商品ID去重
 
普通做法:HashSet → 几百GB内存
布隆过滤器:~1GB内存,但有0.01%误判率
 
0.01%误判率意味着:
- 100亿次查询中,约100万次误判
- 误判后去DB查一次,保证最终正确
- DB查询压力只增加0.01%,但节省99%+内存

7.2 底层原理

bit数组(初始全0) + k个hash函数
 
添加"google":
- hash1("google") = 2  → bit[2] = 1
- hash2("google") = 5  → bit[5] = 1
- hash3("google") = 8  → bit[8] = 1
 
查询"google":
- 三个位置都是1 → 可能存在
 
查询"facebook":
- hash1 = 2(是1)
- hash2 = 7(是1)
- hash3 = 9(是1)→ 可能存在
 
查询"orange":
- hash1 = 2(是1)
- hash2 = 6(是0)→ 一定不存在!

7.3 为什么不能删除

假设:
- 添加"google" → bit[2,5,8] = 1
- 添加"facebook" → bit[2,7,9] = 1
 
删除"google" → bit[2,5,8] = 0
 
再查"facebook":
- hash1 = 2 → bit[2] = 0 → 判断不存在
- 但"facebook"明明存在!
 
原因:bit[2]被google和facebook共用
删除google时把facebook的hash1位置也清掉了

解决方案

  • 计数布隆过滤器(支持计数)
  • 布谷鸟过滤器(Redis 6.2+支持删除)
  • 用Redis Set记录已删除的ID

7.4 Redis布隆过滤器使用

# 添加元素
BF.ADD myfilter "apple"
BF.MADD myfilter "banana" "orange"
 
# 查询元素
BF.EXISTS myfilter "apple"    # 1(存在)
BF.EXISTS myfilter "watermelon" # 0(不存在)
 
# 创建自定义参数的过滤器
BF.RESERVE myfilter 0.01 1000000
# 参数:误判率1%,预计100万元素

7.5 实战:缓存穿透防护

bf = RedisBloomFilter("product:bloom")
 
def get_product(product_id):
    pid = str(product_id)
    
    # 第一层:布隆过滤器(快速判断)
    if not bf.exists(pid):
        return None  # 一定不存在,不查DB
    
    # 第二层:Redis缓存
    cache = redis.get(f"product:{pid}")
    if cache:
        return json.loads(cache)
    
    # 第三层:DB
    product = db.query(f"SELECT * FROM products WHERE id={pid}")
    
    if product:
        redis.setex(f"product:{pid}", 3600, json.dumps(product))
        bf.add(pid)  # 同步添加
    else:
        # 关键:DB没有也要加入过滤器
        bf.add(pid)
    
    return product

八、分布式锁

分布式锁用于在多进程/多机器环境下对共享资源进行互斥访问,是分布式系统的基础原语。

8.1 分布式锁的五个要求

根据分布式锁专家Marc Rentinger的观点:

  1. 互斥性:同一时刻只能有一个客户端持有锁
  2. 不会死锁:即使客户端崩溃也能自动释放锁
  3. 可重入:同一个客户端可以多次获取锁
  4. 容错性:大部分Redis节点正常就能获取/释放锁
  5. 锁的时效性:锁要有过期时间,防止死锁

8.2 Redis实现分布式锁

基础版本(SETNX + EXPIRE)的问题

# 错误!不是原子操作
redis.setnx(lock_key, "1")
redis.expire(lock_key, 10)
# 如果setnx成功后expire执行前崩溃,锁永不释放

正确版本(SET原子操作)

def acquire(key, expire_seconds=10):
    lock_key = f"lock:{key}"
    # SET + NX + EX 是原子操作
    result = redis.set(lock_key, "1", nx=True, ex=expire_seconds)
    return result is not None
 
def release(key):
    redis.delete(f"lock:{key}")

防误删版本(唯一token + Lua)

import uuid
 
def acquire(key, expire_seconds=10):
    lock_key = f"lock:{key}"
    token = str(uuid.uuid4())
    
    result = redis.set(lock_key, token, nx=True, ex=expire_seconds)
    if result:
        return token
    return None
 
def release(key, token):
    # Lua脚本保证原子性:检查token匹配后再删除
    lua = """
    if redis.call("get", KEYS[1]) == ARGV[1] then
        return redis.call("del", KEYS[1])
    else
        return 0
    end
    """
    redis.eval(lua, 1, f"lock:{key}", token)

8.3 Redisson工业级实现

Redisson是Java中最好的分布式锁实现,它的核心是看门狗机制

// 看门狗原理:
// 1. 锁默认leaseTime=30秒
// 2. 每隔10秒,自动续期30秒
// 3. 只要持有锁的进程还活着,锁就不会过期
// 4. 进程崩溃 → 不续期 → 锁30秒后自动释放
 
RLock lock = redisson.getLock("order:12345");
try {
    // tryLock(等待时间, 锁定时间, 时间单位)
    // 如果leaseTime=-1,使用看门狗机制
    boolean acquired = lock.tryLock(10, 30, TimeUnit.SECONDS);
    
    if (acquired) {
        // 业务逻辑
    }
} finally {
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

8.4 主从切换问题与RedLock

问题:
1. 客户端A获取master的锁
2. master挂掉,slave晋升为master
3. 新master没有锁信息
4. 客户端B也能获取锁成功
 
解决方案:RedLock算法
- 5个独立Redis节点
- 超过半数节点获取成功才算成功
- 即使2个节点故障,锁仍然有效

8.5 使用场景

库存扣减(防止超卖)

RLock lock = redisson.getLock("stock:" + productId);
try {
    lock.lock(30, TimeUnit.SECONDS);
    
    int stock = Integer.parseInt(redis.get("stock:" + productId));
    if (stock < quantity) {
        throw new Exception("库存不足");
    }
    redis.decrby("stock:" + productId, quantity);
} finally {
    lock.unlock();
}

定时任务幂等

RLock lock = redisson.getLock("task:dailyReport");
if (lock.tryLock()) {
    try {
        generateDailyReport();
    } finally {
        lock.unlock();
    }
}
// 其他节点跳过执行

九、Redis大Key问题

大Key是生产环境中常见的问题,指Value体积过大或集合元素过多的Key。

9.1 什么是大Key

类型阈值
StringValue > 10KB
Hash/List/Set/ZSet元素数量 > 10000

但阈值是死的,业务场景才是活的。合理的大Key有时比拆分更优(比如1MB二进制数据)。

9.2 大Key的危害

危害说明
内存膨胀每个key有元数据开销,碎片率飙升
单线程阻塞O(n)命令(如HGETALL)会阻塞秒级
带宽打满10MB的String读取耗尽网卡
持久化阻塞RDB fork时copy-on-write压力大
集群倾斜热点key撑爆单节点

9.3 排查方法

# 快速扫描
redis-cli --bigkeys
 
# 精确排查
redis-cli --scan | head -1000 | while read key; do
    echo "$(redis-cli MEMORY USAGE "$key") $key"
done | sort -rn | head -20
 
# 查看内存碎片率
redis-cli INFO memory | grep mem_fragmentation_ratio
# 超过1.5需要关注,超过1.8需要重启修复

9.4 处理方案

拆分(核心思路)

# 原来:一个大Hash存用户画像
user:10086:profile → 100万个field
 
# 优化后:按业务维度拆分
user:10086:basic      → 基本信息(100字段
user:10086:behavior   → 行为数据(1000字段
user:10086:social     → 社交数据(500字段

分桶存储

# 原来:一个关注列表1百万粉丝
user:10086:followers
 
# 优化后:分桶
user:10086:followers:00-9999
user:10086:followers:110000-19999
user:10086:followers:220000-29999

异步删除

# DEL会阻塞,用UNLINK异步删除
redis.unlink("large:key")

总结

Redis的核心技术远不止缓存这么简单:

  • 持久化:RDB+AOF,各有适用场景
  • 高可用:主从 → 哨兵 → 集群,逐级演进
  • 缓存问题:穿透/击穿/雪崩,方案各有侧重
  • 双写一致性:没有完美方案,"先更DB再删缓存+MQ重试"是均衡选择
  • 布隆过滤器:空间换时间,解决缓存穿透
  • 分布式锁:SET+唯一token+看门狗,工业级实现用Redisson
  • 大Key治理:拆分是核心,但合理的大Key不必强行拆

最后记住:Redis是工具,不是银弹。理解每种技术的适用场景和局限性,才能在架构设计中做出正确的选择。


本文对你有帮助吗?欢迎留言讨论。

分享
0%