redis的数据结构
Redis是一个高性能的键值存储数据库,支持多种数据结构。本文将深入讲解Redis的各种数据结构,包括其内存分布、基本命令使用和适合的应用场景,并通过数据模拟演示每个操作的具体效果。
1. Redis字符串 (String)
数据结构
Redis字符串是最基本的数据类型,内部使用SDS(Simple Dynamic String)实现。
graph TD
A[Redis String] --> B[SDS结构]
B --> C[len: 字符串长度]
B --> D[alloc: 分配的空间大小]
B --> E[flags: 标志位]
B --> F[buf: 字符数组]
G[内存布局] --> H["len(4字节) | alloc(4字节) | flags(1字节) | buf[]"]
内存分布
- len: 记录字符串的实际长度
- alloc: 记录分配给字符数组的空间大小
- flags: 标志位,用于标识SDS类型
- buf: 实际存储字符串内容的字节数组
基本命令使用与数据模拟
初始状态
Redis内存: 空
操作演示
1. SET key "Hello Redis" - 设置字符串键值对,将指定的值存储到键中
执行前: key不存在
执行后:
key: "mykey"
value: SDS {
len: 11
alloc: 16
flags: 1
buf: "Hello Redis\0"
}
内存占用: ~27字节
2. GET key - 获取指定key的值 - 获取指定键的字符串值
执行前: key = "Hello Redis"
执行后: 返回 "Hello Redis" (数据不变)
3. APPEND key " World" - 在现有字符串值的末尾追加新内容
执行前: key = "Hello Redis"
执行后:
key: "mykey"
value: SDS {
len: 17
alloc: 32
flags: 1
buf: "Hello Redis World\0"
}
内存占用: ~41字节
4. STRLEN key - 返回字符串值的长度(字符数)
执行前: key = "Hello Redis World"
执行后: 返回 17 (数据不变)
5. 数值操作演示
SET counter 10 - 设置数值型字符串
执行后:
counter: SDS {
len: 2
alloc: 8
flags: 1
buf: "10\0"
}
INCR counter - 将数值型字符串递增1
执行后:
counter: SDS {
len: 2
alloc: 8
flags: 1
buf: "11\0"
}
INCRBY counter 5 - 将数值型字符串按指定值递增
执行后:
counter: SDS {
len: 2
alloc: 8
flags: 1
buf: "16\0"
}
sequenceDiagram
participant C as Client
participant R as Redis
participant M as Memory
C->>R: SET mykey "Hello"
R->>M: 分配SDS结构
M-->>R: 返回内存地址
R-->>C: OK
C->>R: APPEND mykey " World"
R->>M: 扩展SDS缓冲区
M-->>R: 返回新地址
R-->>C: 11 (新长度)
C->>R: GET mykey
R->>M: 读取SDS内容
M-->>R: "Hello World"
R-->>C: "Hello World"
适合场景
- 缓存用户信息
- 计数器(点赞数、访问量)
- 分布式锁
- 会话存储
2. Redis列表 (List)
数据结构
Redis列表使用双向链表实现,支持快速的头尾插入和删除操作。
graph LR
A[List Header] --> B[Node1]
B --> C[Node2]
C --> D[Node3]
D --> E[Node4]
B -.-> A
C -.-> B
D -.-> C
E -.-> D
F[Node结构] --> G[prev指针]
F --> H[next指针]
F --> I[value值]
内存分布
- listNode: 链表节点,包含前驱指针、后继指针和值
- list: 链表结构,包含头节点、尾节点、长度等信息
- 每个节点独立分配内存,通过指针连接
基本命令使用与数据模拟
初始状态
Redis内存: 空
List结构: NULL
操作演示
1. LPUSH mylist "world" - 从列表左端(头部)插入元素
执行前: mylist不存在
执行后:
mylist: List {
head: Node1
tail: Node1
len: 1
}
Node1: {
prev: NULL
next: NULL
value: "world"
}
结构: ["world"]
2. LPUSH mylist "hello" - 从列表左端(头部)插入元素
执行前: ["world"]
执行后:
mylist: List {
head: Node2
tail: Node1
len: 2
}
Node2: {
prev: NULL
next: Node1
value: "hello"
}
Node1: {
prev: Node2
next: NULL
value: "world"
}
结构: ["hello", "world"]
3. RPUSH mylist "!" - 从列表右端(尾部)插入元素
执行前: ["hello", "world"]
执行后:
mylist: List {
head: Node2
tail: Node3
len: 3
}
Node3: {
prev: Node1
next: NULL
value: "!"
}
结构: ["hello", "world", "!"]
4. LRANGE mylist 0 -1 - 获取列表指定范围内的元素
执行前: ["hello", "world", "!"]
执行后: 返回 ["hello", "world", "!"] (数据不变)
5. LPOP mylist - 从列表左端(头部)弹出并返回元素
执行前: ["hello", "world", "!"]
执行后:
返回: "hello"
mylist: List {
head: Node1
tail: Node3
len: 2
}
释放Node2内存
结构: ["world", "!"]
6. RPOP mylist - 从列表右端(尾部)弹出并返回元素
执行前: ["world", "!"]
执行后:
返回: "!"
mylist: List {
head: Node1
tail: Node1
len: 1
}
释放Node3内存
结构: ["world"]
适合场景
- 消息队列
- 最新消息列表
- 用户操作历史
- 实现栈和队列
3. Redis哈希表 (Hash)
数据结构
Redis哈希表使用字典(dict)实现,采用链地址法解决哈希冲突。
graph TD
A[Hash Table] --> B[dictht 0]
A --> C[dictht 1]
B --> D["table[0]"]
B --> E["table[1]"]
B --> F["table[2]"]
B --> G["table[...]"]
D --> H[dictEntry]
H --> I[key]
H --> J[value]
H --> K[next]
L[渐进式rehash] --> M["正常时使用dictht[0]"]
L --> N["rehash时同时使用两个表"]
基本命令使用与数据模拟
初始状态
Redis内存: 空
Hash结构: NULL
操作演示
1. HSET user:1 name "张三" - 设置哈希表中指定字段的值
执行前: user:1不存在
执行后:
user:1: dict {
dictht[0]: {
table: [NULL, NULL, Entry1, NULL, ...]
size: 4
used: 1
}
}
Entry1: {
key: "name"
value: "张三"
next: NULL
}
哈希表: {"name": "张三"}
2. HSET user:1 age 25 - 设置哈希表中指定字段的值
执行前: {"name": "张三"}
执行后:
user:1: dict {
dictht[0]: {
table: [NULL, Entry2, Entry1, NULL, ...]
size: 4
used: 2
}
}
Entry2: {
key: "age"
value: "25"
next: NULL
}
哈希表: {"name": "张三", "age": "25"}
3. HSET user:1 city "北京" - 设置哈希表中指定字段的值
执行前: {"name": "张三", "age": "25"}
执行后:
user:1: dict {
dictht[0]: {
table: [NULL, Entry2, Entry1, Entry3, ...]
size: 4
used: 3
}
}
Entry3: {
key: "city"
value: "北京"
next: NULL
}
哈希表: {"name": "张三", "age": "25", "city": "北京"}
4. HGET user:1 name - 获取哈希表中指定字段的值
执行前: {"name": "张三", "age": "25", "city": "北京"}
执行后: 返回 "张三" (数据不变)
查找过程:
1. 计算"name"的哈希值
2. 定位到table[2]
3. 返回Entry1.value
5. HGETALL user:1 - 获取哈希表中所有字段和值
执行前: {"name": "张三", "age": "25", "city": "北京"}
执行后: 返回 ["name", "张三", "age", "25", "city", "北京"]
遍历过程:
1. 遍历table数组
2. 对每个非空Entry输出key-value
6. HDEL user:1 age - 删除哈希表中指定的字段
执行前: {"name": "张三", "age": "25", "city": "北京"}
执行后:
user:1: dict {
dictht[0]: {
table: [NULL, NULL, Entry1, Entry3, ...]
size: 4
used: 2
}
}
释放Entry2内存
哈希表: {"name": "张三", "city": "北京"}
适合场景
- 存储对象信息
- 用户属性管理
- 购物车
- 配置信息存储
4. Redis集合 (Set)
数据结构
Redis集合使用哈希表实现,确保元素的唯一性。
graph TD
A[Set] --> B[dict字典]
B --> C["哈希表存储元素"]
C --> D["key: 集合元素"]
C --> E["value: NULL"]
F[集合操作] --> G[并集 UNION]
F --> H[交集 INTER]
F --> I[差集 DIFF]
G --> J["Set A ∪ Set B"]
H --> K["Set A ∩ Set B"]
I --> L["Set A - Set B"]
基本命令使用与数据模拟
初始状态
Redis内存: 空
Set结构: NULL
操作演示
1. SADD myset "apple" - 向集合中添加一个或多个元素
执行前: myset不存在
执行后:
myset: dict {
dictht[0]: {
table: [NULL, Entry1, NULL, NULL, ...]
size: 4
used: 1
}
}
Entry1: {
key: "apple"
value: NULL
next: NULL
}
集合: {"apple"}
2. SADD myset "banana" - 向集合中添加一个或多个元素
执行前: {"apple"}
执行后:
myset: dict {
dictht[0]: {
table: [NULL, Entry1, Entry2, NULL, ...]
size: 4
used: 2
}
}
Entry2: {
key: "banana"
value: NULL
next: NULL
}
集合: {"apple", "banana"}
3. SADD myset "orange" - 向集合中添加一个或多个元素
执行前: {"apple", "banana"}
执行后:
myset: dict {
dictht[0]: {
table: [Entry3, Entry1, Entry2, NULL, ...]
size: 4
used: 3
}
}
Entry3: {
key: "orange"
value: NULL
next: NULL
}
集合: {"apple", "banana", "orange"}
4. SADD myset "apple" - 向集合中添加一个或多个元素(重复元素会被忽略)
执行前: {"apple", "banana", "orange"}
执行后: 返回 0 (元素已存在,集合不变)
集合: {"apple", "banana", "orange"}
5. SMEMBERS myset - 返回集合中的所有成员
执行前: {"apple", "banana", "orange"}
执行后: 返回 ["apple", "banana", "orange"] (顺序可能不同)
6. SISMEMBER myset "apple" - 判断元素是否是集合的成员
执行前: {"apple", "banana", "orange"}
执行后: 返回 1 (存在)
查找过程:
1. 计算"apple"的哈希值
2. 在哈希表中查找
3. 找到则返回1
7. SREM myset "banana" - 从集合中移除一个或多个元素
执行前: {"apple", "banana", "orange"}
执行后:
myset: dict {
dictht[0]: {
table: [Entry3, Entry1, NULL, NULL, ...]
size: 4
used: 2
}
}
释放Entry2内存
集合: {"apple", "orange"}
8. 集合运算演示
SADD set1 "a" "b" "c" - 创建集合set1并添加元素
set1: {"a", "b", "c"}
SADD set2 "b" "c" "d" - 创建集合set2并添加元素
set2: {"b", "c", "d"}
SUNION set1 set2 - 返回两个集合的并集
结果: {"a", "b", "c", "d"}
算法: 遍历两个集合,合并所有元素
SINTER set1 set2 - 返回两个集合的交集
结果: {"b", "c"}
算法: 遍历较小集合,检查元素是否在另一个集合中
SDIFF set1 set2 - 返回第一个集合与其他集合的差集
结果: {"a"}
算法: 遍历set1,排除在set2中的元素
flowchart LR
A["空集合"] --> B["SADD apple"]
B --> C["{apple}"]
C --> D["SADD banana"]
D --> E["{apple, banana}"]
E --> F["SADD orange"]
F --> G["{apple, banana, orange}"]
G --> H["SREM banana"]
H --> I["{apple, orange}"]
J["set1: {a,b,c}"] --> K["SUNION"]
L["set2: {b,c,d}"] --> K
K --> M["{a,b,c,d}"]
J --> N["SINTER"]
L --> N
N --> O["{b,c}"]
适合场景
- 标签系统
- 好友关系
- 去重操作
- 共同关注/粉丝
5. Redis有序集合 (ZSet)
数据结构
Redis有序集合同时使用哈希表和跳跃表实现,既保证元素唯一性又维护有序性。
基本命令使用与数据模拟
初始状态
Redis内存: 空
ZSet结构: NULL
操作演示
1. ZADD leaderboard 100 "player1" - 向有序集合添加一个或多个成员,或者更新已存在成员的分数
执行前: leaderboard不存在
执行后:
leaderboard: {
dict: {
"player1" -> 100.0
}
skiplist: {
level: 1
length: 1
header -> Node1 -> NULL
}
}
Node1: {
member: "player1"
score: 100.0
level[0]: {forward: NULL, span: 0}
}
有序集合: [("player1", 100)]
2. ZADD leaderboard 200 "player2" - 向有序集合添加一个或多个成员,或者更新已存在成员的分数
执行前: [("player1", 100)]
执行后:
leaderboard: {
dict: {
"player1" -> 100.0
"player2" -> 200.0
}
skiplist: {
level: 2
length: 2
header -> Node1 -> Node2 -> NULL
}
}
Node2: {
member: "player2"
score: 200.0
level[0]: {forward: NULL, span: 0}
level[1]: {forward: NULL, span: 0}
}
有序集合: [("player1", 100), ("player2", 200)]
3. ZADD leaderboard 150 "player3" - 向有序集合添加一个或多个成员,或者更新已存在成员的分数
执行前: [("player1", 100), ("player2", 200)]
执行后:
插入位置: player1和player2之间
leaderboard: {
dict: {
"player1" -> 100.0
"player2" -> 200.0
"player3" -> 150.0
}
skiplist: {
level: 2
length: 3
header -> Node1 -> Node3 -> Node2 -> NULL
}
}
有序集合: [("player1", 100), ("player3", 150), ("player2", 200)]
4. ZRANGE leaderboard 0 -1 WITHSCORES - 通过索引区间返回有序集合指定区间内的成员
执行前: [("player1", 100), ("player3", 150), ("player2", 200)]
执行后: 返回 ["player1", "100", "player3", "150", "player2", "200"]
遍历过程:
1. 从跳跃表头节点开始
2. 按level[0]链表顺序遍历
3. 输出member和score
5. ZREVRANGE leaderboard 0 2 - 返回有序集合中指定区间内的成员,通过索引,分数从高到低
执行前: [("player1", 100), ("player3", 150), ("player2", 200)]
执行后: 返回 ["player2", "player3", "player1"]
遍历过程:
1. 从跳跃表尾节点开始
2. 按backward指针逆序遍历
3. 取前3个元素
6. ZSCORE leaderboard "player1" - 返回有序集合中成员的分数值
执行前: [("player1", 100), ("player3", 150), ("player2", 200)]
执行后: 返回 "100"
查找过程:
1. 在dict中查找"player1"
2. 直接返回对应的score值
时间复杂度: O(1)
7. ZRANK leaderboard "player3" - 返回有序集合中指定成员的排名
执行前: [("player1", 100), ("player3", 150), ("player2", 200)]
执行后: 返回 1 (排名从0开始)
查找过程:
1. 在跳跃表中查找"player3"
2. 计算前面节点的span总和
3. 返回排名位置
8. ZRANGEBYSCORE leaderboard 100 200 - 通过分数返回有序集合指定区间内的成员
执行前: [("player1", 100), ("player3", 150), ("player2", 200)]
执行后: 返回 ["player1", "player3", "player2"]
查找过程:
1. 在跳跃表中定位score>=100的第一个节点
2. 顺序遍历直到score>200
3. 返回范围内的所有member
sequenceDiagram
participant C as Client
participant Z as ZSet
participant D as Dict
participant S as SkipList
C->>Z: ZADD key 100 "player1"
Z->>D: 添加 "player1" -> 100
Z->>S: 插入节点("player1", 100)
S-->>Z: 插入完成
Z-->>C: 1 (新增元素数)
C->>Z: ZADD key 150 "player3"
Z->>D: 添加 "player3" -> 150
Z->>S: 插入节点("player3", 150)
Note over S: 维护有序性,插入到正确位置
S-->>Z: 插入完成
Z-->>C: 1
C->>Z: ZSCORE key "player1"
Z->>D: 查找 "player1"
D-->>Z: 返回 100
Z-->>C: "100"
适合场景
- 排行榜
- 优先级队列
- 延时队列
- 范围查询
6. Redis地理空间 (GEO)
数据结构
Redis GEO基于有序集合实现,使用GeoHash算法将二维坐标转换为一维分数。
graph TD
A[GEO] --> B[ZSet有序集合]
B --> C["member: 地点名称"]
B --> D["score: GeoHash值"]
E[GeoHash编码] --> F["经纬度 (116.4, 39.9)"]
F --> G["二进制编码"]
G --> H["Base32编码"]
H --> I["最终GeoHash: wx4g0ec1"]
J[空间查询] --> K["GEORADIUS: 半径查询"]
J --> L["GEODIST: 距离计算"]
J --> M["GEOPOS: 坐标获取"]
基本命令使用与数据模拟
初始状态
Redis内存: 空
GEO结构: NULL (底层为ZSet)
操作演示
1. GEOADD cities 116.4074 39.9042 "北京" - 将指定的地理空间位置(纬度、经度、名称)添加到指定的key中
执行前: cities不存在
执行后:
GeoHash计算:
经度: 116.4074 -> 二进制编码
纬度: 39.9042 -> 二进制编码
交错编码 -> GeoHash: 4069885555089301
cities: ZSet {
dict: {"北京" -> 4069885555089301}
skiplist: Node1("北京", 4069885555089301)
}
地理集合: [("北京", 116.4074, 39.9042)]
2. GEOADD cities 121.4737 31.2304 "上海" - 将指定的地理空间位置(纬度、经度、名称)添加到指定的key中
执行前: [("北京", 116.4074, 39.9042)]
GeoHash计算:
上海坐标 -> GeoHash: 4054803516645980
执行后:
cities: ZSet {
dict: {
"北京" -> 4069885555089301
"上海" -> 4054803516645980
}
skiplist: 按GeoHash值排序
}
地理集合: [("上海", 121.4737, 31.2304), ("北京", 116.4074, 39.9042)]
3. GEOADD cities 113.2644 23.1291 "广州" - 将指定的地理空间位置(纬度、经度、名称)添加到指定的key中
执行前: [("上海", 121.4737, 31.2304), ("北京", 116.4074, 39.9042)]
GeoHash计算:
广州坐标 -> GeoHash: 4046432192579584
执行后:
地理集合: [("广州", 113.2644, 23.1291), ("上海", 121.4737, 31.2304), ("北京", 116.4074, 39.9042)]
4. GEOPOS cities "北京" - 从key里返回所有给定位置元素的位置(经度和纬度)
执行前: 地理集合包含北京
执行后: 返回 [116.40740036964416, 39.90419993200187]
解码过程:
1. 从dict获取"北京"的GeoHash值
2. 解码GeoHash为经纬度
3. 返回坐标(可能有精度损失)
5. GEODIST cities "北京" "上海" km - 返回两个给定位置之间的距离
执行前: 北京和上海都存在
执行后: 返回 "1067.3788"
计算过程:
1. 获取两个城市的坐标
2. 使用Haversine公式计算球面距离
3. 转换为指定单位(km)
6. GEORADIUS cities 116.4 39.9 100 km WITHDIST - 以给定的经纬度为中心,找出某一半径内的元素
执行前: 查询点(116.4, 39.9),半径100km
执行后: 返回 [["北京", "0.0668"]]
查询过程:
1. 计算查询点的GeoHash值
2. 确定GeoHash范围
3. 在ZSet中查找范围内的元素
4. 精确计算距离并过滤
5. 返回符合条件的地点
7. GEOHASH cities "北京" - 返回一个或多个位置元素的GeoHash表示
执行前: 北京存在于集合中
执行后: 返回 "wx4g0ec19tz00"
过程:
1. 获取"北京"的GeoHash数值
2. 转换为Base32字符串
3. 返回GeoHash字符串
flowchart TD
A["坐标(116.4, 39.9)"] --> B["经度二进制编码"]
A --> C["纬度二进制编码"]
B --> D["交错编码"]
C --> D
D --> E["GeoHash数值"]
E --> F["存储到ZSet"]
G["半径查询"] --> H["计算查询范围"]
H --> I["GeoHash范围查找"]
I --> J["精确距离计算"]
J --> K["返回结果"]
L["距离计算"] --> M["获取两点坐标"]
M --> N["Haversine公式"]
N --> O["球面距离"]
适合场景
- 位置服务
- 附近的人/店铺
- 地理围栏
- 路径规划
7. Redis基数统计 (HyperLogLog)
数据结构
HyperLogLog使用概率算法进行基数估算,占用固定的12KB内存。
graph TD
A[HyperLogLog基数统计] --> B["固定12KB内存"]
A --> C["16384个桶(2^14)"]
A --> D["每桶6位存储"]
subgraph "算法原理"
E["输入元素"] --> F["64位哈希值"]
F --> G["前14位: 桶索引<br/>(0-16383)"]
F --> H["后50位: 计算前导零"]
G --> I["定位桶位置"]
H --> J["统计连续0的个数"]
I --> K["更新桶值"]
J --> K
K --> L["取最大值存储"]
end
subgraph "基数估算公式"
M["所有桶值"] --> N["调和平均数计算"]
N --> O["α × m² / Σ(2^(-M[j]))"]
O --> P["小范围修正"]
O --> Q["大范围修正"]
P --> R["最终基数估计"]
Q --> R
end
subgraph "实际示例"
S["hash('user1')<br/>= 0x8A7B2C3D..."]
S --> T["前14位: 0x22EC<br/>桶索引: 8940"]
S --> U["后50位前导零: 3个"]
T --> V["registers[8940] = max(old, 3)"]
U --> V
end
基本命令使用与数据模拟
初始状态
Redis内存: 空
HyperLogLog结构: NULL
操作演示
1. PFADD unique_visitors "user1" - 添加指定元素到HyperLogLog中
执行前: unique_visitors不存在
执行后:
创建HyperLogLog结构:
unique_visitors: {
registers: [0, 0, 0, ..., 0] // 16384个桶,初始值为0
}
处理"user1":
1. hash("user1") = 0x8a7b2c3d4e5f6789
2. 前14位: 0x22ec (确定桶8940)
3. 后续位前导零: 3个
4. registers[8940] = max(0, 3) = 3
返回: 1 (估算基数变化)
2. PFADD unique_visitors "user2" - 添加指定元素到HyperLogLog中
执行前: registers[8940] = 3, 其他为0
执行后:
处理"user2":
1. hash("user2") = 0x1b2c3d4e5f6789ab
2. 前14位: 0x06cb (确定桶1739)
3. 后续位前导零: 2个
4. registers[1739] = max(0, 2) = 2
返回: 1 (估算基数变化)
3. PFADD unique_visitors "user3" - 添加指定元素到HyperLogLog中
执行前: registers[8940] = 3, registers[1739] = 2
执行后:
处理"user3":
1. hash("user3") = 0x3c4d5e6f7a8b9cde
2. 前14位: 0x0f13 (确定桶3859)
3. 后续位前导零: 1个
4. registers[3859] = max(0, 1) = 1
返回: 1 (估算基数变化)
4. PFADD unique_visitors "user1" - 添加指定元素到HyperLogLog中(重复元素)
执行前: 已有user1的记录
执行后:
处理"user1":
1. hash("user1") = 0x8a7b2c3d4e5f6789 (相同)
2. 前14位: 0x22ec (桶8940)
3. 后续位前导零: 3个
4. registers[8940] = max(3, 3) = 3 (无变化)
返回: 0 (估算基数无变化)
5. PFCOUNT unique_visitors - 返回给定HyperLogLog的基数估算值
执行前: registers中有3个非零值
执行后: 返回估算基数
计算过程:
1. 统计非零桶数: 3
2. 计算调和平均数:
raw_estimate = α * m² / Σ(2^(-registers[i]))
其中 α ≈ 0.7213, m = 16384
3. 应用偏差修正
4. 最终估算: 约3
6. 合并操作演示
PFADD hll1 "a" "b" "c" - 创建第一个HyperLogLog并添加元素
hll1状态:
registers[hash("a")的桶] = 前导零数
registers[hash("b")的桶] = 前导零数
registers[hash("c")的桶] = 前导零数
PFADD hll2 "c" "d" "e" - 创建第二个HyperLogLog并添加元素
hll2状态:
registers[hash("c")的桶] = 前导零数 (可能与hll1不同桶)
registers[hash("d")的桶] = 前导零数
registers[hash("e")的桶] = 前导零数
PFMERGE result hll1 hll2 - 将多个HyperLogLog合并为一个HyperLogLog
result状态:
对每个桶i: result.registers[i] = max(hll1.registers[i], hll2.registers[i])
PFCOUNT result - 返回合并后HyperLogLog的基数估算值
返回: 合并后的基数估算 (约5)
sequenceDiagram
participant U as User
participant H as HyperLogLog
participant B as Buckets
U->>H: PFADD key "user1"
H->>H: hash("user1")
H->>B: 更新bucket[8940] = 3
H-->>U: 1
U->>H: PFADD key "user2"
H->>H: hash("user2")
H->>B: 更新bucket[1739] = 2
H-->>U: 1
U->>H: PFADD key "user1"
H->>H: hash("user1")
H->>B: bucket[8940] 无变化
H-->>U: 0
U->>H: PFCOUNT key
H->>B: 读取所有buckets
H->>H: 计算调和平均数
H-->>U: 估算基数
适合场景
- 网站UV统计
- 去重计数
- 大数据基数估算
- 实时统计分析
8. Redis位图 (Bitmap)
数据结构
Bitmap基于字符串实现,每个位可以存储0或1,支持位操作。
graph TD
A[Bitmap] --> B["String字符串"]
B --> C["字节数组"]
C --> D["位操作"]
E["位图示例"] --> F["用户签到: 0110101"]
F --> G["第0天: 0 (未签到)"]
F --> H["第1天: 1 (已签到)"]
F --> I["第2天: 1 (已签到)"]
F --> J["第3天: 0 (未签到)"]
K[位运算] --> L["AND: 交集"]
K --> M["OR: 并集"]
K --> N["XOR: 异或"]
K --> O["NOT: 取反"]
基本命令使用与数据模拟
初始状态
Redis内存: 空
Bitmap结构: NULL
操作演示
1. SETBIT user:sign:2024 0 1 - 对key所储存的字符串值,设置或清除指定偏移量上的位(bit)
执行前: user:sign:2024不存在
执行后:
创建字符串结构:
user:sign:2024: SDS {
len: 1
alloc: 1
buf: [0x80] // 二进制: 10000000
}
位图状态: [1,0,0,0,0,0,0,0]
第0天: 已签到
2. SETBIT user:sign:2024 1 0 - 对key所储存的字符串值,设置或清除指定偏移量上的位(bit)
执行前: buf = [0x80] // 10000000
执行后:
buf = [0x80] // 10000000 (第1位本来就是0)
位图状态: [1,0,0,0,0,0,0,0]
第1天: 未签到
3. SETBIT user:sign:2024 2 1 - 对key所储存的字符串值,设置或清除指定偏移量上的位(bit)
执行前: buf = [0x80] // 10000000
执行后:
buf = [0xA0] // 10100000
位图状态: [1,0,1,0,0,0,0,0]
第2天: 已签到
4. SETBIT user:sign:2024 8 1 - 对key所储存的字符串值,设置或清除指定偏移量上的位(bit)
执行前: buf = [0xA0], len = 1
执行后:
扩展字符串:
user:sign:2024: SDS {
len: 2
alloc: 2
buf: [0xA0, 0x80] // 10100000 10000000
}
位图状态: [1,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0]
第8天: 已签到
5. GETBIT user:sign:2024 0 - 对key所储存的字符串值,获取指定偏移量上的位(bit)
执行前: buf = [0xA0, 0x80]
执行后: 返回 1
查找过程:
1. 计算字节位置: 0 / 8 = 0
2. 计算位位置: 0 % 8 = 0
3. 检查buf[0]的第0位: (0xA0 >> 7) & 1 = 1
6. GETBIT user:sign:2024 1 - 对key所储存的字符串值,获取指定偏移量上的位(bit)
执行前: buf = [0xA0, 0x80]
执行后: 返回 0
查找过程:
1. 计算字节位置: 1 / 8 = 0
2. 计算位位置: 1 % 8 = 1
3. 检查buf[0]的第1位: (0xA0 >> 6) & 1 = 0
7. BITCOUNT user:sign:2024 - 计算给定字符串中,被设置为1的比特位的数量
执行前: buf = [0xA0, 0x80] // 10100000 10000000
执行后: 返回 3
计算过程:
1. 遍历每个字节
2. 使用位计数算法统计1的个数
3. buf[0] = 0xA0 有2个1
4. buf[1] = 0x80 有1个1
5. 总计: 3个1
8. 位运算演示
SETBIT bitmap1 0 1 - 设置bitmap1的第0位为1
SETBIT bitmap1 1 0 - 设置bitmap1的第1位为0
SETBIT bitmap1 2 1 - 设置bitmap1的第2位为1
bitmap1: buf = [0xA0] // 10100000
SETBIT bitmap2 0 0 - 设置bitmap2的第0位为0
SETBIT bitmap2 1 1 - 设置bitmap2的第1位为1
SETBIT bitmap2 2 1 - 设置bitmap2的第2位为1
bitmap2: buf = [0x60] // 01100000
BITOP AND result bitmap1 bitmap2 - 对一个或多个保存二进制位的字符串key进行位元操作
计算过程:
result.buf[0] = bitmap1.buf[0] & bitmap2.buf[0]
= 0xA0 & 0x60
= 0x20 // 00100000
result位图: [0,0,1,0,0,0,0,0]
BITOP OR result bitmap1 bitmap2 - 对一个或多个保存二进制位的字符串key进行位元操作
计算过程:
result.buf[0] = bitmap1.buf[0] | bitmap2.buf[0]
= 0xA0 | 0x60
= 0xE0 // 11100000
result位图: [1,1,1,0,0,0,0,0]
BITOP XOR result bitmap1 bitmap2 - 对一个或多个保存二进制位的字符串key进行位元操作
计算过程:
result.buf[0] = bitmap1.buf[0] ^ bitmap2.buf[0]
= 0xA0 ^ 0x60
= 0xC0 // 11000000
result位图: [1,1,0,0,0,0,0,0]
flowchart TD
A["SETBIT key 0 1"] --> B["创建字符串"]
B --> C["buf[0] = 0x80"]
C --> D["SETBIT key 2 1"]
D --> E["buf[0] = 0xA0"]
E --> F["SETBIT key 8 1"]
F --> G["扩展到2字节"]
G --> H["buf = [0xA0, 0x80]"]
I["bitmap1: 0xA0"] --> J["AND运算"]
K["bitmap2: 0x60"] --> J
J --> L["result: 0x20"]
I --> M["OR运算"]
K --> M
M --> N["result: 0xE0"]
I --> O["XOR运算"]
K --> O
O --> P["result: 0xC0"]
适合场景
- 用户签到统计
- 在线用户统计
- 权限管理
- 布隆过滤器
9. Redis位域 (Bitfield)
数据结构
Bitfield允许在位图中操作任意位宽的整数,支持有符号和无符号整数。
graph TD
A[Bitfield] --> B["位图基础"]
B --> C["任意位宽整数"]
C --> D["有符号整数"]
C --> E["无符号整数"]
F["位域操作"] --> G["GET: 获取位域"]
F --> H["SET: 设置位域"]
F --> I["INCRBY: 增加位域"]
J["溢出处理"] --> K["WRAP: 回绕"]
J --> L["SAT: 饱和"]
J --> M["FAIL: 失败"]
基本命令使用与数据模拟
初始状态
Redis内存: 空
Bitfield结构: NULL (基于String)
操作演示
1. BITFIELD mykey SET u8 0 255 - 设置指定位域的值
执行前: mykey不存在
执行后:
创建字符串:
mykey: SDS {
len: 1
alloc: 1
buf: [0xFF] // 255的二进制: 11111111
}
位域布局: [11111111] (位置0-7: 255)
返回: [0] (原值)
2. BITFIELD mykey SET i8 8 -128 - 设置指定位域的值
执行前: buf = [0xFF]
执行后:
扩展字符串:
mykey: SDS {
len: 2
alloc: 2
buf: [0xFF, 0x80] // 255, -128的二进制表示
}
位域布局: [11111111][10000000]
位置0-7: 255 (u8)
位置8-15: -128 (i8)
返回: [0] (原值)
3. BITFIELD mykey GET u8 0 - 获取指定位域的值
执行前: buf = [0xFF, 0x80]
执行后: 返回 [255]
读取过程:
1. 从位置0开始读取8位
2. 读取buf[0] = 0xFF
3. 解释为无符号8位整数: 255
4. BITFIELD mykey GET i8 8 - 获取指定位域的值
执行前: buf = [0xFF, 0x80]
执行后: 返回 [-128]
读取过程:
1. 从位置8开始读取8位
2. 读取buf[1] = 0x80
3. 解释为有符号8位整数: -128
5. BITFIELD mykey INCRBY u8 0 1 - 对指定位域进行增量操作
执行前: buf = [0xFF, 0x80], 位置0值为255
执行后:
计算: 255 + 1 = 256
溢出处理(默认WRAP): 256 % 256 = 0
buf = [0x00, 0x80]
返回: [0] (新值)
6. BITFIELD mykey INCRBY i8 8 1 - 对指定位域进行增量操作
执行前: buf = [0x00, 0x80], 位置8值为-128
执行后:
计算: -128 + 1 = -127
buf = [0x00, 0x81]
返回: [-127] (新值)
7. 溢出控制演示
BITFIELD mykey SET u8 16 200 - 设置位置16的8位无符号整数为200
执行后: buf = [0x00, 0x81, 0xC8]
位域布局: [00000000][10000001][11001000]
BITFIELD mykey OVERFLOW SAT INCRBY u8 16 100 - 设置溢出策略为饱和模式并增加100
执行前: 位置16值为200
计算: 200 + 100 = 300
溢出处理(SAT): min(300, 255) = 255
执行后: buf = [0x00, 0x81, 0xFF]
返回: [255]
BITFIELD mykey OVERFLOW FAIL INCRBY u8 16 1 - 设置溢出策略为失败模式并增加1
执行前: 位置16值为255
计算: 255 + 1 = 256 (溢出)
溢出处理(FAIL): 返回NULL,不修改
执行后: buf不变
返回: [null]
BITFIELD mykey OVERFLOW WRAP INCRBY u8 16 1 - 设置溢出策略为回绕模式并增加1
执行前: 位置16值为255
计算: 255 + 1 = 256
溢出处理(WRAP): 256 % 256 = 0
执行后: buf = [0x00, 0x81, 0x00]
返回: [0]
8. 复杂位域操作
BITFIELD counters SET u4 0 15 SET u4 4 10 SET u4 8 5
执行后:
buf = [0xFA, 0x50] // 11111010 01010000
位域布局:
位置0-3: 15 (1111)
位置4-7: 10 (1010)
位置8-11: 5 (0101)
位置12-15: 0 (0000)
BITFIELD counters GET u4 0 GET u4 4 GET u4 8
返回: [15, 10, 5]
BITFIELD counters INCRBY u4 0 1 INCRBY u4 4 1 INCRBY u4 8 1
计算:
位置0: 15 + 1 = 16, 溢出回绕为0
位置4: 10 + 1 = 11
位置8: 5 + 1 = 6
执行后: buf = [0x0B, 0x60] // 00001011 01100000
返回: [0, 11, 6]
sequenceDiagram
participant C as Client
participant B as Bitfield
participant M as Memory
C->>B: SET u8 0 255
B->>M: 写入位置0-7: 11111111
M-->>B: 完成
B-->>C: [0]
C->>B: SET i8 8 -128
B->>M: 写入位置8-15: 10000000
M-->>B: 完成
B-->>C: [0]
C->>B: INCRBY u8 0 1
B->>M: 读取位置0-7: 255
B->>B: 计算255+1=256, 溢出处理
B->>M: 写入位置0-7: 00000000
M-->>B: 完成
B-->>C: [0]
适合场景
- 计数器数组
- 统计信息存储
- 紧凑的数值存储
- 位级别的精确控制
10. Redis流 (Stream)
数据结构
Redis Stream是一个日志型数据结构,支持消息的持久化和消费者组。
graph TD
A[Stream] --> B["Radix Tree"]
B --> C["消息节点"]
C --> D["消息ID"]
C --> E["字段值对"]
F[消费者组] --> G["Consumer Group"]
G --> H["Consumer 1"]
G --> I["Consumer 2"]
G --> J["Consumer 3"]
K["消息流转"] --> L["生产者 XADD"]
L --> M["Stream存储"]
M --> N["消费者 XREAD"]
N --> O["确认 XACK"]
基本命令使用与数据模拟
初始状态
Redis内存: 空
Stream结构: NULL
操作演示
1. XADD mystream * name "张三" age 25 - 向流中添加消息
执行前: mystream不存在
执行后:
创建Stream结构:
mystream: {
radix_tree: {
length: 1
entries: [
{
id: "1640995200000-0"
fields: {"name": "张三", "age": "25"}
}
]
}
last_id: "1640995200000-0"
length: 1
}
返回: "1640995200000-0"
2. XADD mystream * name "李四" age 30 - 向流中添加消息
执行前: length = 1, last_id = "1640995200000-0"
执行后:
mystream: {
radix_tree: {
length: 2
entries: [
{
id: "1640995200000-0"
fields: {"name": "张三", "age": "25"}
},
{
id: "1640995200001-0"
fields: {"name": "李四", "age": "30"}
}
]
}
last_id: "1640995200001-0"
length: 2
}
返回: "1640995200001-0"
3. XREAD STREAMS mystream 0 - 从一个或多个流中读取消息
执行前: Stream包含2条消息
执行后: 返回所有消息
[ [ "mystream", [ ["1640995200000-0", ["name", "张三", "age", "25"]],
["1640995200001-0", ["name", "李四", "age", "30"]]
]
]
]
4. XGROUP CREATE mystream mygroup 0 - 创建消费者组
执行前: Stream存在,消费者组不存在
执行后:
mystream: {
radix_tree: {...} // 消息数据不变
consumer_groups: {
"mygroup": {
last_delivered_id: "0-0"
consumers: {}
pending: {}
}
}
}
返回: OK
5. XREADGROUP GROUP mygroup consumer1 STREAMS mystream > - 从消费者组中读取消息
执行前: 消费者组存在,consumer1首次消费
执行后:
消费者组状态:
"mygroup": {
last_delivered_id: "1640995200001-0"
consumers: {
"consumer1": {
pending: {
"1640995200000-0": timestamp,
"1640995200001-0": timestamp
}
}
}
pending: {
"1640995200000-0": "consumer1",
"1640995200001-0": "consumer1"
}
}
返回: 所有未消费的消息
6. XACK mystream mygroup 1640995200000-0 - 确认消息已被消费者组中的消费者处理
执行前: 消息在pending列表中
执行后:
消费者组状态:
"mygroup": {
last_delivered_id: "1640995200001-0"
consumers: {
"consumer1": {
pending: {
"1640995200001-0": timestamp
}
}
}
pending: {
"1640995200001-0": "consumer1"
}
}
返回: 1 (确认的消息数)
7. XINFO - 以一种易于理解和阅读的格式,返回关于Redis服务器的各种信息和统计数值 STREAM mystream - 返回流的详细信息
执行前: Stream包含完整信息
执行后: 返回Stream详细信息
[
"length", 2,
"radix-tree-keys", 1,
"radix-tree-nodes", 2,
"last-generated-id", "1640995200001-0",
"groups", 1,
"first-entry", ["1640995200000-0", ["name", "张三", "age", "25"]],
"last-entry", ["1640995200001-0", ["name", "李四", "age", "30"]]
]
8. 消息修剪演示
XTRIM mystream MAXLEN 1 - 修剪流,限制流的长度
执行前: Stream包含2条消息
执行后:
mystream: {
radix_tree: {
length: 1
entries: [
{
id: "1640995200001-0"
fields: {"name": "李四", "age": "30"}
}
]
}
last_id: "1640995200001-0"
length: 1
}
删除最旧的消息,保留最新的1条
返回: 1 (删除的消息数)
sequenceDiagram
participant P as Producer
participant S as Stream
participant G as ConsumerGroup
participant C as Consumer
P->>S: XADD * name "张三" age 25
S-->>P: "1640995200000-0"
P->>S: XADD * name "李四" age 30
S-->>P: "1640995200001-0"
Note over G: XGROUP CREATE mystream mygroup 0
C->>G: XREADGROUP GROUP mygroup consumer1 STREAMS mystream >
G->>S: 读取未消费消息
S-->>G: 返回消息列表
G->>G: 添加到pending列表
G-->>C: 返回消息
C->>G: XACK mystream mygroup 1640995200000-0
G->>G: 从pending列表移除
G-->>C: 1
flowchart TD
A["空Stream"] --> B["XADD消息1"]
B --> C["Stream: [消息1]"]
C --> D["XADD消息2"]
D --> E["Stream: [消息1, 消息2]"]
E --> F["创建消费者组"]
F --> G["消费者读取"]
G --> H["消息进入pending"]
H --> I["消费者确认"]
I --> J["消息从pending移除"]
K["消息修剪"] --> L["保留最新N条"]
L --> M["删除旧消息"]
适合场景
- 消息队列
- 事件溯源
- 日志收集
- 实时数据流处理
总结
Redis提供了丰富的数据结构,每种数据结构都有其特定的应用场景和内部实现机制:
graph LR
A[Redis数据结构] --> B[String]
A --> C[List]
A --> D[Hash]
A --> E[Set]
A --> F[ZSet]
A --> G[GEO]
A --> H[HyperLogLog]
A --> I[Bitmap]
A --> J[Bitfield]
A --> K[Stream]
B --> B1["SDS结构<br/>缓存/计数"]
C --> C1["双向链表<br/>队列/栈"]
D --> D1["哈希表<br/>对象存储"]
E --> E1["哈希表<br/>去重/集合运算"]
F --> F1["跳跃表+哈希表<br/>排行榜"]
G --> G1["ZSet+GeoHash<br/>地理位置"]
H --> H1["概率算法<br/>基数统计"]
I --> I1["字符串+位操作<br/>位图统计"]
J --> J1["字符串+位域<br/>紧凑存储"]
K --> K1["基数树<br/>消息流"]
选择建议
- 简单键值存储: 使用String
- 队列/栈操作: 使用List
- 对象属性存储: 使用Hash
- 去重/集合运算: 使用Set
- 排序/排行榜: 使用ZSet
- 地理位置服务: 使用GEO
- 大数据去重计数: 使用HyperLogLog
- 位级别统计: 使用Bitmap
- 紧凑数值存储: 使用Bitfield
- 消息队列/事件流: 使用Stream
11. Redis其他常用指令
除了各种数据结构的专用命令外,Redis还提供了许多通用的管理和操作命令。
11.1 键管理命令
基本键操作
graph TD
A[键管理命令] --> B[EXISTS]
A --> C[TYPE]
A --> D[DEL]
A --> E[EXPIRE]
A --> F[TTL]
A --> G[KEYS]
A --> H[SCAN]
B --> B1["检查键是否存在"]
C --> C1["获取键的数据类型"]
D --> D1["删除一个或多个键"]
E --> E1["设置键的过期时间"]
F --> F1["查看键的剩余生存时间"]
G --> G1["查找匹配模式的键"]
H --> H1["迭代数据库中的键"]
操作演示
1. 键存在性检查
初始状态: Redis中有键 "user:1", "user:2"
EXISTS user:1 - 检查给定key是否存在
返回: 1 (存在)
EXISTS user:3 - 检查给定key是否存在
返回: 0 (不存在)
EXISTS user:1 user:2 user:3 - 检查多个key是否存在
返回: 2 (存在的键数量)
2. 键类型查询
设置不同类型的键:
SET name "张三" # String类型
LPUSH list "item" # List类型
HSET hash field value # Hash类型
TYPE name - 返回key所储存的值的类型
返回: "string"
TYPE list - 返回key所储存的值的类型
返回: "list"
TYPE hash - 返回key所储存的值的类型
返回: "hash"
TYPE nonexistent - 返回key所储存的值的类型
返回: "none"
3. 键删除操作
初始状态: 存在键 key1, key2, key3
DEL key1 - 删除一个或多个key
执行后: key1被删除
返回: 1 (删除的键数量)
DEL key2 key3 key4 - 删除一个或多个key
执行后: key2和key3被删除,key4不存在
返回: 2 (实际删除的键数量)
4. 过期时间管理
SET session:123 "user_data"
EXPIRE session:123 3600 - 为给定key设置过期时间
执行后: 键将在3600秒后过期
返回: 1 (设置成功)
TTL session:123 - 以秒为单位,返回给定key的剩余生存时间
返回: 3599 (剩余秒数,会递减)
等待过期后:
TTL session:123
返回: -2 (键已过期不存在)
TTL persistent_key
返回: -1 (键存在但无过期时间)
11.2 数据库管理命令
flowchart LR
A[数据库管理] --> B[SELECT]
A --> C[DBSIZE - 返回当前数据库的key的数量]
A --> D[FLUSHDB]
A --> E[FLUSHALL]
A --> F[MOVE]
B --> B1["切换数据库"]
C --> C1["查看当前数据库键数量"]
D --> D1["清空当前数据库"]
E --> E1["清空所有数据库"]
F --> F1["移动键到其他数据库"]
操作演示
1. 数据库切换
默认连接到数据库0
SELECT 1 - 切换到指定的数据库
执行后: 切换到数据库1
返回: OK
SET key "value in db1"
SELECT 0
GET key
返回: (nil) - 因为key在数据库1中
2. 数据库大小查询
当前数据库包含100个键
DBSIZE
返回: 100
添加新键后:
SET newkey "value" - 设置指定key的值
DBSIZE
返回: 101
3. 键在数据库间移动
当前在数据库0,存在键 "user:1"
MOVE user:1 1 - 将当前数据库的key移动到给定的数据库db当中
执行后:
- 数据库0中的user:1被删除
- 数据库1中创建user:1
返回: 1 (移动成功)
MOVE nonexistent 1 - 将当前数据库的key移动到给定的数据库db当中
返回: 0 (键不存在,移动失败)
11.3 服务器信息命令
graph TD
A[服务器信息] --> B[INFO]
A --> C[CONFIG]
A --> D[CLIENT]
A --> E[MONITOR]
A --> F[SLOWLOG]
B --> B1["服务器统计信息"]
C --> C1["配置参数管理"]
D --> D1["客户端连接管理"]
E --> E1["实时监控命令"]
F --> F1["慢查询日志"]
操作演示
1. 服务器信息查询
INFO
返回服务器详细信息:
# Server
redis_version:7.0.0
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:0
redis_mode:standalone
os:Linux 5.4.0-74-generic x86_64
# Memory
used_memory:1048576
used_memory_human:1.00M
used_memory_rss:2097152
# Stats
total_connections_received:100
total_commands_processed:1000
instantaneous_ops_per_sec:10
2. 配置管理
CONFIG GET maxmemory - 获取运行时配置参数
返回:
1) "maxmemory"
2) "1073741824"
CONFIG SET maxmemory 2147483648 - 设置运行时配置参数
返回: OK
CONFIG GET save - 获取运行时配置参数
返回:
1) "save"
2) "900 1 300 10 60 10000"
3. 客户端管理
CLIENT LIST - 以人类可读的格式,返回所有连接到服务器的客户端信息和统计数据
返回当前连接的客户端信息:
id=3 addr=127.0.0.1:52555 fd=8 name= age=855 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
CLIENT SETNAME myclient - 为当前连接分配一个名字
返回: OK
CLIENT GETNAME - 返回当前连接的名字
返回: "myclient"
11.4 事务命令
sequenceDiagram
participant C as Client
participant R as Redis
C->>R: MULTI - 标记一个事务块的开始
R-->>C: OK
Note over R: 开始事务
C->>R: SET key1 value1
R-->>C: QUEUED
C->>R: INCR counter
R-->>C: QUEUED
C->>R: LPUSH list item
R-->>C: QUEUED
Note over R: 命令入队列
C->>R: EXEC - 执行所有事务块内的命令
R->>R: 原子执行所有命令
R-->>C: [OK, 1, 1]
Note over R: 事务完成
操作演示
1. 正常事务执行
MULTI
返回: OK
事务状态: 开始
SET account:1 100 - 设置指定key的值(在事务中)
返回: QUEUED
命令队列: ["SET account:1 100"]
SET account:2 200 - 设置指定key的值(在事务中)
返回: QUEUED
命令队列: ["SET account:1 100", "SET account:2 200"]
EXEC
执行过程:
1. SET account:1 100 -> OK
2. SET account:2 200 -> OK
返回: ["OK", "OK"]
事务状态: 完成
2. 事务取消
MULTI
返回: OK
SET key1 value1
返回: QUEUED
SET key2 value2
返回: QUEUED
DISCARD - 取消事务,放弃执行事务块内的所有命令
执行后: 清空命令队列,取消事务
返回: OK
结果: key1和key2都没有被设置
3. 监视键的事务
客户端1:
WATCH balance - 监视一个(或多个)key,如果在事务执行之前这个(或这些)key被其他命令所改动,那么事务将被打断
GET balance
返回: "100"
MULTI
SET balance 90
QUEUED
客户端2 (在客户端1执行EXEC前):
SET balance 50
返回: OK
客户端1:
EXEC
返回: (nil) - 事务被取消,因为balance被其他客户端修改
11.5 发布订阅命令
graph LR
A[Publisher] -->|PUBLISH| B[Channel]
B --> C[Subscriber1]
B --> D[Subscriber2]
B --> E[Subscriber3]
F[Pattern Subscriber] -->|PSUBSCRIBE| G["news.*"]
G --> H["news.sports"]
G --> I["news.tech"]
G --> J["news.finance"]
操作演示
1. 基本发布订阅
订阅者:
SUBSCRIBE news - 订阅给定的一个或多个频道的信息
返回:
1) "subscribe"
2) "news"
3) (integer) 1
发布者:
PUBLISH news "重要新闻内容" - 将信息发送到指定的频道
返回: 1 (接收消息的订阅者数量)
订阅者接收到:
1) "message"
2) "news"
3) "重要新闻内容"
2. 模式订阅
订阅者:
PSUBSCRIBE news.* - 订阅一个或多个符合给定模式的频道
返回:
1) "psubscribe"
2) "news.*"
3) (integer) 1
发布者:
PUBLISH news.sports "体育新闻" - 将信息发送到指定的频道
PUBLISH news.tech "科技新闻" - 将信息发送到指定的频道
PUBLISH weather.today "天气预报" - 将信息发送到指定的频道
订阅者接收到:
1) "pmessage"
2) "news.*"
3) "news.sports"
4) "体育新闻"
1) "pmessage"
2) "news.*"
3) "news.tech"
4) "科技新闻"
(weather.today不匹配模式,不会收到)
12. 总结
通过本文的详细分析和数据模拟演示,我们深入了解了Redis的核心数据结构和常用命令。Redis作为一个高性能的内存数据库,其精心设计的数据结构为不同的应用场景提供了最优的解决方案。
12.1 数据结构特点总结
核心数据结构概览
| 数据结构 | 底层实现 | 主要特点 | 时间复杂度 | 典型应用场景 |
|---|---|---|---|---|
| String | SDS动态字符串 | 二进制安全、自动扩容 | O(1) | 缓存、计数器、分布式锁 |
| List | 双向链表 | 快速头尾操作、有序 | O(1)头尾/O(N)中间 | 消息队列、最新列表 |
| Hash | 哈希表 | 字段级操作、内存优化 | O(1)平均 | 对象存储、用户信息 |
| Set | 哈希表 | 自动去重、集合运算 | O(1)平均 | 标签系统、好友关系 |
| ZSet | 跳跃表+哈希表 | 有序性、范围查询 | O(log N) | 排行榜、优先队列 |
| GEO | ZSet+GeoHash | 地理位置编码 | O(log N) | LBS服务、附近搜索 |
| HyperLogLog | 概率算法 | 固定内存、基数估算 | O(1) | UV统计、去重计数 |
| Bitmap | 字符串+位操作 | 极致空间效率 | O(1) | 用户签到、布隆过滤器 |
| Bitfield | 位域操作 | 紧凑数值存储 | O(1) | 数值数组、状态标记 |
| Stream | Radix Tree | 消息持久化、消费组 | O(log N) | 消息队列、事件流 |
数据结构选择指南
flowchart TD
A["数据存储需求"] --> B["简单键值对"]
A --> C["集合类数据"]
A --> D["有序数据"]
A --> E["特殊场景"]
B --> B1["String"]
B1 --> B2["✓ 缓存数据<br/>✓ 计数器<br/>✓ 分布式锁"]
C --> C1["需要去重?"]
C1 -->|是| C2["Set"]
C1 -->|否| C3["需要字段操作?"]
C3 -->|是| C4["Hash"]
C3 -->|否| C5["List"]
C2 --> C6["✓ 标签系统<br/>✓ 集合运算"]
C4 --> C7["✓ 对象存储<br/>✓ 用户信息"]
C5 --> C8["✓ 消息队列<br/>✓ 时间线"]
D --> D1["ZSet"]
D1 --> D2["✓ 排行榜<br/>✓ 范围查询<br/>✓ 优先队列"]
E --> E1["地理位置"]
E --> E2["基数统计"]
E --> E3["位操作"]
E --> E4["消息流"]
E1 --> E5["GEO"]
E2 --> E6["HyperLogLog"]
E3 --> E7["Bitmap/Bitfield"]
E4 --> E8["Stream"]
12.2 性能对比分析
| 数据结构 | 插入复杂度 | 查找复杂度 | 删除复杂度 | 内存效率 | 适用场景 |
|---|---|---|---|---|---|
| String | O(1) | O(1) | O(1) | 高 | 简单键值存储 |
| List | O(1)头尾 | O(N) | O(1)头尾 | 中 | 队列、栈操作 |
| Hash | O(1)平均 | O(1)平均 | O(1)平均 | 高 | 对象属性存储 |
| Set | O(1)平均 | O(1)平均 | O(1)平均 | 高 | 去重、集合运算 |
| ZSet | O(log N) | O(1)按成员 O(log N)按分数 | O(log N) | 中 | 排序、范围查询 |
| GEO | O(log N) | O(log N) | O(log N) | 中 | 地理位置查询 |
| HyperLogLog | O(1) | O(1) | - | 极高 | 大数据基数统计 |
| Bitmap | O(1) | O(1) | O(1) | 极高 | 位级别统计 |
| Bitfield | O(1) | O(1) | O(1) | 极高 | 紧凑数值存储 |
| Stream | O(1) | O(log N) | O(1) | 中 | 消息流处理 |
12.3 选择决策树
flowchart TD
A["需要存储什么数据?"] --> B["简单值"]
A --> C["复杂结构"]
B --> D["需要数值操作?"]
D -->|是| E["String"]
D -->|否| F["需要位操作?"]
F -->|是| G["Bitmap/Bitfield"]
F -->|否| E
C --> H["需要排序?"]
H -->|是| I["ZSet"]
H -->|否| J["需要去重?"]
J -->|是| K["Set"]
J -->|否| L["需要顺序?"]
L -->|是| M["List"]
L -->|否| N["Hash"]
O["特殊需求"] --> P["地理位置"]
O --> Q["基数统计"]
O --> R["消息队列"]
P --> S["GEO"]
Q --> T["HyperLogLog"]
R --> U["Stream"]
12.4 最佳实践建议
12.4.1 性能优化
-
键命名规范
- 使用有意义的前缀:
user:1001:profile - 避免过长的键名
- 使用一致的命名约定
- 使用有意义的前缀:
-
内存优化
- 合理设置过期时间
- 使用压缩友好的数据结构
- 定期清理无用数据
-
操作优化
- 批量操作优于单个操作
- 使用管道减少网络往返
- 避免大键操作阻塞
12.4.2 数据安全
-
持久化策略
- RDB:适合备份和灾难恢复
- AOF:适合数据安全要求高的场景
- 混合持久化:兼顾性能和安全
-
高可用部署
- 主从复制:读写分离
- 哨兵模式:自动故障转移
- 集群模式:水平扩展
12.4.3 监控运维
-
关键指标监控
- 内存使用率
- 命令执行延迟
- 连接数和QPS
- 慢查询日志
-
容量规划
- 预估数据增长
- 监控内存碎片率
- 合理配置最大内存
12.5 结语
Redis的强大之处在于其丰富的数据结构和高效的实现。每种数据结构都针对特定的使用场景进行了优化,理解其内部机制有助于我们:
- 正确选择:根据业务需求选择最适合的数据结构
- 性能优化:了解操作复杂度,避免性能陷阱
- 内存管理:合理使用内存,提高存储效率
- 架构设计:设计可扩展、高性能的系统架构
通过本文的学习,相信你已经掌握了Redis数据结构的核心知识。在实际应用中,建议结合具体的业务场景,灵活运用这些数据结构,充分发挥Redis的性能优势。
graph LR
A["理论学习"] --> B["实践应用"]
B --> C["性能调优"]
C --> D["架构优化"]
D --> E["业务成功"]
F["持续学习"] --> A
E --> F
style A fill:#e1f5fe
style B fill:#f3e5f5
style C fill:#fff3e0
style D fill:#e8f5e8
style E fill:#ffebee
style F fill:#fce4ec
Redis的世界博大精深,本文只是一个开始。继续探索Redis的高级特性,如Lua脚本、模块系统、集群管理等,将帮助你在数据存储和处理领域达到更高的水平。