02-Redis进阶

4 阅读9分钟

Redis 进阶特性

深入理解 Redis 高级特性和底层原理

1. Redis 单线程模型

1.1 为什么单线程还这么快?

客户端连接
    ↓
网络 IO(epoll)
    ↓
命令解析
    ↓
执行命令(单线程)
    ↓
返回结果(epoll)

原因

  1. 纯内存操作:无磁盘 IO
  2. IO 多路复用:epoll 高效处理网络 IO
  3. 避免锁竞争:单线程无需锁
  4. 高效数据结构:底层优化

1.2 IO 多路复用

// epoll 伪代码
while (true) {
    // 等待事件
    events = epoll_wait(epfd, timeout);
    
    // 处理事件
    for (event in events) {
        if (event.type == READ) {
            read_data();
            execute_command();
        } else if (event.type == WRITE) {
            write_data();
        }
    }
}

1.3 Redis 6.0 多线程

注意:Redis 6.0 引入了 IO 多线程,但命令执行仍是单线程

  • 多线程:处理网络 IO(读写)
  • 单线程:执行命令
  • 目的:充分利用多核 CPU

2. 高级数据类型

2.1 Bitmaps(位图)

基本操作
# SETBIT/GETBIT
SETBIT user:sign:2024:10 1 1   # 10月1日签到
SETBIT user:sign:2024:10 2 1   # 10月2日签到
GETBIT user:sign:2024:10 1     # 1

# BITCOUNT(统计)
BITCOUNT user:sign:2024:10     # 签到天数

# BITPOS(查找第一个 0 或 1)
BITPOS user:sign:2024:10 1     # 第一个签到的日期

# BITOP(位运算)
BITOP AND result key1 key2     # 与
BITOP OR result key1 key2      # 或
BITOP XOR result key1 key2     # 异或
应用场景
  • 签到统计:每天一位,一年 365 位 = 46 字节
  • 用户在线状态:10 亿用户 = 119MB
  • 活跃用户统计:按天/周/月统计
# 示例:签到统计
SETBIT user:1:sign:202410 1 1
SETBIT user:1:sign:202410 2 1
SETBIT user:1:sign:202410 5 1

# 统计10月签到天数
BITCOUNT user:1:sign:202410  # 3

# 统计连续签到
# 需要配合程序逻辑

2.2 HyperLogLog(基数统计)

基本操作
# PFADD(添加元素)
PFADD uv:page:1 user1 user2 user3

# PFCOUNT(统计基数)
PFCOUNT uv:page:1

# PFMERGE(合并)
PFMERGE uv:total uv:page:1 uv:page:2
特点
  • 占用空间小:12KB 可统计 2^64 个元素
  • 有误差:0.81% 的误差率
  • 不返回元素:只能统计数量
应用场景
  • UV 统计:网站独立访客
  • 搜索引擎:查询去重
  • 大数据:海量数据去重计数
# UV 统计示例
# 每个用户访问时
PFADD uv:2024:10:23 user123
PFADD uv:2024:10:23 user456

# 查询当天 UV
PFCOUNT uv:2024:10:23

# 合并一周的 UV
PFMERGE uv:week:42 uv:2024:10:17 uv:2024:10:18 ... uv:2024:10:23
PFCOUNT uv:week:42

2.3 Geo(地理位置)

基本操作
# GEOADD(添加位置)
GEOADD locations 116.404 39.915 "beijing"
GEOADD locations 121.472 31.231 "shanghai"

# GEOPOS(获取坐标)
GEOPOS locations "beijing"

# GEODIST(计算距离)
GEODIST locations "beijing" "shanghai" km  # 1067.5 km

# GEORADIUS(半径查询)
GEORADIUS locations 116.404 39.915 100 km WITHCOORD WITHDIST

# GEORADIUSBYMEMBER(以成员为中心查询)
GEORADIUSBYMEMBER locations "beijing" 1000 km

# GEOHASH(获取 Geohash 字符串)
GEOHASH locations "beijing"
应用场景
  • LBS 应用:附近的人、附近的商家
  • 打车软件:查找附近的司机
  • 外卖平台:查找附近的餐厅
# 示例:查找附近的餐厅
GEOADD restaurants 116.404 39.915 "restaurant1"
GEOADD restaurants 116.405 39.916 "restaurant2"

# 查找用户 3km 内的餐厅
GEORADIUS restaurants 116.404 39.915 3 km WITHCOORD WITHDIST

2.4 Stream(数据流)

基本操作
# XADD(添加消息)
XADD stream:orders * user_id 123 amount 100
XADD stream:orders * user_id 456 amount 200

# XLEN(消息数量)
XLEN stream:orders

# XRANGE(范围查询)
XRANGE stream:orders - +       # 所有消息
XRANGE stream:orders 1234567890000-0 +  # 从指定 ID 开始

# XREAD(读取消息)
XREAD COUNT 10 STREAMS stream:orders 0

# XREAD BLOCK(阻塞读取)
XREAD BLOCK 5000 STREAMS stream:orders $

# 消费者组
XGROUP CREATE stream:orders group1 0
XREADGROUP GROUP group1 consumer1 COUNT 10 STREAMS stream:orders >

# XACK(确认消息)
XACK stream:orders group1 1234567890000-0
特点
  • 持久化:消息持久存储
  • 消费者组:支持多消费者
  • 消息确认:确保消息被处理
  • Pending 列表:跟踪未确认消息
应用场景
  • 消息队列:替代 List 实现更强大的消息队列
  • 日志系统:实时日志流
  • 事件溯源:事件流存储

3. 发布订阅

3.1 基本操作

# SUBSCRIBE(订阅频道)
SUBSCRIBE channel1

# PUBLISH(发布消息)
PUBLISH channel1 "Hello Redis"

# PSUBSCRIBE(模式订阅)
PSUBSCRIBE news.*

# UNSUBSCRIBE(取消订阅)
UNSUBSCRIBE channel1

3.2 应用场景

  • 实时聊天:聊天室
  • 消息通知:系统通知
  • 实时数据:股票行情、体育赛事

3.3 注意事项

  • 消息不持久化:订阅者不在线则丢失
  • 无消息确认:不保证消息送达
  • 适合场景:实时性要求高,可容忍消息丢失

4. 主从复制

4.1 主从架构

┌─────────┐
│  Master │ (读写)
└────┬────┘
     │
  ┌──┴──┬──────┐
  │     │      │
┌─▼─┐ ┌─▼─┐ ┌──▼──┐
│Slave│Slave│Slave │ (只读)
└────┘ └───┘ └─────┘

4.2 配置主从

# 从节点配置(redis.conf)
replicaof 127.0.0.1 6379    # 主节点地址
masterauth password         # 主节点密码(如果有)

# 或使用命令
REPLICAOF 127.0.0.1 6379

# 取消主从
REPLICAOF NO ONE

4.3 复制原理

全量同步
1. 从节点发送 PSYNC 命令
2. 主节点执行 BGSAVE 生成 RDB
3. 主节点发送 RDB 文件
4. 从节点清空数据并加载 RDB
5. 主节点发送缓冲区的增量命令
增量同步
1. 主节点将写命令发送到复制缓冲区
2. 从节点接收并执行命令
3. 保持数据同步

4.4 应用场景

  • 读写分离:主节点写,从节点读
  • 数据备份:从节点作为备份
  • 故障恢复:从节点提升为主节点

5. 哨兵模式(Sentinel)

5.1 哨兵架构

┌──────────┐  ┌──────────┐  ┌──────────┐
│ Sentinel │  │ Sentinel │  │ Sentinel │
└─────┬────┘  └─────┬────┘  └─────┬────┘
      │             │              │
      └─────────────┼──────────────┘
                    │
      ┌─────────────┴──────────────┐
      │                            │
┌─────▼────┐                 ┌─────▼─────┐
│  Master  │ ←───────────────│   Slave   │
└──────────┘                 └───────────┘

5.2 哨兵功能

  1. 监控:检查主从节点是否正常
  2. 通知:故障时通知管理员
  3. 自动故障转移:主节点故障时自动切换
  4. 配置提供:客户端获取主节点地址

5.3 配置哨兵

# sentinel.conf
port 26379
sentinel monitor mymaster 127.0.0.1 6379 2    # 2 个哨兵同意才故障转移
sentinel auth-pass mymaster password
sentinel down-after-milliseconds mymaster 5000  # 5 秒未响应视为下线
sentinel parallel-syncs mymaster 1              # 故障转移时同时同步的从节点数
sentinel failover-timeout mymaster 15000        # 故障转移超时时间

# 启动哨兵
redis-sentinel sentinel.conf

5.4 故障转移流程

1. 主观下线(Subjectively Down):单个哨兵认为主节点下线
2. 客观下线(Objectively Down):多数哨兵认为主节点下线
3. 选举领导哨兵:Raft 算法选举
4. 故障转移:
   - 选择一个从节点提升为主节点
   - 其他从节点复制新主节点
   - 通知客户端新主节点地址

6. 集群(Cluster)

6.1 集群架构

┌─────────────────────────────────────┐
│              Redis Cluster          │
├──────────┬──────────┬──────────────┤
│ Master 1 │ Master 2 │ Master 3     │
│ (0-5460) │(5461-10922)│(10923-16383)│
├──────────┼──────────┼──────────────┤
│ Slave 1  │ Slave 2  │ Slave 3      │
└──────────┴──────────┴──────────────┘

6.2 集群特点

  • 分片存储:16384 个槽位,数据分散存储
  • 高可用:主节点故障,从节点自动提升
  • 无中心化:节点之间通过 Gossip 协议通信
  • 水平扩展:可动态增删节点

6.3 槽位分配

# Redis 使用 CRC16 算法计算键的槽位
slot = CRC16(key) % 16384

# 示例
CRC16("user:123") % 16384 = 5678  # user:123 存储在槽位 5678

6.4 集群命令

# 创建集群
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

# 查看集群信息
CLUSTER INFO
CLUSTER NODES

# 添加节点
redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000

# 删除节点
redis-cli --cluster del-node 127.0.0.1:7000 <node-id>

# 重新分配槽位
redis-cli --cluster reshard 127.0.0.1:7000

6.5 客户端路由

客户端  任意节点  MOVED 错误  正确节点

# MOVED 错误示例
(error) MOVED 5678 127.0.0.1:7001

# 客户端收到 MOVED 后,直接连接正确节点

7. Redis 性能优化

7.1 大 Key 问题

什么是大 Key?
  • String:值大于 10KB
  • Hash/List/Set/ZSet:元素超过 5000
危害
  • 阻塞其他命令
  • 内存不均衡
  • 删除耗时
解决方案
# 1. 拆分大 Key
# 原:user:1:orders (10000 个订单)
# 改:user:1:orders:1 (1000 个)
#     user:1:orders:2 (1000 个)
#     ...

# 2. 异步删除
UNLINK key  # Redis 4.0+,后台删除

# 3. 监控大 Key
redis-cli --bigkeys

7.2 热 Key 问题

什么是热 Key?

频繁访问的 Key,如热门商品、明星微博。

危害
  • 单点压力大
  • 可能导致节点崩溃
解决方案
# 1. 本地缓存
# 客户端缓存热 Key

# 2. 多副本
# key_replica_1
# key_replica_2
# 随机选择一个读取

# 3. 读写分离
# 热 Key 从从节点读取

7.3 慢查询优化

查看慢查询
# 查看慢查询日志
SLOWLOG GET 10

# 慢查询配置
CONFIG SET slowlog-log-slower-than 10000  # 10ms
CONFIG SET slowlog-max-len 128            # 最多保留 128 条
常见慢命令
  • KEYS *:生产环境禁用,改用 SCAN
  • HGETALL:大 Hash 慢,改用 HSCAN
  • SMEMBERS:大 Set 慢,改用 SSCAN

7.4 内存优化

内存分析
# 查看内存使用
INFO memory

# 内存分析
redis-cli --memkeys
redis-rdb-tools dump.rdb  # 分析 RDB 文件
优化建议
# 1. 设置最大内存
CONFIG SET maxmemory 1gb

# 2. 选择淘汰策略
CONFIG SET maxmemory-policy allkeys-lru

# 3. 压缩数据
# 使用 Hash 代替 String(省内存)
HMSET user:1 name "Alice" age 25

# 4. 设置过期时间
EXPIRE key 3600

8. Redis 安全

8.1 访问控制

# 设置密码
CONFIG SET requirepass your-password

# 使用密码连接
redis-cli -a your-password

# 重命名危险命令
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command KEYS ""

8.2 网络安全

# 绑定内网 IP
bind 10.0.0.1

# 防火墙
# 只允许内网访问 6379 端口

8.3 ACL(Redis 6.0+)

# 创建用户
ACL SETUSER alice on >password ~cached:* +get

# 查看用户
ACL LIST

# 删除用户
ACL DELUSER alice

9. 高频问题

Q1: Redis 单线程为什么还这么快?

答案

  1. 纯内存操作
  2. IO 多路复用(epoll)
  3. 单线程避免锁竞争
  4. 高效的数据结构

Q2: Redis 主从复制原理?

答案

  • 全量同步:BGSAVE + RDB + 增量命令
  • 增量同步:主节点发送写命令到从节点

Q3: Redis 哨兵的作用?

答案

  1. 监控主从节点
  2. 故障检测
  3. 自动故障转移
  4. 配置提供

Q4: Redis 集群如何分片?

答案

  • 16384 个槽位
  • CRC16(key) % 16384
  • 槽位分配给不同节点

Q5: 如何解决大 Key 问题?

答案

  1. 拆分大 Key
  2. 使用 UNLINK 异步删除
  3. 监控大 Key

总结

Redis 进阶核心:

  1. ✅ 单线程模型和 IO 多路复用
  2. ✅ 高级数据类型(Bitmap、HyperLogLog、Geo、Stream)
  3. ✅ 发布订阅
  4. ✅ 主从复制
  5. ✅ 哨兵模式
  6. ✅ 集群分片
  7. ✅ 性能优化
  8. ✅ 安全配置

下一步:学习 Lua 脚本编程!