Redis 数据结构与命令详解
Redis(Remote Dictionary Server)是一款基于内存的 KV 数据库,支持多种数据结构。本文档详细讲解 Redis 的 5 种核心数据结构、底层实现原理、命令用法与时间复杂度。
一、Redis 线程模型
1.1 单线程模型
Redis 6 以前,核心操作是单线程模型:
客户端请求 → 事件循环(单线程)
↓
顺序执行所有命令
↓
返回结果
为什么单线程这么快?
- 纯内存操作:内存访问速度极快
- 非阻塞 IO:使用 epoll/select/kqueue 多路复用
- 避免了锁竞争:单线程无线程安全问题
- 数据结构高效:底层数据结构经过优化
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