Redis核心技术解析:从持久化到分布式锁
深入解析Redis持久化机制、缓存三大问题(穿透/击穿/雪崩)、高可用架构、双写一致性以及布隆过滤器和分布式锁的实战应用。
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的观点:
- 互斥性:同一时刻只能有一个客户端持有锁
- 不会死锁:即使客户端崩溃也能自动释放锁
- 可重入:同一个客户端可以多次获取锁
- 容错性:大部分Redis节点正常就能获取/释放锁
- 锁的时效性:锁要有过期时间,防止死锁
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
| 类型 | 阈值 |
|---|---|
| String | Value > 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:0 → 0-9999
user:10086:followers:1 → 10000-19999
user:10086:followers:2 → 20000-29999异步删除:
# DEL会阻塞,用UNLINK异步删除
redis.unlink("large:key")总结
Redis的核心技术远不止缓存这么简单:
- 持久化:RDB+AOF,各有适用场景
- 高可用:主从 → 哨兵 → 集群,逐级演进
- 缓存问题:穿透/击穿/雪崩,方案各有侧重
- 双写一致性:没有完美方案,"先更DB再删缓存+MQ重试"是均衡选择
- 布隆过滤器:空间换时间,解决缓存穿透
- 分布式锁:SET+唯一token+看门狗,工业级实现用Redisson
- 大Key治理:拆分是核心,但合理的大Key不必强行拆
最后记住:Redis是工具,不是银弹。理解每种技术的适用场景和局限性,才能在架构设计中做出正确的选择。
本文对你有帮助吗?欢迎留言讨论。