前言
💡 痛点: Redis 内存泄漏?缓存穿透/击穿/雪崩全中?集群选型头疼?Lua 脚本分布式锁踩坑?
🎯 解决方案: 从数据类型→持久化→集群→缓存策略→安全认证,系统掌握 Redis 7 分布式缓存。
#mermaid-svg-67e8NF4f8yf8CiOu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-67e8NF4f8yf8CiOu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-67e8NF4f8yf8CiOu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-67e8NF4f8yf8CiOu .error-icon{fill:#552222;}#mermaid-svg-67e8NF4f8yf8CiOu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-67e8NF4f8yf8CiOu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-67e8NF4f8yf8CiOu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-67e8NF4f8yf8CiOu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-67e8NF4f8yf8CiOu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-67e8NF4f8yf8CiOu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-67e8NF4f8yf8CiOu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-67e8NF4f8yf8CiOu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-67e8NF4f8yf8CiOu .marker.cross{stroke:#333333;}#mermaid-svg-67e8NF4f8yf8CiOu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-67e8NF4f8yf8CiOu p{margin:0;}#mermaid-svg-67e8NF4f8yf8CiOu .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-67e8NF4f8yf8CiOu .cluster-label text{fill:#333;}#mermaid-svg-67e8NF4f8yf8CiOu .cluster-label span{color:#333;}#mermaid-svg-67e8NF4f8yf8CiOu .cluster-label span p{background-color:transparent;}#mermaid-svg-67e8NF4f8yf8CiOu .label text,#mermaid-svg-67e8NF4f8yf8CiOu span{fill:#333;color:#333;}#mermaid-svg-67e8NF4f8yf8CiOu .node rect,#mermaid-svg-67e8NF4f8yf8CiOu .node circle,#mermaid-svg-67e8NF4f8yf8CiOu .node ellipse,#mermaid-svg-67e8NF4f8yf8CiOu .node polygon,#mermaid-svg-67e8NF4f8yf8CiOu .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-67e8NF4f8yf8CiOu .rough-node .label text,#mermaid-svg-67e8NF4f8yf8CiOu .node .label text,#mermaid-svg-67e8NF4f8yf8CiOu .image-shape .label,#mermaid-svg-67e8NF4f8yf8CiOu .icon-shape .label{text-anchor:middle;}#mermaid-svg-67e8NF4f8yf8CiOu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-67e8NF4f8yf8CiOu .rough-node .label,#mermaid-svg-67e8NF4f8yf8CiOu .node .label,#mermaid-svg-67e8NF4f8yf8CiOu .image-shape .label,#mermaid-svg-67e8NF4f8yf8CiOu .icon-shape .label{text-align:center;}#mermaid-svg-67e8NF4f8yf8CiOu .node.clickable{cursor:pointer;}#mermaid-svg-67e8NF4f8yf8CiOu .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-67e8NF4f8yf8CiOu .arrowheadPath{fill:#333333;}#mermaid-svg-67e8NF4f8yf8CiOu .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-67e8NF4f8yf8CiOu .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-67e8NF4f8yf8CiOu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-67e8NF4f8yf8CiOu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-67e8NF4f8yf8CiOu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-67e8NF4f8yf8CiOu .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-67e8NF4f8yf8CiOu .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-67e8NF4f8yf8CiOu .cluster text{fill:#333;}#mermaid-svg-67e8NF4f8yf8CiOu .cluster span{color:#333;}#mermaid-svg-67e8NF4f8yf8CiOu div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-67e8NF4f8yf8CiOu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-67e8NF4f8yf8CiOu rect.text{fill:none;stroke-width:0;}#mermaid-svg-67e8NF4f8yf8CiOu .icon-shape,#mermaid-svg-67e8NF4f8yf8CiOu .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-67e8NF4f8yf8CiOu .icon-shape p,#mermaid-svg-67e8NF4f8yf8CiOu .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-67e8NF4f8yf8CiOu .icon-shape .label rect,#mermaid-svg-67e8NF4f8yf8CiOu .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-67e8NF4f8yf8CiOu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-67e8NF4f8yf8CiOu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-67e8NF4f8yf8CiOu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Redis Cluster
客户端
主节点组
Gossip 协议
SET/GET
SET/GET
SET/GET
Application
Master 1
6380
Replica 1
6381
Master 2
6382
Replica 2
6383
Master 3
6384
Replica 3
6385
Redis 7 新特性速览:
| 特性 | 说明 | 性能/功能影响 |
|---|---|---|
| Redis Functions | 服务端 Lua 脚本升级版(取代_scripts_load) | 脚本可复用/版本管理 |
| ACL v2 | 细粒度权限控制(key patterns + commands) | 安全提升 |
| Sharded Pub/Sub | 分片发布订阅 | 多节点 Pub/Sub 支持 |
| MULTI/EXEC Transactions | Redis 7 支持 ACL 下的事务 | 原子性保障 |
| Cluster Rebalance API | 集群自动均衡 API | 运维自动化 |
| Command Restrictions | 按用户限制命令 | 防止误操作 |
一、数据类型与底层结构
1.1 五大基本类型
python
# ===== Redis 数据类型 =====
import redis
r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
# String(简单字符串)
r.set('user:1001:name', 'Alice')
r.set('user:1001:age', 28)
r.setnx('user:1001:lock', '1') # 不存在才设置(分布式锁基础)
r.setex('session:abc', 3600, 'data') # 1小时后过期
r.get('user:1001:name') # Alice
# Hash(用户对象)
r.hset('user:1001', mapping={
'name': 'Alice',
'age': '28',
'city': 'Beijing'
})
r.hget('user:1001', 'name') # Alice
r.hgetall('user:1001') # {'name': 'Alice', 'age': '28', ...}
r.hincrby('user:1001', 'age', 1) # age++
# List(消息队列)
r.lpush('queue:orders', 'order:1001', 'order:1002', 'order:1003')
r.rpop('queue:orders') # order:1001(FIFO)
r.lrange('queue:orders', 0, -1) # 全部元素
# Set(去重集合)
r.sadd('tags:python', 'aiohttp', 'fastapi', 'redis', 'aiohttp') # 自动去重
r.smembers('tags:python') # {'aiohttp', 'fastapi', 'redis'}
r.sinter('tags:python', 'tags:java') # 交集
# ZSet(排行榜)
r.zadd('ranking:2024', {'Alice': 95.5, 'Bob': 88.0, 'Charlie': 92.0})
r.zrevrange('ranking:2024', 0, 2, withscores=True) # Top 3
r.zrank('ranking:2024', 'Bob') # 获取排名
# ===== Bitmaps(用户签到)=====
# 每日签到 BITFIELD
key = f'checkin:user:1001:2024'
r.setbit(key, day_of_year, 1) # 第 N 天签到
checkin_count = r.bitcount(key) # 累计签到天数
r.bitfield(key, 'GET', f'u1 #{day_of_year}', 0) # 查询某天是否签到
# ===== HyperLogLog(UV 统计)=====
r.pfadd('uv:daily:2024-06-10', 'user:1001', 'user:1002', 'user:1003')
uv_count = r.pfcount('uv:daily:2024-06-10') # 近似去重 UV
# 内存:12KB,误差率 0.81%
# ===== Geospatial(附近的人)=====
r.geoadd('locations', (116.397128, 39.916527, 'user:1001')) # 添加位置
r.georadius('locations', 116.397128, 39.916527, 5, 'km', withdist=True) # 5km内用户
1.2 底层数据结构
c
/* ===== Redis Object 结构 =====
typedef struct redisObject {
unsigned type:4; // 类型(STRING/LIST/HASH/SET/ZSET)
unsigned encoding:4; // 编码方式
unsigned lru:LRU_BITS; // LRU 时钟(用于淘汰)
int refcount; // 引用计数
void *ptr; // 指向实际数据
} robj;
*/
/* ===== 编码类型 vs 数据结构 =====
┌──────────┬──────────────────────────┐
│ Encoding │ 底层数据结构 │
├──────────┼──────────────────────────┤
│ INT │ 整数(用 embstr/INT) │
│ EMBSTR │ 连续内存字符串(≤44字节) │
│ RAW │ 动态字符串 SDS │
│ ZIPLIST │ 压缩列表(<512元素) │
│ QUICKLIST│ LinkedList + ZipList 组合 │
│ HT │ 哈希表(Dict) │
│ INTSET │ 整数集合(纯整数 Set) │
│ SKIPLIST │ 跳表 + 哈希表(ZSet) │
│ LISTPACK │ 替代 ZIPLIST(Redis 7) │
└──────────┴──────────────────────────┘
*/
// ===== SDS 动态字符串 =====
/*
SDS(Simple Dynamic String)vs C 字符串:
C 字符串:char buf[32] = "hello"; // 固定长度,需手动管理
SDS:
struct __attribute__((packed)) sdshdr64 {
uint64_t len; // 已用长度
uint64_t alloc; // 分配长度(不含头部)
unsigned char flags; // 类型(sdshdr8/16/32/64)
char buf[]; // 柔性数组,存储实际字符串
};
优点:
- O(1) 获取长度(SDS.len)
- 空间预分配 + 惰性释放
- 二进制安全(可存 \0)
*/
// ===== Quicklist(List 类型默认编码)=====
/*
Quicklist = 多个 ZipList 组成的双向链表
[head] --> [ZipList1: item1,item2] --> [ZipList2: item3] --> [tail]
(每 ZipList ≤ 8KB)
特性:
- 空间紧凑(ZipList 压缩)
- 两端操作 O(1)(链表)
- 中间操作 O(n)(需遍历 ZipList)
*/
二、持久化机制
2.1 RDB 与 AOF
python
# ===== RDB(快照持久化)=====
# 触发方式
# 1. 手动触发
r.save() # 同步阻塞(生产慎用)
r.lastsave() # 返回上次保存时间戳
# 2. 异步后台保存
r.background_save() # BGSAVE(非阻塞)
# 3. 自动触发(配置)
# redis.conf:
# save 900 1 # 900秒内≥1次写操作 → BGSAVE
# save 300 10 # 300秒内≥10次写操作 → BGSAVE
# save 60 10000 # 60秒内≥10000次写操作 → BGSAVE
# RDB 优势:
# - 紧凑格式,适合备份/恢复(文件小)
# - 恢复大数据集快(fork 子进程生成 .rdb)
# RDB 劣势:
# - 最后一次 BGSAVE 后的数据丢失
# - fork 消耗内存(copy-on-write)
# ===== AOF(追加日志持久化)=====
# 开启 AOF
# redis.conf:
# appendonly yes
# appendfilename "appendonly.aof"
# appendfsync everysec # 推荐:每秒同步
# appendfsync 三种策略:
# - always: 每次写操作同步(最安全,最慢)
# - everysec: 每秒同步(默认,推荐)
# - no: 交给 OS 同步(最快,可能丢 1 秒数据)
# AOF 重写(压缩文件)
r.bgrewriteaof() # 异步压缩 AOF
# AOF vs RDB 对比:
# - RDB: 紧凑、快速恢复,但可能丢数据
# - AOF: 数据安全可调(always/everysec/no),文件大
2.2 混合持久化(推荐)
python
# ===== 混合持久化(Redis 4.0+)=====
# redis.conf:
# aof-use-rdb-preamble yes # 开启混合持久化
# 工作原理:
# 1. AOF 重写时,先以 RDB 格式写入文件开头
# 2. 后续增量操作以 AOF 格式追加
# 3. 恢复时:先加载 RDB 部分,再执行后续 AOF 命令
# 结果:
# - 文件开头是 RDB 格式(二进制,体积小)
# - 文件结尾是 AOF 格式(增量命令)
# - 恢复速度:RDB 快 + AOF 完整
# ===== 数据恢复流程 =====
# 1. 如果开启混合持久化,Redis 自动选择
# → 有 .aof 文件时,优先使用 AOF 恢复
# → 加载顺序:RDB 头部 → AOF 尾部
# 2. 手动指定恢复
# redis-server --rdb /path/to/dump.rdb
# redis-server --aof /path/to/appendonly.aof
三、缓存策略
3.1 三大缓存问题
python
# ===== 缓存穿透(数据不存在,但请求穿透到 DB)=====
# 问题:恶意请求大量不存在的数据,DB 压力剧增
# 解决1:布隆过滤器
from bloom_filter import BloomFilter
bloom = BloomFilter(max_elements=100000, error_rate=0.01)
def get_user(user_id):
if user_id not in bloom:
return None # 布隆过滤器说没有,直接返回
# 布隆过滤器说可能有,查 DB
user = db.query('SELECT * FROM users WHERE id = %s', user_id)
if not user:
bloom.add(user_id) # 空结果也加入,防止重复穿透
return user
# 解决2:缓存空值
def get_user_cached(user_id):
key = f'user:{user_id}'
cached = r.get(key)
if cached is None:
user = db.query('SELECT * FROM users WHERE id = %s', user_id)
if user:
r.setex(key, 3600, json.dumps(user))
else:
r.setex(key, 60, 'NULL') # 空值缓存(短期)
return user
return None if cached == 'NULL' else json.loads(cached)
# ===== 缓存击穿(热点 key 过期,瞬间大量请求打 DB)=====
# 问题:热点 key 过期瞬间,N 个请求同时穿透到 DB
# 解决1:互斥锁(单节点)
import redis
from contextlib import contextmanager
lock = redis.Redis(connection_pool=pool)
LOCK_KEY = 'lock:user:1001'
@contextmanager
def acquire_lock(lock_key, timeout=10):
while True:
if lock.set(LOCK_KEY, '1', nx=True, ex=timeout):
yield
lock.delete(LOCK_KEY)
break
time.sleep(0.01)
def get_user_anti_thundering(user_id):
key = f'user:{user_id}'
user = r.get(key)
if user:
return json.loads(user)
with acquire_lock(LOCK_KEY):
# Double Check:其他请求可能已加载
user = r.get(key)
if user:
return json.loads(user)
# 只有一个请求去查 DB
user = db.query('SELECT * FROM users WHERE id = %s', user_id)
if user:
r.setex(key, 3600, json.dumps(user))
return user
# 解决2:热点数据永不过期 + 后台异步更新
def get_user永不过期(user_id):
key = f'user:{user_id}'
user = r.get(key)
if not user:
user = db.query(...)
r.set(key, json.dumps(user))
return user
# 逻辑过期:检查是否快过期
data = json.loads(user)
if data['ttl'] < time.time() - 300: # 5分钟内过期
# 异步更新,不阻塞请求
threading.Thread(target=_refresh_user, args=(user_id,)).start()
return data
# ===== 缓存雪崩(大量 key 同时过期)=====
# 问题:大量 key 在同一时间过期,请求全部打 DB
# 解决1:随机过期时间
TTL_BASE = 3600
TTL_OFFSET = random.randint(0, 300) # 0-5分钟随机
r.setex(key, TTL_BASE + TTL_OFFSET, value)
# 解决2:多级缓存
# L1: 本地缓存(Caffeine/Guava) TTL=10s
# L2: Redis 缓存 TTL=1h
# L3: DB
# 解决3:Redis Cluster 主从 + 哨兵
# 即使主节点故障,从节点顶替
3.2 Cache Aside 模式
python
# ===== Cache Aside(旁路缓存,最常用)=====
"""
读流程:
1. 查缓存 → 命中 → 返回
2. 查缓存 → 未命中 → 查 DB → 写缓存 → 返回
写流程:
1. 写 DB
2. 删除缓存(不是更新缓存!避免并发问题)
"""
class CacheAsideService:
def __init__(self, redis_client, db_pool):
self.r = redis_client
self.db = db_pool
def get_user(self, user_id):
key = f'user:{user_id}'
# Step 1: 查缓存
cached = self.r.get(key)
if cached:
return json.loads(cached)
# Step 2: 查 DB
user = self.db.query(
'SELECT * FROM users WHERE id = %s', user_id
)
# Step 3: 写缓存(如果 DB 有数据)
if user:
self.r.setex(key, 3600, json.dumps(user))
return user
def update_user(self, user_id, data):
# Step 1: 写 DB
self.db.execute(
'UPDATE users SET ... WHERE id = %s', user_id, data
)
# Step 2: 删除缓存(不是更新!)
# 如果更新缓存:并发时可能出现旧数据覆盖
# 删除缓存:下次读会从 DB 加载最新数据
self.r.delete(f'user:{user_id}')
def delete_user(self, user_id):
# Step 1: 删 DB
self.db.execute('DELETE FROM users WHERE id = %s', user_id)
# Step 2: 删除缓存
self.r.delete(f'user:{user_id}')
四、Redis Sentinel 哨兵集群
4.1 哨兵原理
#mermaid-svg-PvZ4oBFIccvQnRkf{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PvZ4oBFIccvQnRkf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PvZ4oBFIccvQnRkf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PvZ4oBFIccvQnRkf .error-icon{fill:#552222;}#mermaid-svg-PvZ4oBFIccvQnRkf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PvZ4oBFIccvQnRkf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PvZ4oBFIccvQnRkf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PvZ4oBFIccvQnRkf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PvZ4oBFIccvQnRkf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PvZ4oBFIccvQnRkf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PvZ4oBFIccvQnRkf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PvZ4oBFIccvQnRkf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PvZ4oBFIccvQnRkf .marker.cross{stroke:#333333;}#mermaid-svg-PvZ4oBFIccvQnRkf svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PvZ4oBFIccvQnRkf p{margin:0;}#mermaid-svg-PvZ4oBFIccvQnRkf .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PvZ4oBFIccvQnRkf .cluster-label text{fill:#333;}#mermaid-svg-PvZ4oBFIccvQnRkf .cluster-label span{color:#333;}#mermaid-svg-PvZ4oBFIccvQnRkf .cluster-label span p{background-color:transparent;}#mermaid-svg-PvZ4oBFIccvQnRkf .label text,#mermaid-svg-PvZ4oBFIccvQnRkf span{fill:#333;color:#333;}#mermaid-svg-PvZ4oBFIccvQnRkf .node rect,#mermaid-svg-PvZ4oBFIccvQnRkf .node circle,#mermaid-svg-PvZ4oBFIccvQnRkf .node ellipse,#mermaid-svg-PvZ4oBFIccvQnRkf .node polygon,#mermaid-svg-PvZ4oBFIccvQnRkf .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PvZ4oBFIccvQnRkf .rough-node .label text,#mermaid-svg-PvZ4oBFIccvQnRkf .node .label text,#mermaid-svg-PvZ4oBFIccvQnRkf .image-shape .label,#mermaid-svg-PvZ4oBFIccvQnRkf .icon-shape .label{text-anchor:middle;}#mermaid-svg-PvZ4oBFIccvQnRkf .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PvZ4oBFIccvQnRkf .rough-node .label,#mermaid-svg-PvZ4oBFIccvQnRkf .node .label,#mermaid-svg-PvZ4oBFIccvQnRkf .image-shape .label,#mermaid-svg-PvZ4oBFIccvQnRkf .icon-shape .label{text-align:center;}#mermaid-svg-PvZ4oBFIccvQnRkf .node.clickable{cursor:pointer;}#mermaid-svg-PvZ4oBFIccvQnRkf .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PvZ4oBFIccvQnRkf .arrowheadPath{fill:#333333;}#mermaid-svg-PvZ4oBFIccvQnRkf .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PvZ4oBFIccvQnRkf .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PvZ4oBFIccvQnRkf .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PvZ4oBFIccvQnRkf .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PvZ4oBFIccvQnRkf .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PvZ4oBFIccvQnRkf .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PvZ4oBFIccvQnRkf .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PvZ4oBFIccvQnRkf .cluster text{fill:#333;}#mermaid-svg-PvZ4oBFIccvQnRkf .cluster span{color:#333;}#mermaid-svg-PvZ4oBFIccvQnRkf div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PvZ4oBFIccvQnRkf .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PvZ4oBFIccvQnRkf rect.text{fill:none;stroke-width:0;}#mermaid-svg-PvZ4oBFIccvQnRkf .icon-shape,#mermaid-svg-PvZ4oBFIccvQnRkf .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PvZ4oBFIccvQnRkf .icon-shape p,#mermaid-svg-PvZ4oBFIccvQnRkf .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PvZ4oBFIccvQnRkf .icon-shape .label rect,#mermaid-svg-PvZ4oBFIccvQnRkf .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PvZ4oBFIccvQnRkf .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PvZ4oBFIccvQnRkf .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PvZ4oBFIccvQnRkf :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 主从复制
哨兵集群
Leader 选举
主从复制
监控
sentinel://
Sentinel 1
26379
Sentinel 2
26380
Sentinel 3
26381
Master
6379
Replica 1
6380
Replica 2
6381
Application
python
# ===== 哨兵配置(sentinel.conf)=====
# sentinel.conf 关键配置
# 监控主节点(myMaster 是别名)
sentinel monitor myMaster 127.0.0.1 6379 2
# 参数:主节点IP 端口 投票阈值(≥2票认定故障)
# 主观下线时间(SDOWN)
sentinel down-after-milliseconds myMaster 30000
# 30秒无响应 → 主观下线
# 客观下线后故障转移超时
sentinel failover-timeout myMaster 180000
# 18秒内完成故障转移
# 故障转移后同步的新从节点数
sentinel parallel-syncs myMaster 1
# 每次只同步 1 个从节点(避免雪崩)
# ===== Python 连接哨兵 =====
from redis.sentinel import Sentinel
# 创建哨兵连接
sentinel = Sentinel(
[('localhost', 26379), ('localhost', 26380), ('localhost', 26381)],
socket_timeout=0.1
)
# 获取主节点(自动发现)
master = sentinel.master_for('myMaster', socket_timeout=0.1, decode_responses=True)
slave = sentinel.slave_for('myMaster', socket_timeout=0.1, decode_responses=True)
# 读从写主
def read_user(user_id):
return slave.get(f'user:{user_id}')
def write_user(user_id, data):
master.setex(f'user:{user_id}', 3600, json.dumps(data))
4.2 故障转移流程
python
# ===== 故障转移步骤 =====
"""
1. 主观下线(SDOWN)
- 哨兵每秒 ping 主节点
- 超过 down-after-milliseconds 无响应 → SDOWN
2. 客观下线(ODOWN)
- 多个哨兵投票(≥quorum 配置)
- 多数哨兵认为主节点 SDOWN → ODOWN
3. 哨兵Leader 选举
- 使用 Raft 算法选举 Leader
- 得票最多的哨兵负责故障转移
4. 故障转移
- Leader 哨兵选择一个从节点
- 选主标准:优先级 → 偏移量 → runid 小
- 执行 SLAVEOF NO ONE(新主节点)
- 通知其他从节点指向新主节点
- 更新配置(主从映射)
"""
五、Redis Cluster 集群
5.1 槽分区原理
python
# ===== Redis Cluster 槽分区 =====
"""
Redis Cluster 将 16384 个槽分配给 N 个主节点
每个节点负责 16384/N 个槽
数据路由:
key → CRC16(key) % 16384 → 槽 → 节点
槽分配示例(3 主 3 从):
Master 1: 槽 0-5460
Master 2: 槽 5461-10922
Master 3: 槽 10923-16383
"""
# ===== CRC16 哈希槽算法 =====
def hash_slot(key):
"""Redis Cluster 槽计算"""
key = str(key)
# 找第一个 {tag}
if '{' in key:
end = key.find('}')
if end > 0:
key = key[1:end]
return crc16(key) % 16384
# 所有相同 tag 的 key 会在同一个槽
# ✅ user:{1001}:profile → 槽 A(同一用户数据集中)
# ✅ user:{1001}:orders → 槽 A
# ❌ user:1001:profile → 随机槽(分散)
# ===== 创建集群 =====
# 使用 redis-cli 创建集群
# redis-cli --cluster create \
# 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6382 \
# --cluster-replicas 1
# # 6380/6381/6382 为主节点
# # 6383/6384/6385 为从节点(每主1从)
# ===== Python 连接集群 =====
from redis.cluster import RedisCluster
# 集群节点配置
rc = RedisCluster(
host='127.0.0.1',
port=6380,
skip_full_coverage_check=True, # 小集群跳过完整检查
decode_responses=True
)
# 自动路由:客户端知道槽分布
rc.set('user:1001', 'Alice') # 自动路由到正确节点
rc.get('user:1001')
# ===== MOVED 重定向 =====
# 客户端请求错误的节点
# Server 返回 MOVED 错误
"""
GET user:1001
-MOVED 12345 127.0.0.1:6382
# 12345 = 槽编号,6382 = 正确的节点
# smart 客户端自动重定向并缓存槽映射
"""
5.2 集群故障转移
python
# ===== 主从切换 =====
"""
1. 从节点检测主节点下线(主观下线)
2. 超过半数的从节点投票认定(客观下线)
3. 从节点发起故障转移
- 延迟选举(failover-timeout 内等待)
- 竞争master_offset 领先
- 获得票数后成为新主
4. 新主广播 PONG,其他节点更新拓扑
# ===== 集群扩缩容 =====
# 添加新主节点
redis-cli --cluster add-node 127.0.0.1:6390 127.0.0.1:6380
redis-cli --cluster reshard 127.0.0.1:6390 \
--cluster-to <new-node-id> \
--cluster-slots <num-slots> \
--cluster-from <source-node-id>
# 迁移槽时自动重定向
# MOVED <slot> <ip:port>
"""
六、分布式锁与 Lua 脚本
6.1 Redlock 算法
python
# ===== 单节点分布式锁 =====
import redis
import time
import uuid
class DistributedLock:
def __init__(self, redis_client, lock_name, timeout=30):
self.r = redis_client
self.lock_name = f'lock:{lock_name}'
self.timeout = timeout
self.token = str(uuid.uuid4()) # 唯一标识
def acquire(self, blocking=True, blocking_timeout=10):
"""获取锁"""
deadline = time.time() + blocking_timeout
while True:
# SET NX EX(原子操作)
if self.r.set(self.lock_name, self.token, nx=True, ex=self.timeout):
return True
if not blocking:
return False
if time.time() > deadline:
return False
time.sleep(0.01) # 避免 CPU 空转
def release(self):
"""释放锁(Lua 脚本保证原子性)"""
# ❌ 不安全:get 后 del(可能被其他进程持有)
# if self.r.get(self.lock_name) == self.token:
# self.r.delete(self.lock_name)
# ✅ 安全:Lua 脚本原子检查+删除
lua_script = """
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
"""
self.r.eval(lua_script, 1, self.lock_name, self.token)
def extend(self, add_time):
"""延长锁过期时间"""
lua_script = """
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("EXPIRE", KEYS[1], ARGV[2])
else
return 0
end
"""
return self.r.eval(lua_script, 1, self.lock_name, self.token, add_time)
# ===== 使用示例 =====
lock = DistributedLock(r, 'order:payment:1001', timeout=30)
if lock.acquire(blocking_timeout=5):
try:
# 业务逻辑
process_order(1001)
finally:
lock.release()
else:
print('获取锁失败')
# ===== Redlock(多节点,推荐生产使用)=====
import redis
from typing import List
class Redlock:
def __init__(self, nodes: List[dict], ttl=30000):
"""
nodes: [{'host': '127.0.0.1', 'port': 6380}, ...]
ttl: 锁过期时间(毫秒)
"""
self.nodes = [redis.Redis(**n) for n in nodes]
self.ttl = ttl
def lock(self, resource):
token = str(uuid.uuid4())
ttl_ms = self.ttl
# 在 N 个节点上顺序获取锁
acquired = 0
for node in self.nodes:
if node.set(f'lock:{resource}', token, nx=True, px=ttl_ms):
acquired += 1
# 超过 N/2+1 节点成功 = 获得锁
quorum = len(self.nodes) // 2 + 1
if acquired >= quorum:
return token
# 获取失败,释放所有节点
self.unlock(resource, token)
return None
def unlock(self, resource, token):
lua_script = """
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
"""
for node in self.nodes:
node.eval(lua_script, 1, f'lock:{resource}', token)
6.2 Redis Functions(Redis 7)
python
# ===== Redis Functions(Lua 脚本升级版)=====
# 注册 Lua 函数
r.function_load("""
#!lua name=rate_limiter
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = tonumber(redis.call('GET', key) or '0')
if current >= limit then
return 0
end
redis.call('INCR', key)
if current == 0 then
redis.call('PEXPIRE', key, window)
end
return redis.call('GET', key)
""")
# 调用函数
result = r.evalsha(
'rate_limiter', # 函数名
1, # key 数量
'rate:api:/users', # key
100, # 限制 100 次
60000 # 60 秒窗口
)
# 管理 Functions
r.function_list() # 列出所有注册的函数
r.function_delete('rate_limiter') # 删除函数
七、安全与认证
7.1 ACL 配置
python
# ===== Redis ACL(Redis 6+)=====
# 创建用户(只读 + 特定 key 权限)
r.acl_setuser('readonly_user',
enabled=True,
nopass=True,
keys=['user:*', 'product:*'],
commands=['+READ', '-WRITE', '-ADMIN'])
# 创建应用用户(特定命令)
r.acl_setuser('app_user',
enabled=True,
passwords=['+8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918',], # SHA256
keys=['app:*'],
commands=['+GET', '+SET', '+EXISTS', '+DEL', '+HGET', '+HSET', '+SELECT'])
# 使用用户连接
app_r = redis.Redis(
host='localhost', port=6379,
username='app_user', password='admin123'
)
# ACL 规则速查表
# +@all 允许所有命令
# -@dangerous 禁止危险命令(FLUSHDB/KEYS/DEBUG)
# +GET 允许 GET
# -GET 禁止 GET
# ~user:* key 模式匹配
# >password 设置密码
# on/off 启用/禁用用户
7.2 TLS 加密
ini
# ===== Redis TLS 配置 =====
# redis.conf
# TLS 配置
tls-port 6379
port 0 # 禁用非 TLS
tls-cert-file /etc/redis/tls/redis.crt
tls-key-file /etc/redis/tls/redis.key
tls-ca-cert-file /etc/redis/tls/ca.crt
tls-auth-clients yes # 双向认证(客户端证书)
tls-protocols TLSv1.2 TLSv1.3
# Python 连接 TLS
r = redis.Redis(
host='localhost', port=6379,
ssl=True,
ssl_certfile='/etc/redis/tls/client.crt',
ssl_keyfile='/etc/redis/tls/client.key',
ssl_ca_certs='/etc/redis/tls/ca.crt'
)
八、生产运维
8.1 内存优化
python
# ===== 内存淘汰策略 =====
"""
配置:maxmemory-policy
volatile-lru: 只淘汰设置了过期时间的 LRU key
allkeys-lru: 所有 key 中淘汰 LRU(推荐)
volatile-ttl: 淘汰 TTL 最短的 key
volatile-random: 随机淘汰有过期时间的 key
allkeys-random: 随机淘汰所有 key
noeviction: 不淘汰(默认,写入报错)
一般设置:
maxmemory 4gb
maxmemory-policy allkeys-lru
"""
# ===== Big Key 诊断 =====
import redis
def find_big_keys(r, top_n=10):
"""找出大 key"""
big_keys = []
for key_type in ['string', 'list', 'set', 'zset', 'hash']:
if key_type == 'string':
for key in r.scan_iter(count=1000):
size = r.memory_usage(key)
if size and size > 100_000: # > 100KB
big_keys.append((key, size, 'string'))
else:
for key in r.scan_iter(f'*{key_type}*', count=1000):
count = r.llen(key) if key_type == 'list' else r.scard(key)
if count and count > 10000: # > 10000 元素
big_keys.append((key, count, key_type))
return sorted(big_keys, key=lambda x: x[1], reverse=True)[:top_n]
# ===== MEMORY USAGE =====
# 查看单个 key 内存占用
r.memory_usage('user:1001') # 字节数
# 诊断内存泄漏
# redis-cli --bigkeys
# redis-cli --scan | head -1000 | redis-cli --pipe-timeout 3 --mem-stats
8.2 性能监控
python
# ===== Redis INFO 关键指标 =====
info = r.info()
# 内存
print(f"Used Memory: {info['used_memory_human']}") # 已用内存
print(f"Peak Memory: {info['used_memory_peak_human']}") # 峰值
print(f"Max Memory: {info['maxmemory_human']}") # 配置的最大内存
# 连接
print(f"Connected Clients: {info['connected_clients']}") # 客户端连接数
print(f"Blocked Clients: {info['blocked_clients']}") # 阻塞客户端
# 持久化
print(f"RDB: {info['rdb_changes_since_last_save']}") # 上次保存后的变化数
print(f"AOF: {info['aof_current_size']}") # AOF 文件大小
# 统计
print(f"QPS: {info['instantaneous_ops_per_sec']}") # 每秒命令数
print(f"Keyspace Hits: {info['keyspace_hits']}") # 缓存命中
print(f"Keyspace Misses: {info['keyspace_misses']}") # 缓存未命中
hit_rate = info['keyspace_hits'] / (info['keyspace_hits'] + info['keyspace_misses'])
print(f"Hit Rate: {hit_rate:.2%}")
# 复制
print(f"Role: {info['role']}") # master/slave
if info['role'] == 'master':
print(f"Connected Replicas: {info['connected_slaves']}")
# ===== Redis slowlog =====
# 查看慢查询
slow_logs = r.slowlog_get(10) # 最近 10 条
for log in slow_logs:
print(f"Time: {log['start_time']}")
print(f"Duration: {log['duration']}ms")
print(f"Command: {' '.join(log['args'])}")
# 设置慢查询阈值(ms)
r.config_set('slowlog-log-slower-than', 1000) # > 1s 记录
r.config_set('slowlog-max-len', 128) # 保留 128 条
九、总结
技术全景
| 层 | 核心概念 | 关键点 |
|---|---|---|
| 数据类型 | String/Hash/List/Set/ZSet/Bitmap/HyperLogLog | 编码 vs 底层结构 |
| 持久化 | RDB + AOF + 混合持久化 | everysec 策略最佳 |
| 缓存问题 | 穿透/击穿/雪崩 | 布隆过滤/Lock/随机TTL |
| 缓存模式 | Cache Aside | 写删,读写分离 |
| Sentinel | Raft + 故障转移 | 主观/客观下线 |
| Cluster | 16384 槽 + Gossip | CRC16 路由 + MOVED 重定向 |
| 分布式锁 | SET NX EX + Redlock | 5节点多数派 |
| Redis Functions | Lua 脚本升级版 | 可复用/版本管理 |
| 安全 | ACL v2 + TLS | 细粒度权限 |
最佳实践
| 实践 | 说明 |
|---|---|
| 混合持久化 | 开启 aof-use-rdb-preamble yes |
| 淘汰策略 | allkeys-lru,热点数据常驻 |
| pipeline | 批量操作,减少 RTT |
| Big Key | String ≤ 10KB,List ≤ 10000 元素 |
| Pipeline vs Lua | 原子性操作用 Lua,非原子用 Pipeline |
| 连接池 | JedisPool / Lettuce 连接池复用 |
| Pipeline 监控 | slowlog 监控慢命令 |
| 缓存穿透 | 布隆过滤器或空值缓存 |
本文涵盖 Redis 7 分布式缓存完整知识:五大数据类型与底层结构、RDB/AOF/混合持久化、缓存穿透/击穿/雪崩解决方案、Cache Aside 模式、Sentinel 哨兵集群、Redis Cluster 槽分区、分布式锁 Redlock 算法、Redis Functions(Lua 升级版)、ACL 安全认证、内存优化与性能监控。