CC 咖啡猫的工作空间 Coding Space

Redis 数据结构与命令详解

Redis(Remote Dictionary Server)是一款基于内存的 KV 数据库,支持多种数据结构。本文档详细讲解 Redis 的 5 种核心数据结构、底层实现原理、命令用法与时间复杂度。


一、Redis 线程模型

1.1 单线程模型

Redis 6 以前,核心操作是单线程模型

客户端请求 → 事件循环(单线程)
              ↓
         顺序执行所有命令
              ↓
         返回结果

为什么单线程这么快?

  1. 纯内存操作:内存访问速度极快
  2. 非阻塞 IO:使用 epoll/select/kqueue 多路复用
  3. 避免了锁竞争:单线程无线程安全问题
  4. 数据结构高效:底层数据结构经过优化

1.2 多线程(Redis 6+)

主线程(IO 线程)→ 读取命令、解析命令、发送响应
                              ↓
        工作线程池(默认 4 个)→ 执行命令
                              ↓
                       返回结果

注意:多线程只用于处理网络 IO,命令执行仍是单线程。


二、Redis 底层数据结构

2.1 五大数据类型与底层实现

数据类型 底层实现 说明
String SDS(Simple Dynamic String) 动态字符串
Hash ziplist / hashtable 压缩列表 或 哈希表
List quicklist 双向链表 + ziplist
Set intset / hashtable 整数集合 或 哈希表
Sorted Set(ZSet) ziplist / skiplist 跳表

2.2 SDS(Simple Dynamic String)

Redis 自研的字符串类型,优于 C 字符串:

// SDS 结构
struct __attribute__ ((__packed__)) sdshdr8 {
    // 记录 buf 数组中已使用字节的数量(即 SDS 长度)
    uint8_t len;
    // 记录 buf 数组中未使用字节的数量
    uint8_t alloc;
    // 特殊标识 flags
    //   bit 0-2: SDS_TYPE_*(SDS 类型)
    //   bit 3: 是否为二进制安全
    unsigned char flags;
    // 柔性数组,存放字符串
    char buf[];
};

SDS vs C 字符串的优势

特性 C 字符串 SDS
获取长度 O(n) 需遍历 O(1) 已有 len 字段
缓冲区溢出 可能发生 自动扩容,不会溢出
内存重分配 每 次增长都要 realloc 惰性空间释放/预分配
二进制安全 可能中途截断 完整存储二进制
追加效率 每次可能 O(n) 预分配策略,均摊 O(1)

SDS 惰性空间释放:缩短字符串时,不立即释放空间,而是用 len 记录新长度,等将来使用。

2.3 ziplist(压缩列表)

内存紧凑的列表,用于小数据量场景:

zlbytes(4) | zltail(4) | zllen(2) | entry1 | entry2 | ... | zlend(1)
     |          |        |                        |
   总字节数    尾偏移量   元素数量              固定值 0xFF

entry 结构

previous_entry_length:前一元素长度(1 或 5 字节)
encoding:编码方式(字符串/整数)
content:实际内容

ziplist 优点

  • 内存紧凑,无额外指针(比双向链表省 16 字节/元素)
  • CPU Cache 友好

ziplist 缺点

  • 插入/删除元素时,可能导致级联更新(最坏 O(n²))
  • 元素数量超过 hash-max-ziplist-entries(512)或值超过 hash-max-ziplist-value(64字节)时,转换为 hashtable

2.4 quicklist(快速列表)

Redis 3.2+ 引入,是 ziplist 的双向链表:

quicklist
    ↓
head (ziplist) ↔ ziplist ↔ ziplist ↔ tail (ziplist)
  • 每个 ziplist 默认最大 8KB(list-max-ziplist-size 配置)
  • 元素太多时,自动创建新 ziplist 链接
  • 结合了 ziplist(紧凑)和 linkedlist(灵活插入)的优点

2.5 intset(整数集合)

Set 类型在数据量小时使用整数集合:

struct intset {
    uint32_t encoding;  // 编码:INTSET_ENC_INT16/32/64
    uint32_t length;    // 元素数量
    int8_t contents[]; // 柔性数组,存放整数(从小到大排序)
};

特点

  • 内存紧凑,无重复元素
  • 支持升级(从小类型到大类型,如 int16 → int32),不支持降级
  • 元素数量超过 set-max-intset-entries(512)时,转换为 hashtable

2.6 skiplist(跳表)

ZSet 的有序集合实现,O(log N) 查找:

Level 3: ──────────────────────────→ NULL
Level 2: ─────────→ ──────────────→ NULL
Level 1: ───→ ───→ ───→ ───→ ───→ NULL
Level 0: → → → → → → → → → → → NULL
         [1] [2] [3] [4] [5] [6]   (原始链表)

跳表 vs 红黑树

  • 跳表区间查找 O(log N),红黑树 O(log N)(但更复杂)
  • 跳表实现简单,锁粒度更细(并发友好)
  • Redis ZSet 用跳表 + hashtable 实现(dict 存 member→score)

三、String(字符串)

3.1 应用场景

场景 示例
缓存 SET user:100 '{"name":"zhang","age":25}'
计数器 INCR views:article:123
分布式锁 SET lock:product:100 "uuid" NX EX 30
Session SET session:abc123 '{"userId":100}'
限流 INCR rate:ip:192.168.1.1

3.2 核心命令

# ========== 基本操作 ==========

# 设置值(SET = SET key value)
# NX:key 不存在才设置(用于分布式锁)
# EX 30:30 秒过期
# PX 30000:毫秒级过期
SET lock:order:123 "uuid-xxx" NX EX 30
SET user:100 "zhangsan"
SET product:456 "iphone" EX 3600  # 1 小时后自动删除

# 批量设置
MSET user:100:name "zhang" user:100:age "25"

# 获取值
GET user:100                        # => "zhangsan"
GET product:456                      # => "iphone"

# 批量获取
MGET user:100:name user:100:age      # => ["zhang", "25"]

# 获取旧值并设置新值(原子操作,用于分布式锁释放)
GETSET lock:order:123 "new-uuid"     # => 返回旧值

# 删除 key(返回删除数量)
DEL user:100                          # => 1

# 设置多个 key 的值(MSET = Multi SET)
MSET user:100 "zhang" user:200 "li" user:300 "wang"

# ========== 数值操作 ==========

# 递增/递减(原子操作,值为非整数时报错)
SET counter:views 100
INCR counter:views                   # => 101(原子 +1)
INCRBY counter:views 10              # => 111(原子 +10)
DECR counter:views                   # => 110(原子 -1)
DECRBY counter:views 5              # => 105(原子 -5)

# 浮点递增
SET rate:score 95.5
INCRBYFLOAT rate:score 2.5          # => 98.0

# ========== 字符串操作 ==========

# 追加字符串(拼接)
APPEND user:100:tags "java,"          # 返回拼接后长度
APPEND user:100:tags "python"         # => "java,python"

# 获取字符串长度
STRLEN user:100                       # => 11

# 获取子字符串(范围)
SET msg "hello world redis"
GETRANGE msg 0 4                      # => "hello"(0-indexed,闭区间)
GETRANGE msg 6 -1                     # => "world redis"(-1 表示最后)

# 设置指定位置的字符
SETRANGE msg 6 "Redis"                # => "hello Redis redis"(从位置 6 开始覆盖)

# ========== 位图操作 ==========

# 设置某位的值(0 或 1)
SETBIT online:user:100 10000 1        # 用户 100 的第 10000 天签到

# 获取某位的值
GETBIT online:user:100 10000          # => 1

# 位图统计(统计 1 的个数)
BITCOUNT online:user:100             # => 签到天数

# 位图运算(AND/OR/XOR/NOT)
BITOP AND result:key1:key2 key1 key2  # key1 AND key2 的结果存 result

# ========== Key 操作 ==========

# 设置过期时间
EXPIRE user:100 3600                  # 1 小时后过期
EXPIREAT user:100 1735689600          # 过期时间戳
TTL user:100                           # 查看剩余时间(秒,-1 永久,-2 不存在)
PTTL user:100                          # 查看剩余时间(毫秒)

# 设置 key 的值(仅当 key 不存在)
SETNX user:new "li"                   # => 1(成功,key 不存在)
SETNX user:new "li"                   # => 0(失败,key 已存在)

# SETEX = SET + EXPIRE(原子操作)
SETEX cache:page:1 300 "html content"  # 设置值并 300 秒过期

# PSETEX(毫秒级过期)
PSETEX cache:page:1 300000 "html"

# 查找 key(生产环境慎用 KEYS)
KEYS user:*                           # 查找所有 user: 开头的 key
KEYS *                                # 查找所有 key(阻塞!勿在生产用)
SCAN 0 MATCH user:* COUNT 100         # 渐进式遍历(替代 KEYS)

# 判断 key 是否存在
EXISTS user:100                       # => 1(存在)
EXISTS user:100 user:200              # => 2(两个都存在)

# 重命名 key
RENAME old:key new:key                # 如果 new:key 已存在,会覆盖
RENAMENX old:key new:key              # 仅当 new:key 不存在时重命名

3.3 时间复杂度

命令 复杂度 说明
SET/GET O(1) 常数时间
MSET/MGET O(k) k 是 key 数量
INCR/INCRBY O(1) 原子递增
STRLEN O(1) SDS 已有长度
GETRANGE/SETRANGE O(n) n 是子串长度
APPEND O(1) 惰性空间释放,均摊
GETSET O(1) 获取并设置

四、Hash(哈希)

4.1 应用场景

场景 示例
对象存储 HSET user:100 name "zhang" age "25"
购物车 HSET cart:100:20240101 product:001 2
配置缓存 HSET config:app host "localhost" port "8080"

4.2 核心命令

# ========== 基本操作 ==========

# 设置哈希字段(HSET = Hash SET)
HSET user:100 name "zhangsan" age "25" city "Beijing"
# => 3(返回成功设置的字段数)

# 批量设置哈希字段
HMSET user:200 name "lisi" age "30" city "Shanghai"
# HMSET 已废弃,功能等同于 HSET

# 获取哈希字段值
HGET user:100 name                     # => "zhangsan"
HGET user:100 age                      # => "25"

# 批量获取哈希字段值
HMGET user:100 name age city           # => ["zhangsan", "25", "Beijing"]

# 获取所有字段和值
HGETALL user:100
# => ["name", "zhangsan", "age", "25", "city", "Beijing"]

# 获取所有字段名
HKEYS user:100                          # => ["name", "age", "city"]

# 获取所有值
HVALS user:100                          # => ["zhangsan", "25", "Beijing"]

# 获取字段数量
HLEN user:100                           # => 3

# ========== 数值操作 ==========

# 字段值递增/递减(字段值必须是数字)
HSET product:001 stock 100
HINCRBY product:001 stock -10           # => 90(原子 -10)
HINCRBY product:001 stock 50            # => 140(原子 +50)

# 浮点递增
HSET product:001 price "99.5"
HINCRBYFLOAT product:001 price 0.5     # => "100.0"

# ========== 查询操作 ==========

# 判断字段是否存在
HEXISTS user:100 name                   # => 1(存在)
HEXISTS user:100 gender                # => 0(不存在)

# 删除字段(返回删除的字段数)
HDEL user:100 city gender              # => 2

# ========== 扫描操作 ==========

# 渐进式遍历哈希字段(大数据量时不会阻塞)
HSCAN user:big hash 0 MATCH name:* COUNT 100
# cursor=0 开始,每次返回新的 cursor,直到 cursor=0 结束

# ========== 应用技巧 ==========

# 小技巧:结合 HSETNX 实现购物车
HSETNX cart:100:20240101 product:001 1  # 已存在则不覆盖

# 小技巧:获取哈希表大小(字段数量)
HLEN user:100                           # => 字段数量

4.3 ziplist vs hashtable 编码转换

# 当哈希表满足以下条件时,使用 ziplist(内存紧凑):
#   1. 字段数量 < hash-max-ziplist-entries(默认 512)
#   2. 每个字段值长度 < hash-max-ziplist-value(默认 64 字节)

# 查看编码类型
OBJECT ENCODING user:100                 # => "ziplist" 或 "hashtable"

# 编码转换(手动触发)
# Redis 自动根据数据量判断,通常不需要手动干预

4.4 时间复杂度

命令 复杂度 说明
HSET/HGET O(1) 哈希表操作
HGETALL O(k) k 是字段数量
HMSET/MGET O(k) k 是字段数量
HINCRBY O(1) 原子递增
HSCAN O(1) 每次调用返回少量数据

五、List(列表)

5.1 应用场景

场景 示例
消息队列 LPUSH queue:msg "hello" / BRPOP queue:msg
最新消息 LPUSH timeline:100 "msg1" / LTRIM timeline:100 0 99
栈/队列 LPUSH + RPOP(队列)或 LPUSH + LPOP(栈)
关注列表 LPUSH user:100:follow "user:200"

5.2 核心命令

# ========== 插入操作 ==========

# 从左边(头部)插入
LPUSH list:msgs "msg1" "msg2" "msg3"   # => 3
# 列表内容(从左到右):["msg3", "msg2", "msg1"]

# 从右边(尾部)插入
RPUSH list:msgs "msg4" "msg5"          # => 5
# 列表内容(从左到右):["msg3", "msg2", "msg1", "msg4", "msg5"]

# 在某个元素前后插入(元素不存在时不执行)
LINSERT list:msgs BEFORE "msg2" "msg1.5"  # => 6(在 msg2 前插入)
LINSERT list:msgs AFTER "msg2" "msg2.5"   # => 7(在 msg2 后插入)

# 设置指定索引位置的元素(0-indexed)
LSET list:msgs 0 "new-first"              # 把第一个元素设为 "new-first"

# ========== 获取操作 ==========

# 获取指定范围的元素
LRANGE list:msgs 0 -1                    # => 获取所有元素
LRANGE list:msgs 0 4                    # => 前 5 个元素
LRANGE list:msgs -3 -1                  # => 最后 3 个元素

# 获取指定索引位置的元素
LINDEX list:msgs 0                       # => "new-first"(第一个)
LINDEX list:msgs -1                      # => "msg5"(最后一个)

# 获取列表长度
LLEN list:msgs                           # => 7

# ========== 删除操作 ==========

# 从左边(头部)删除并返回
LPOP list:msgs                           # => "new-first"(弹出第一个)
LPOP list:msgs 2                         # 弹出并返回最多 2 个元素

# 从右边(尾部)删除并返回
RPOP list:msgs                           # => "msg5"(弹出最后一个)

# 删除列表中指定值的元素(count=0 删除所有,count>0 从左到右删 count 个,count<0 从右到左删)
LREM list:msgs 1 "msg1"                  # 删除 1 个 "msg1"(从左到右)
LREM list:msgs -1 "msg1"                 # 删除 1 个 "msg1"(从右到左)
LREM list:msgs 0 "msg1"                  # 删除所有 "msg1"

# 截断列表(保留指定范围)
LTRIM list:msgs 0 99                    # 只保留 0-99 的元素,其他删除
# 常用于:LRANGE + LTRIM 组合实现"只保留最新 N 条"

# ========== 阻塞操作 ==========

# 阻塞从左边弹出(队列)
# BRPOP = Blocking RPOP
# 如果列表为空,阻塞等待 timeout 秒,timeout=0 无限等待
BRPOP list:queue 0                       # 等待队列有元素,有元素时立即返回
BRPOP list:queue 30                       # 等待 30 秒超时

# 同时监听多个列表(按顺序检查,先就绪的优先)
BRPOP list:queue1 list:queue2 list:queue3 0

# 阻塞从左边弹出(BLPOP = Blocking LPOP)
BLPOP list:queue 0                        # 等待队列有元素

# ========== 高级操作 ==========

# 范围获取并删除(原子操作,RPOPLPUSH 的变体)
LMOVE source dest LEFT                   # 相当于 LPOP + LPUSH
LMOVE source dest RIGHT                  # 相当于 RPOP + RPUSH

# 阻塞版本(BLMOVE)
BLMOVE source dest LEFT RIGHT 0          # 从 source 左边弹出,右边压入 dest

# ========== 实战技巧 ==========

# 实现栈(先进后出)
LPUSH stack:data "A" "B" "C"
LPOP stack:data                          # => "C"(最后进的先出)

# 实现队列(先进先出)
LPUSH queue:data "A" "B" "C"
RPOP queue:data                          # => "A"(最先进的先出)

# 实现最新消息列表(只保留 100 条)
LPUSH timeline:100 "new msg"
LTRIM timeline:100 0 99                  # 只保留前 100 条

# 实现消息队列
# 生产者
LPUSH queue:order "order:1001"
# 消费者
BRPOP queue:order 0                       # 阻塞等待,有消息立即消费

5.3 时间复杂度

命令 复杂度 说明
LPUSH/RPUSH O(1) 头部/尾部插入
LPOP/RPOP O(1) 头部/尾部弹出
LINSERT O(n) 指定位置插入,n 是到插入点的距离
LSET O(n) 指定位置设置,n 是索引位置
LRANGE O(k) k 是获取的元素数量
LTRIM O(k) k 是保留的元素数量
BRPOP/BLPOP O(1) 阻塞,但弹出发起返回

六、Set(集合)

6.1 应用场景

场景 示例
标签集合 SADD tags:article:100 "java" "redis" "mysql"
抽奖 SRANDMEMBER prize:pool / SPOP prize:pool
关注/粉丝 SADD user:100:followings "200" "300"
集合运算 交集 SINTER、并集 SUNION、差集 SDIFF

6.2 核心命令

# ========== 基本操作 ==========

# 添加元素(自动去重,返回成功添加的数量)
SADD tags:article:100 "java" "redis" "mysql" "java"  # => 3(去重后只加 3 个)
SADD tags:article:100 "python"                        # => 1

# 获取集合所有元素
SMEMBERS tags:article:100
# => ["mysql", "java", "redis", "python"]

# 获取集合大小
SCARD tags:article:100                               # => 4

# 判断元素是否在集合中
SISMEMBER tags:article:100 "java"                    # => 1(存在)
SISMEMBER tags:article:100 "go"                     # => 0(不存在)

# ========== 随机操作 ==========

# 随机获取元素(不删除)
SRANDMEMBER tags:article:100                         # => 随机返回一个
SRANDMEMBER tags:article:100 2                      # => 返回 2 个随机元素

# 随机弹出元素(删除)
SPOP tags:article:100                               # => 随机弹出一个并删除
SPOP tags:article:100 2                             # => 随机弹出 2 个

# ========== 删除操作 ==========

# 删除指定元素(返回删除数量)
SREM tags:article:100 "java" "python"               # => 2

# ========== 集合运算 ==========

# 差集(A 有 B 没有的)
SADD set:A "a" "b" "c" "d"
SADD set:B "c" "d" "e" "f"
SDIFF set:A set:B                                    # => ["a", "b"]
SDIFF set:A set:B set:C                              # => A - B - C

# 差集并存储
SDIFFSTORE result:A:B set:A set:B                    # => 2(A-B 的结果存入 result)

# 交集(A 和 B 都有)
SINTER set:A set:B                                    # => ["c", "d"]
SINTERSTORE result:A∩B set:A set:B                   # => 2(结果存入 result)

# 并集(A 和 B 合并,去重)
SUNION set:A set:B                                    # => ["a", "b", "c", "d", "e", "f"]
SUNIONSTORE result:A∪B set:A set:B                    # => 6(结果存入 result)

# ========== 扫描操作 ==========

# 遍历集合元素(SCAN = 渐进式,无阻塞)
 SSCAN set:A 0 MATCH * COUNT 100

# ========== 实战技巧 ==========

# 抽奖实现:所有用户 ID 入集合
SADD prize:users user:001 user:002 user:003 ...
SADD prize:users user:001 user:002 user:003

# 一等奖 1 人
SPOP prize:users 1

# 二等奖 5 人
SRANDMEMBER prize:users 5                              # 不删除,中奖者还在

# 标签聚合查询(标签 a 的所有用户 OR 标签 b 的所有用户)
SUNION tag:users:a tag:users:b

# 标签精确查询(标签 a AND 标签 b 的所有用户)
SINTER tag:users:a tag:users:b

# 微博好友推荐(A 关注的人中,B 没关注的)
SDIFF set:A:followings set:B:followings

6.3 intset 编码

# 当 Set 满足以下条件时,使用 intset(内存紧凑,只能存整数):
#   1. 所有元素都是整数
#   2. 元素数量 <= set-max-intset-entries(默认 512)

# 查看编码类型
OBJECT ENCODING set:integer                            # => "intset"
OBJECT ENCODING set:string                             # => "hashtable"

6.4 时间复杂度

命令 复杂度 说明
SADD/SREM O(1) ~ O(k) k 是添加/删除的元素数量
SISMEMBER O(1) 哈希表查找
SMEMBERS O(k) k 是集合大小
SCARD O(1) 获取基数
SINTER/SUNION/SDIFF O(k * m) k 是小集合大小,m 是大集合数量

七、ZSet(有序集合)

7.1 应用场景

场景 示例
排行榜 ZADD leaderboard:article 10000 "article:100"
延时队列 ZADD delay:queue timestamp "task:001"
滑动窗口限流 ZADD rate:limit:ip:${ip} now score
热搜排序 ZINCRBY hot:search 1 "热搜词"

7.2 核心命令

# ========== 基本操作 ==========

# 添加元素(score 可以是整数或浮点数)
ZADD leaderboard:article 10000 "article:100"  # => 1(成功添加数量)
ZADD leaderboard:article 9500 "article:200"
ZADD leaderboard:article 9800 "article:300"
ZADD leaderboard:article 10000 "article:100"   # => 0(member 已存在,更新 score)

# 批量添加
ZADD leaderboard:article 9000 "article:400" 9500 "article:500"

# 获取元素的 score
ZSCORE leaderboard:article "article:100"         # => "10000"

# 获取元素的排名(0-indexed,从小到大)
ZRANK leaderboard:article "article:100"         # => 2(第 3 名)
ZRANK leaderboard:article "article:400"         # => 0(第 1 名)

# 获取元素的排名(从大到小)
ZREVRANK leaderboard:article "article:100"      # => 0(第 1 名,分数最高)

# ========== 范围查询 ==========

# 按分数升序获取(withscores 返回分数)
ZRANGE leaderboard:article 0 9 WITHSCORES
# => ["article:400", "9000", "article:500", "9500", "article:300", "9800", "article:100", "10000"]

# 按分数降序获取
ZREVRANGE leaderboard:article 0 9 WITHSCORES
# => 分数最高的在前

# 获取指定分数范围的元素(闭区间)
ZRANGEBYSCORE leaderboard:article 9000 10000
ZREVRANGEBYSCORE leaderboard:article 10000 9000  # 降序

# 获取分数范围的元素数量
ZCOUNT leaderboard:article 9000 10000             # => 4

# ========== 删除操作 ==========

# 删除指定元素
ZREM leaderboard:article "article:100"            # => 1

# 删除指定排名的元素
ZREMRANGEBYRANK leaderboard:article 0 9          # 删除前 10 名

# 删除指定分数范围的元素
ZREMRANGEBYSCORE leaderboard:article 0 9000      # 删除分数 <= 9000 的

# ========== 集合运算 ==========

# 交集(按权重相加)
ZADD set:Z1 1 "a" 2 "b"
ZADD set:Z2 2 "a" 3 "b" 4 "c"
ZINTERSTORE result:Z1∩Z2 2 set:Z1 set:Z2         # => 2(交集数量)
# result: a=3 (1+2), b=5 (2+3)

# 带权重的交集
ZINTERSTORE result:Z1∩Z2:weight 2 set:Z1 set:Z2 WEIGHTS 2 3
# result: a=1*2+2*3=8, b=2*2+3*3=13

# 并集
ZUNIONSTORE result:Z1∪Z2 2 set:Z1 set:Z2         # => 3(并集数量,自动去重)

# ========== 递增/递减 ==========

# 分数递增/递减
ZINCRBY leaderboard:article 500 "article:100"   # => 10500(原子 +500)
ZINCRBY leaderboard:article -1000 "article:400"  # => 8000(原子 -1000)

# ========== 扫描操作 ==========

# 遍历 ZSet
ZSCAN leaderboard:article 0 MATCH article:* COUNT 100

# ========== 实战技巧 ==========

# 排行榜实现
# 1. 用户阅读文章 +10 分
ZINCRBY article:read:rank 10 "user:100"

# 2. 获取 Top 10 用户
ZREVRANGE article:read:rank 0 9 WITHSCORES

# 3. 获取用户排名
ZREVRANK article:read:rank "user:100"             # +1 就是第几名

# 延时队列实现
# 1. 添加延时任务(score = 执行时间戳)
ZADD delay:queue 1735689600 "order:1001:refund"
ZADD delay:queue 1735693200 "order:1002:refund"

# 2. 消费者轮询获取到期任务
ZRANGEBYSCORE delay:queue 0 NOW                  # 获取到期任务
ZREMRANGEBYRANK delay:queue 0 9                 # 删除已获取的任务

# 滑动窗口限流实现
# 1. 每个时间窗口一个 ZSet key
ZADD rate:limit:ip:192.168.1.1:2024010112 1 "req:001"
ZADD rate:limit:ip:192.168.1.1:2024010112 2 "req:002"

# 2. 统计时间窗口内请求数
ZCOUNT rate:limit:ip:192.168.1.1:2024010112 0 NOW

# 3. 清理过期数据
ZREMRANGEBYSCORE rate:limit:ip:192.168.1.1:2024010112 0 TIMESTAMP-窗口大小

# 热搜榜实现
# 1. 每次搜索 +1
ZINCRBY hot:search 1 "python"

# 2. 获取实时 Top 10
ZREVRANGE hot:search 0 9 WITHSCORES

7.3 skiplist + dict 组合实现

Redis ZSet 使用skiplist + dict 双重实现:

ZSet
    ↓
dict(哈希表):member → score,O(1) 获取 score
    ↓
skiplist:按 score 排序,O(log N) 按序查找

为什么双重实现?

  • dict 保证 ZSCORE 查询 O(1)
  • skiplist 保证 ZRANGEBYSCORE 范围查询 O(log N)

7.4 时间复杂度

命令 复杂度 说明
ZADD O(log N) 跳表插入/更新
ZSCORE O(1) dict 查询
ZRANK/ZREVRANK O(log N) 跳表查找
ZRANGE/ZREVRANGE O(k + log N) k 是返回数量
ZINCRBY O(log N) 跳表更新
ZINTERSTORE O(k * n) n 是集合数量,k 是结果基数

八、特殊数据类型

8.1 Bitmaps(位图)

不是独立数据类型,是 String 的位操作扩展:

# 用户签到(设置第 N 位为 1)
SETBIT sign:user:100:202401 0 1   # 2024 年 1 月 1 日签到

# 检查是否签到
GETBIT sign:user:100:202401 0       # => 1

# 统计签到天数
BITCOUNT sign:user:100:202401       # => 1

# 获取日期范围内的签到状态(bitfield)
BITFIELD sign:user:100:202401 GET r0-4 0  # 获取 5 天的签到状态

存储计算:用户 ID 1 签到 → 位偏移量 = ID-1 → 位 0 = 1 1 亿用户 × 365 天 = 365 × 100000000 / 8 / 1024 / 1024 ≈ 4.3 GB

8.2 HyperLogLog

基数统计(不重复元素数量),内存极小(12KB):

# 添加元素
PFADD hll:uv "user:001" "user:002" "user:003"

# 估算基数(标准误差 0.81%)
PFCOUNT hll:uv                                    # => 3

# 合并多个 HyperLogLog
PFADD hll:uv2 "user:003" "user:004" "user:005"
PFMERGE hll:total hll:uv hll:uv2
PFCOUNT hll:total                                  # => 5

应用:日活 DAU、UV 统计。存 1 亿数据只需 12KB(传统 Set 需要几百 GB)。

8.3 Geospatial(地理空间)

存储经纬度,计算距离:

# 添加位置
GEOADD cities:china 116.4074 39.9042 "beijing"
GEOADD cities:china 121.4737 31.2304 "shanghai"
GEOADD cities:china 120.1536 30.2885 "hangzhou"

# 获取位置坐标
GEOPOS cities:china "beijing"                     # => [116.4074, 39.9042]

# 计算两点距离
GEODIST cities:china "beijing" "shanghai"         # => "1068.0675"(默认米)
GEODIST cities:china "beijing" "shanghai" km     # => "1068.0675"(公里)

# 获取指定坐标范围内的位置
GEORADIUS cities:china 116.4074 39.9042 100 km WITHDIST ASC
# 返回北京 100km 范围内的城市,按距离排序

# 获取指定位置范围内的位置(不用指定坐标)
GEOSEARCH cities:china FROMLONLAT 116.4074 39.9042 BYRADIUS 100 km WITHDIST ASC

8.4 Stream(流)

Redis 5.0+ 引入,消息队列:

# 添加消息(自动生成 ID)
XADD stream:orders "*" user_id "1001" amount "99.5"
# => "1704062000000-0"(时间戳-序号)

# 读取新消息
XREAD STREAMS stream:orders 0   # 从头读
XREAD COUNT 10 STREAMS stream:orders $  # 从最新消息开始

# 阻塞读取新消息
XREAD BLOCK 0 STREAMS stream:orders $   # $ 表示最新消息之后

# 消费者组(类似 Kafka Consumer Group)
XGROUP CREATE stream:orders consumer_group_1 0  # 从头开始消费
XGROUP CREATECONSUMER stream:orders consumer_group_1 consumer_1  # 创建消费者

# 读取(自动分配消费者)
XREADGROUP GROUP consumer_group_1 consumer_1 COUNT 10 STREAMS stream:orders ">"

# 确认消息已处理
XACK stream:orders consumer_group_1 "1704062000000-0"

# 查看消费者组状态
XINFO GROUPS stream:orders
XINFO CONSUMERS stream:orders consumer_group_1

九、Key 过期策略

9.1 过期时间设置

# 设置过期时间
EXPIRE key 3600             # 1 小时后过期
EXPIREAT key 1735689600     # 过期时间戳
PEXPIRE key 3600000         # 毫秒
PEXPIREAT key 1735689600000 # 毫秒时间戳

# 查看剩余时间
TTL key                     # 秒(-1 永久,-2 不存在)
PTTL key                    # 毫秒

# 移除过期时间(变成永久)
PERSIST key                 # => 1(成功)

9.2 过期删除策略

策略 说明 优缺点
惰性删除 访问 key 时检查,过期才删除 节省 CPU,内存可能堆积
定期删除 每隔一段时间,扫描部分 key 删除 折中方案
定时删除 给 key 设置 TTL 时创建定时器 保证内存,但 CPU 压力大

Redis 采用:惰性删除 + 定期删除组合

9.3 内存淘汰策略

当内存达到 maxmemory 时,Redis 按策略淘汰 key:

# maxmemory 1gb
# maxmemory-policy allkeys-lru
策略 说明
noeviction 不淘汰,返回错误(默认)
allkeys-lru 所有 key 中,LRU(最近最少使用)淘汰
allkeys-lfu 所有 key 中,LFU(最不经常使用)淘汰
allkeys-random 所有 key 中,随机淘汰
volatile-lru 已设置过期时间的 key 中,LRU 淘汰
volatile-lfu 已设置过期时间的 key 中,LFU 淘汰
volatile-random 已设置过期时间的 key 中,随机淘汰
volatile-ttl 已设置过期时间的 key 中,TTL 最小的(快过期)淘汰

十、Redis 持久化

10.1 RDB(快照)

bgsave
    ↓
fork 出一个子进程
    ↓
子进程遍历内存,生成 RDB 文件
    ↓
替换旧的 RDB 文件
# 手动触发
BGSAVE                         # 后台异步保存
SAVE                           # 同步阻塞(生产禁用)

# 自动触发(配置)
# save 900 1    # 900 秒内至少 1 次写操作
# save 300 10   # 300 秒内至少 10 次写操作
# save 60 10000 # 60 秒内至少 10000 次写操作

10.2 AOF(追加日志)

写操作 → 追加到 AOF 缓冲区
              ↓
        fsync 刷盘(按策略)
              ↓
        AOF 文件
# 刷盘策略
appendfsync always     # 每次写都刷盘(最安全,性能差)
appendfsync everysec   # 每秒刷盘(默认,推荐)
appendfsync no         # 由 OS 决定(最快,可能丢 1 秒数据)

10.3 混合持久化(Redis 4.0+)

AOF 重写时:
    ↓
先生成 RDB 格式的 base
    ↓
AOF 增量追加
    ↓
恢复时:先加载 RDB,再重放 AOF
aof-use-rdb-preamble yes  # 开启混合持久化

十一、Pipeline 与事务

11.1 Pipeline(管道)

批量执行命令,减少网络往返:

# 普通方式:N 次网络往返
GET key1
GET key2
GET key3

# Pipeline:1 次网络往返
PIPELINE
GET key1
GET key2
GET key3
END

Java Jedis 使用

// 创建 Pipeline
Pipeline pipeline = jedis.pipelined();

// 批量执行(不阻塞当前连接)
pipeline.set("k1", "v1");
pipeline.get("k1");
pipeline.incr("counter");

// 同步获取结果
List<Object> results = pipeline.syncAndReturnAll();

11.2 事务(MULTI/EXEC)

# 开启事务
MULTI

# 命令入队(不执行)
SET key1 "value1"
GET key1
INCR counter
INCR another_counter

# 执行队列中的所有命令(原子)
EXEC
# => [
#     OK,
#     "value1",
#     (integer) 1,
#     (integer) 1
#   ]

注意:Redis 事务不保证原子性,命令执行失败不会回滚前面的命令。

11.3 乐观锁(WATCH)

# WATCH key(监控 key 变化)
WATCH user:100:balance

# 检查 balance 是否变化(其他客户端修改了就放弃事务)
GET user:100:balance        # 假设是 100

# 开启事务,余额减少
MULTI
DECRBY user:100:balance 10
INCRBY user:100:consumed 10
EXEC                            # 如果 balance 没变,执行成功
                                 # 如果 balance 变了,返回 null(执行失败)

11.4 Lua 脚本

原子执行自定义逻辑:

# 简单示例:先 GET 再 SET 的原子版本
EVAL "return redis.call('GET', KEYS[1])" 1 mykey

# 完整示例:限流 Lua 脚本
EVAL "
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local current = tonumber(redis.call('GET', key) or '0')
if current >= limit then
    return 0
end
current = redis.call('INCR', key)
if current == 1 then
    redis.call('EXPIRE', key, 60)
end
return 1
" 1 rate:limit:ip:192.168.1.1 10

十二、发布订阅

# 订阅频道
SUBSCRIBE channel:news channel:tech

# 订阅模式(支持通配符)
PSUBSCRIBE channel:*

# 发布消息
PUBLISH channel:news "Hello World"

# 退订
UNSUBSCRIBE channel:news
PUNSUBSCRIBE channel:*

注意:发布订阅的消息是临时的,不会持久化。断线重连后,不会收到中间的消息。


十三、常用配置速查

# ========== 内存 ==========
maxmemory 1gb                    # 最大内存(不设置则无限制)
maxmemory-policy allkeys-lru     # 内存淘汰策略

# ========== RDB ==========
save 900 1                      # 900 秒内 1 次写触发 RDB
save 300 10
save 60 10000
dbfilename dump.rdb             # RDB 文件名
dir ./                         # RDB 存储目录

# ========== AOF ==========
appendonly yes                  # 开启 AOF
appendfilename "appendonly.aof" # AOF 文件名
appendfsync everysec            # 刷盘策略
auto-aof-rewrite-percentage 100 # AOF 文件大小超过上次 100% 时重写
auto-aof-rewrite-min-size 64mb # AOF 重写最小文件大小

# ========== 集群 ==========
cluster-enabled yes              # 开启集群模式
cluster-config-file nodes.conf  # 集群节点配置文件

# ========== 连接 ==========
bind 127.0.0.1                  # 绑定 IP
port 6379                      # 端口
timeout 0                      # 连接超时(0 表示禁用)
tcp-keepalive 300              # TCP 保活(秒)

# ========== 慢查询 ==========
slowlog-log-slower-than 10000   # 超过 10ms 记录(微秒)
slowlog-max-len 128             # 最多记录 128 条慢查询

十四、数据结构转换触发条件

String
  └── 最大 512MB

Hash
  ├── <= 512 字段且每值 <= 64 字节 → ziplist
  └── 其他 → hashtable

List
  ├── 每个元素 <= 64 字节 → ziplist(默认 8KB 一段)
  └── 其他 → quicklist

Set
  ├── 全部是整数且 <= 512 个 → intset
  └── 其他 → hashtable

ZSet
  ├── <= 128 元素且每值 <= 64 字节 → ziplist
  └── 其他 → skiplist + dict

十五、命令速度参考

级别 O(1) O(log N) O(N) O(N²)
命令 GET/SET/INCR ZADD/ZRANK LRANGE KEYS
特点 恒定时间 跳表/堆 列表扫描 禁止生产使用

常用 O(1) 命令

GET/SET/MGET/MSET
INCR/INCRBY/DECR/DECRBY
SISMEMBER/SCARD
EXISTS/EXPIRE/TTL
HSET/HGET/HGETALL
LPUSH/RPOP