Redis 7 分布式缓存架构实战

前言

💡 痛点: 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 安全认证、内存优化与性能监控。

相关推荐
zhuhai_xigedian1 小时前
源网荷储一体化 vs 传统供用电模式:差异、优势与转型路径
大数据·人工智能·分布式·系统架构·能源
凯源智能3 小时前
屋顶分布式光伏箱变远程测控实战:宝鸡法士特项目高效交付解析
分布式
一拳一个娘娘腔3 小时前
CVE-2026-43284 — Dirty Frag 深度拆解:当零拷贝遇上原地解密,页缓存成了攻击者的画板
linux·缓存
lx188548698963 小时前
Redis大Key阻塞:单线程CPU100%的致命陷阱
数据库·redis·缓存
IT策士3 小时前
Redis 从入门到精通:位图、HyperLogLog、GEO
数据库·redis·缓存
IT策士4 小时前
Redis 从入门到精通:Python 操作 Redis 进阶
数据库·redis·python
布局呆星4 小时前
Spring Boot + Redis 缓存实战:@Cacheable、序列化踩坑、缓存一致性,一次讲透
spring boot·redis·缓存
Devin~Y4 小时前
大厂 Java 面试实战:从 Spring Boot 微服务到 AI RAG 音视频平台全链路解析
java·spring boot·redis·spring cloud·微服务·rag·spring ai
Amy187021118234 小时前
东南亚智慧物流园区的“隐形守护者”:有源滤波柜如何驯服变频器5/7次谐波
分布式·能源