redis从零单排(一)

0 阅读13分钟

少年硬气。。。 image.png

image.png


Redis 完整知识体系:从入门到出门

Redis是什么

想象你是一个图书馆管理员。传统数据库(如 MySQL)像是一个巨大的地下仓库,每次找书都要走很远的路(磁盘 I/O)。而 Redis 就像是一个放在你办公桌上的记忆宫殿——所有数据都存在内存里,伸手就能拿到,速度极快。

Redis = Remote Dictionary Server,远程字典服务。本质是一个内存中的 Key-Value 数据库

image.png

核心特性:高性能、丰富的数据结构、持久化、分布式支持。


第一幕:初识 Redis —— 基础配置与启动

1.1 安装与启动

# 启动 Redis 服务器
redis-server

# 后台运行(推荐生产环境)
redis-server --daemonize yes

# 使用配置文件启动
redis-server /etc/redis/redis.conf

# 客户端连接
redis-cli -h 127.0.0.1 -p 6379

1.2 核心配置文件 redis.conf

# 绑定 IP(默认只允许本地,生产环境需配置)
bind 127.0.0.1

# 端口
port 6379

# 密码认证
requirepass your_password

# 后台运行
daemonize yes

# 日志文件
logfile /var/log/redis/redis-server.log

# 内存限制(关键!防止 OOM)
maxmemory 256mb

# 内存淘汰策略(后面详细讲)
maxmemory-policy allkeys-lru

第二幕:数据结构 —— Redis 的"十八般武艺"

Redis 不只是简单的"键值对",它提供了丰富的数据结构,每种结构都有特定的应用场景。

mindmap
  root((Redis<br/>数据结构))
    String
      缓存
      计数器
      分布式锁
      会话
    List
      队列
      栈
      最新列表
    Hash
      对象存储
      用户信息
    Set
      去重
      共同好友
      标签
    ZSet
      排行榜
      延时队列
    高级结构
      Bitmap
      HyperLogLog
      GEO
      Stream

2.1 String(字符串)—— 最基础也最常用

# 基础操作
SET user:1001 "张三"           # 设置
GET user:1001                   # 获取 → "张三"

# 计数器(原子递增)
INCR view:article:123          # 阅读量+1 → 1
INCR view:article:123          # → 2
DECR view:article:123          # → 1

# 批量操作(减少网络往返)
MSET user:1001 "张三" user:1002 "李四"
MGET user:1001 user:1002       # → ["张三", "李四"]

# 设置过期时间(缓存必备)
SET session:token:abc "user_data" EX 3600  # 1小时后自动删除

应用场景故事:电商网站的商品库存计数。每次下单,DECR stock:sku:8888,如果返回 -1 说明库存不足。

2.2 List(列表)—— 消息队列的雏形

# 队列操作(FIFO - 先进先出)
LPUSH queue:orders "order_001"   # 左侧入队
LPUSH queue:orders "order_002"
RPOP queue:orders                 # 右侧出队 → "order_001"(先进入的先处理)

# 栈操作(LIFO - 后进先出)
LPUSH stack:history "page1"
LPUSH stack:history "page2"
LPOP stack:history               # → "page2"(后进入的先出来)

# 获取范围(适合分页)
LRANGE queue:orders 0 9          # 获取前10个
BLPOP queue:orders 30            # 阻塞等待30秒,有数据立即返回

应用场景故事:微博的"最新动态"。每个用户有一个 list:user:1001:timeline,发推文时 LPUSH,看动态时 LRANGE 0 20 取前20条。

2.3 Hash(哈希)—— 存储对象的完美选择

# 存储一个用户对象(对比 String 的 JSON 序列化)
HSET user:1001 name "张三" age 25 city "北京"
HGET user:1001 name              # → "张三"
HGETALL user:1001              # → {"name":"张三","age":"25","city":"北京"}

# 对比 String 方式
SET user:1001 '{"name":"张三","age":25,"city":"北京"}'
# 修改年龄时需要:1) GET 整个JSON 2) 解析 3) 修改 4) 序列化 5) SET
# Hash 直接:HSET user:1001 age 26 (只改一个字段,高效!)

应用场景故事:购物车功能。HSET cart:user:1001 sku:8888 2(2件商品),修改数量时只改一个字段,不用序列化整个购物车。

2.4 Set(集合)—— 去重与关系计算

# 添加标签
SADD tags:article:123 "redis" "database" "cache"
SADD tags:article:456 "redis" "tutorial"

# 判断元素是否存在
SISMEMBER tags:article:123 "redis"   # → 1 (true)

# 交集 - 共同标签
SINTER tags:article:123 tags:article:456   # → ["redis"]

# 并集 - 所有标签
SUNION tags:article:123 tags:article:456   # → ["redis","database","cache","tutorial"]

# 随机抽奖(不重复)
SRANDMEMBER lottery:participants 3   # 抽3个幸运儿

应用场景故事:社交网站的"共同好友"。

  • 用户A的好友:SADD friends:user:A B C D E
  • 用户B的好友:SADD friends:user:B C D F G
  • 共同好友:SINTER friends:user:A friends:user:B["C","D"]

2.5 Sorted Set(ZSet)—— 带权重的排行榜

# 添加成员及其分数
ZADD leaderboard 100 "player:张三" 85 "player:李四" 120 "player:王五"

# 按分数范围查询(排行榜)
ZREVRANGE leaderboard 0 2 WITHSCORES   
# → ["player:王五","120","player:张三","100","player:李四","85"](从高到低)

# 查询某个玩家的排名
ZREVRANK leaderboard "player:张三"   # → 1(第2名,从0开始)

# 范围查询(延时队列核心)
ZRANGEBYSCORE delay_queue 0 1716192000   # 获取所有到指定时间前需要执行的任务

应用场景故事:游戏排行榜、延时队列。

  • 排行榜:实时更新分数,ZADD 自动排序。
  • 延时队列:任务执行时间作为 score,定时扫描 ZRANGEBYSCORE 获取到期任务。

2.6 高级数据结构

结构命令应用场景
BitmapSETBIT/BITCOUNT签到(365天=365bit≈46字节)、活跃用户统计
HyperLogLogPFADD/PFCOUNTUV 统计(误差0.81%,内存固定12KB)
GEOGEOADD/GEORADIUS附近的人、地理位置服务
StreamXADD/XREAD/XGROUP消息队列(Kafka 的简化版)
# Bitmap 签到示例(极省内存!)
SETBIT sign:2024:04:user:1001 0 1   # 4月1日签到
SETBIT sign:2024:04:user:1001 1 1   # 4月2日签到
BITCOUNT sign:2024:04:user:1001      # → 2(本月签到2天)

# 对比:用 String 存 "1,1,0,1..." 需要30字节,Bitmap 只需 4字节

第三幕:核心机制 —— Redis 的"生存法则"

数据存在内存里,断电就丢怎么办?内存满了怎么办?Redis 通过一套精妙的机制解决这些问题。

image.png

3.1 持久化 —— 数据不丢的"双保险"

RDB(Redis Database)—— 快照备份
# 配置自动触发(redis.conf)
save 900 1      # 900秒内至少1次修改,触发快照
save 300 10     # 300秒内至少10次修改
save 60 10000   # 60秒内至少10000次修改

# 手动触发
BGSAVE          # 后台异步生成 RDB 文件

原理故事:就像给 Redis 拍照片。每隔一段时间,把内存中的所有数据拍成一张"照片"(二进制文件)保存到磁盘。恢复时直接把照片还原,速度很快。

优缺点

  • ✅ 恢复速度快,文件紧凑
  • ❌ 最后一次快照到宕机之间的数据会丢失(可能丢几分钟数据)
AOF(Append Only File)—— 命令日志
# 配置(redis.conf)
appendonly yes
appendfsync everysec   # 每秒同步(推荐,平衡性能与安全)

原理故事:像银行的交易流水账。每执行一个写命令,就把命令本身记录到文件里。恢复时重新执行这些命令,就能重建数据。

优缺点

  • ✅ 数据更安全,最多丢1秒数据
  • ❌ 文件大,恢复慢(要重放所有命令)
混合持久化(Redis 4.0+)—— 鱼和熊掌兼得
# 配置
aof-use-rdb-preamble yes

原理故事:开头用 RDB 快照(快速加载基础数据),后面用 AOF 增量日志(保证最新数据不丢)。就像先快速搭好房子框架,再慢慢装修细节。

3.2 过期策略 —— 内存的"自动清理"

graph LR
    A[数据设置过期时间] --> B{过期策略}
    B -->|惰性删除| C[访问时检查<br/>过期则删除<br/>省CPU但可能堆积]
    B -->|定期删除| D[定时随机抽样<br/>删除过期键<br/>平衡CPU与内存]
    
    C --> E[最终清理]
    D --> E
# 设置过期(多种方式)
EXPIRE session:token 3600       # 3600秒后过期
SET temp:key "value" EX 60      # 创建时设置60秒过期
TTL temp:key                    # 查看剩余生存时间 → 45

生活类比:惰性删除像"只在有人找这本书时才检查是否过期";定期删除像"图书管理员每天随机抽查一批书,清理过期的"。

3.3 内存淘汰机制 —— 内存满了怎么办?

当内存达到 maxmemory 限制,且数据都没过期时,Redis 必须选择淘汰哪些数据

策略说明适用场景
volatile-lru已设置过期的键中,淘汰最近最少使用缓存场景
allkeys-lru所有键中,淘汰最近最少使用通用缓存(推荐)
volatile-random从已设置过期的键中随机淘汰
allkeys-random从所有键中随机淘汰
volatile-ttl淘汰即将过期的键优先保活久的
noeviction不淘汰,直接报错(默认)不允许丢数据
# 配置
maxmemory-policy allkeys-lru

故事理解:图书馆书架满了,LRU 策略就是"扔掉最久没人借的书",因为没人借说明价值低。

3.4 单线程与多线程 —— Redis 的"并发哲学"

image.png

核心结论:Redis 的命令执行始终是单线程的(保证原子性),6.0+ 只是网络读写用了多线程,解决高并发连接问题。


第四幕:事务与原子操作 —— 保证"要么全做,要么不做"

sequenceDiagram
    participant Client
    participant Redis
    
    Client->>Redis: MULTI(开启事务)
    Client->>Redis: SET balance:A 100
    Client->>Redis: SET balance:B 200
    Client->>Redis: EXEC(执行)
    Redis-->>Client: OK(全部成功)
    
    Note over Client,Redis: 如果中间出错<br/>DISCARD 回滚所有命令
# 事务示例:转账操作
MULTI
DECR balance:张三 100    # 扣100
INCR balance:李四 100    # 加100
EXEC                      # 原子执行

# 乐观锁:防止并发修改
WATCH balance:张三        # 监控这个键
MULTI
DECR balance:张三 100
EXEC
# 如果执行前 balance:张三 被其他客户端改了,EXEC 返回 nil,事务不执行

重要特点

  • Redis 事务不支持回滚(不同于数据库)。如果某个命令语法错误,整个事务会失败;但如果是运行时错误(如对一个 String 做 INCR),其他命令仍会执行。
  • Lua 脚本 可以实现真正的原子复杂逻辑。
# Lua 脚本保证原子性
EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 mykey myvalue

第五幕:高可用架构 —— 从单机到集群

graph TB
    subgraph 主从复制
        M[Master<br/>主节点] -->|同步数据| S1[Slave 1]
        M -->|同步数据| S2[Slave 2]
    end
    
    subgraph 哨兵模式
        Sen1[Sentinel 1] -->|监控| M
        Sen2[Sentinel 2] -->|监控| M
        Sen3[Sentinel 3] -->|监控| M
        Sen1 -->|故障转移| S1
    end
    
    subgraph Cluster集群
        N1[Node 1<br/>0-5460槽] --- N2[Node 2<br/>5461-10922槽]
        N2 --- N3[Node 3<br/>10923-16383槽]
        N1 -.->|MOVED重定向| C[客户端]
    end

5.1 主从复制 —— 读写分离与数据备份

# 从节点配置
replicaof 192.168.1.10 6379   # 指定主节点

同步过程

  1. 全量同步:首次连接,主节点生成 RDB 发送给从节点
  2. 增量同步:后续通过复制积压缓冲区,只传新命令

5.2 哨兵 Sentinel —— 自动故障转移

# sentinel.conf
sentinel monitor mymaster 192.168.1.10 6379 2   # 2个哨兵同意才故障转移
sentinel down-after-milliseconds mymaster 5000    # 5秒无响应认为下线

工作机制

  • 主观下线:某个哨兵觉得主节点挂了
  • 客观下线:多个哨兵(达到配置数量)都认为挂了,才真的进行故障转移
  • 自动选主:从从节点中选一个作为新主节点

5.3 Redis Cluster —— 真正的分布式

# 集群模式启动
redis-cli --cluster create 192.168.1.11:6379 192.168.1.12:6379 \
  192.168.1.13:6379 --cluster-replicas 1

核心概念

  • 16384 个哈希槽:整个集群的键空间被分成 16384 个槽
  • 数据分片:每个节点负责一部分槽,数据均匀分布
  • MOVED 重定向:如果客户端请求的键不在当前节点,返回 MOVED 告诉客户端去正确的节点
# 示例:计算键属于哪个槽
CLUSTER KEYSLOT user:1001    # → 5461(假设)
# 如果当前节点不负责 5461,返回:MOVED 5461 192.168.1.12:6379

第六幕:缓存三大问题 —— 面试必考!

graph TD
    A[缓存三大问题] --> B[缓存穿透<br/>查不存在的数据]
    A --> C[缓存击穿<br/>热点Key过期]
    A --> D[缓存雪崩<br/>大量Key同时过期]
    
    B -->|解决| B1[缓存空值<br/>布隆过滤器]
    C -->|解决| C1[互斥锁<br/>逻辑过期<br/>永不过期]
    D -->|解决| D1[随机过期时间<br/>多级缓存<br/>限流降级]
    
    style B fill:#FFB6C1
    style C fill:#FFE4B5
    style D fill:#FF6B6B

6.1 缓存穿透 —— 查询"不存在的东西"

场景:黑客用不存在的 ID 疯狂请求,缓存里没有,数据库也没有,每次都要查数据库。

// 解决方案1:缓存空值
String data = redis.get(key);
if (data == null) {
    // 查数据库
    data = db.query(key);
    if (data == null) {
        redis.set(key, "null", 60);  // 缓存空值,60秒过期
    }
}

// 解决方案2:布隆过滤器(更优雅)
if (!bloomFilter.mightContain(key)) {
    return null;  // 肯定不存在,直接返回
}
// 可能存在,继续查缓存/数据库

6.2 缓存击穿 —— "明星离婚"热点事件

场景:某个热点 Key 突然过期,大量请求同时打到数据库。

// 解决方案1:互斥锁(只有一个线程去重建)
String data = redis.get(hotKey);
if (data == null) {
    // 尝试获取锁
    if (redis.setnx(lockKey, "1", 60)) {
        // 只有获取到锁的线程去查数据库
        data = db.query(hotKey);
        redis.set(hotKey, data, 3600);
        redis.del(lockKey);  // 释放锁
    } else {
        // 其他线程短暂等待后重试
        Thread.sleep(100);
        return redis.get(hotKey);  // 再次尝试获取
    }
}

// 解决方案2:逻辑过期(永不过期,但标记过期时间)
// 缓存中存:{value: "实际数据", expireTime: 1716192000000}
// 发现逻辑过期了,后台异步更新,前台返回旧数据

6.3 缓存雪崩 —— "双十一零点"

场景:大量 Key 同时过期,或者 Redis 宕机,所有请求涌向数据库。

# 解决方案1:随机过期时间(避免同时失效)
EXPIRE key $((RANDOM % 300 + 300))   # 300-600秒随机

# 解决方案2:多级缓存
浏览器缓存 → CDN → Nginx缓存 → Redis → 数据库

# 解决方案3:限流降级
# 当流量过大,直接返回"系统繁忙",保护数据库

第七幕:分布式锁 —— 跨机器的"排他性"

sequenceDiagram
    participant A as 服务A
    participant R as Redis
    participant B as 服务B
    
    A->>R: SET lock:order:123 NX EX 30
    R-->>A: OK(获取锁成功)
    
    B->>R: SET lock:order:123 NX EX 30
    R-->>B: nil(获取失败)
    
    A->>A: 执行业务逻辑...
    A->>R: DEL lock:order:123(释放锁)
    
    Note over A,B: NX: 不存在才设置<br/>EX: 30秒自动过期<br/>防止死锁!
# 基础实现
SET lock:resource:123 "owner:A" NX EX 30
# NX = Not eXists,只有不存在才设置
# EX = EXpire,30秒自动过期(防止服务崩溃导致死锁)

# 进阶:Redisson 可重入锁(Java)
RLock lock = redisson.getLock("myLock");
lock.lock();    // 阻塞等待
try {
    // 执行业务
} finally {
    lock.unlock();  // 释放锁
}

关键点

  1. 原子加锁SET NX EX 一条命令完成
  2. 过期时间:必须设置,防止死锁
  3. 防误删:释放前检查是否是自己的锁(用唯一标识)

第八幕:发布订阅与管道 —— 高效通信

8.1 Pub/Sub —— 消息广播

# 客户端A:订阅频道
SUBSCRIBE news:sports

# 客户端B:发布消息
PUBLISH news:sports "姚明当选篮协主席"
# 客户端A立即收到消息

局限:消息不持久化,消费者掉线期间的消息会丢失。需要持久化消息队列用 Stream

8.2 Pipeline —— 批量命令减少网络往返

# 普通方式:10000次操作 = 10000次网络往返
SET key1 value1
SET key2 value2
...

# Pipeline:一次发送,一次返回
# 伪代码
pipeline = redis.pipeline()
for i in range(10000):
    pipeline.set(f"key:{i}", f"value:{i}")
pipeline.execute()  # 一次性发送所有命令

效果:网络往返从 N 次降到 1 次,性能提升数十倍!


第九幕:性能优化 —— 让 Redis 飞起来

graph LR
    A[性能优化] --> B[避免大Key<br/>String < 10KB<br/>Hash/List/Set/ZSet < 5000元素]
    A --> C[避免长列表<br/>List/Hash 元素过多<br/>影响性能]
    A --> D[禁用 KEYS /*<br/>使用 SCAN 渐进式遍历]
    A --> E[批量命令<br/>MSET/MGET<br/>Pipeline]
    A --> F[合理内存策略<br/>maxmemory-policy]
    A --> G[系统优化<br/>关闭THP<br/>优化TCP<br/>连接复用]

关键优化点

# 1. 禁用 KEYS 命令(生产环境致命!)
KEYS user:*        # 遍历全库,O(N),阻塞其他命令
# 改用 SCAN(渐进式,不阻塞)
SCAN 0 MATCH user:* COUNT 100

# 2. 批量操作
MSET user:1:name "张三" user:1:age 25 user:1:city "北京"
MGET user:1:name user:1:age user:1:city

# 3. 连接复用(避免频繁创建连接)
# 使用连接池,保持长连接

第十幕:典型业务场景 —— 学以致用

mindmap
  root((Redis<br/>业务场景))
    缓存
      Cache Aside模式
      读: 先缓存后DB
      写: 先DB后删缓存
    秒杀
      分布式锁防超卖
      限流保护系统
      原子减库存 DECR
    排行榜
      ZSet 自动排序
      实时更新分数
    签到
      Bitmap 极省内存
      365天=46字节
    UV统计
      HyperLogLog
      12KB固定内存
      误差0.81%
    延时任务
      ZSet score=执行时间
      定时扫描执行
    分布式Session
      String 存储
      多服务共享登录态

10.1 缓存模式:Cache Aside

// 读操作
public String getData(String key) {
    String data = redis.get(key);
    if (data == null) {
        data = db.query(key);      // 查数据库
        redis.set(key, data, 3600); // 放入缓存
    }
    return data;
}

// 写操作(关键:先更新数据库,再删缓存!)
public void updateData(String key, String value) {
    db.update(key, value);         // 先更新数据库
    redis.del(key);               // 再删缓存(不是更新缓存!)
    // 下次读请求会自动从数据库加载最新数据到缓存
}

为什么删缓存而不是更新缓存?

  • 并发场景下,"更新缓存"可能导致脏数据(A、B两个线程同时写,顺序错乱)
  • "删缓存"简单有效,下次读会重新加载最新数据

10.2 秒杀系统 —— 分布式锁 + 限流 + 原子操作

// 1. 限流:令牌桶/漏桶算法(保护系统不被打垮)
// 2. 获取分布式锁(防止超卖)
if (redis.setnx("lock:sku:8888", "1", 10)) {
    try {
        // 3. 原子减库存
        Long stock = redis.decr("stock:sku:8888");
        if (stock >= 0) {
            // 创建订单
            createOrder(userId, skuId);
        } else {
            // 库存不足,恢复库存(或记录已超卖)
            redis.incr("stock:sku:8888");
            return "已售罄";
        }
    } finally {
        redis.del("lock:sku:8888");
    }
}

10.3 延时任务 —— ZSet 实现

# 添加延时任务(score = 执行时间戳)
ZADD delay_queue 1716192000 "task:send_email:123"
ZADD delay_queue 1716195600 "task:cancel_order:456"

# 定时脚本每秒执行:
ZRANGEBYSCORE delay_queue 0 1716192000 LIMIT 0 1
# 获取到期的任务,执行后删除
ZREM delay_queue "task:send_email:123"

知识串联总图

graph TB
    subgraph "基础篇"
        A1[Redis基础<br/>内存KV数据库] --> A2[安装配置<br/>redis.conf]
    end
    
    subgraph "数据结构篇"
        B1[String<br/>缓存/计数器] --> B2[List<br/>队列/栈]
        B2 --> B3[Hash<br/>对象存储]
        B3 --> B4[Set<br/>去重/关系]
        B4 --> B5[ZSet<br/>排行榜]
        B5 --> B6[高级结构<br/>Bitmap/HLL/GEO/Stream]
    end
    
    subgraph "核心机制篇"
        C1[持久化<br/>RDB/AOF/混合] --> C2[过期策略<br/>惰性+定期]
        C2 --> C3[内存淘汰<br/>8种策略]
        C3 --> C4[单线程<br/>6.0+网络多线程]
    end
    
    subgraph "进阶篇"
        D1[事务<br/>MULTI/EXEC] --> D2[Lua脚本<br/>原子性]
        D2 --> D3[分布式锁<br/>SET NX EX]
        D3 --> D4[Pub/Sub<br/>Pipeline]
    end
    
    subgraph "架构篇"
        E1[主从复制<br/>读写分离] --> E2[哨兵Sentinel<br/>自动故障转移]
        E2 --> E3[Cluster集群<br/>16384槽分片]
    end
    
    subgraph "实战篇"
        F1[缓存问题<br/>穿透/击穿/雪崩] --> F2[性能优化<br/>避免大Key/SCAN]
        F2 --> F3[业务场景<br/>秒杀/签到/排行榜]
    end
    
    A1 --> B1
    B6 --> C1
    C4 --> D1
    D4 --> E1
    E3 --> F1
    
    style A1 fill:#E1F5FE
    style B3 fill:#E8F5E9
    style C2 fill:#FFF3E0
    style D3 fill:#F3E5F5
    style E2 fill:#E8EAF6
    style F1 fill:#FFEBEE