前言

Redis作为现代高性能架构中不可或缺的组件,以其强大的内存数据存储能力和丰富的数据结构支持,为开发者提供了构建高效系统的基础。本文将全面解析Redis的核心数据结构,深入探讨它们的内部实现、应用场景及最佳实践,帮助读者更好地发挥Redis的强大功能。

Redis数据结构概述

Redis不仅仅是简单的键值存储,它支持多种数据结构,使其在不同场景下都能发挥出色的性能。Redis的核心竞争力之一就是提供了丰富的数据结构和操作,让开发者能够更贴近业务需求地使用数据库。

graph TD
    A[Redis数据结构] --> B[基础数据结构]
    A --> C[高级数据结构]
    B --> D[String字符串]
    B --> E[List列表]
    B --> F[Hash哈希]
    B --> G[Set集合]
    B --> H[Sorted Set有序集合]
    C --> I[Bitmap位图]
    C --> J[HyperLogLog]
    C --> K[GEO地理空间]
    C --> L[Stream流]

基础数据结构

1. String(字符串)

内部实现

String是Redis最基本的数据类型,内部实现上采用了SDS(Simple Dynamic String)结构,而非C语言的原生字符串。

1
2
3
┌─────────────┬─────────────┬─────────────┐
│ len(已用长度)│alloc(总长度)│ 字节数组 │
└─────────────┴─────────────┴─────────────┘

SDS具有以下特点:

  • O(1)时间复杂度获取字符串长度
  • 空间预分配策略,减少内存再分配次数
  • 二进制安全,可以存储任何二进制数据
  • 兼容部分C字符串函数

应用场景

  • 缓存对象:直接存储序列化后的对象
  • 计数器:利用INCR/DECR等原子操作
  • 分布式锁:结合SET NX EX命令
  • Session存储:存储用户会话信息

代码示例

1
2
3
4
5
6
7
8
9
10
# 设置字符串
SET user:1 "张三"

# 原子递增
SET counter 0
INCR counter
INCRBY counter 10

# 分布式锁
SET lock:resource "unique_value" NX EX 10

2. List(列表)

内部实现

List在Redis 3.2之前使用压缩列表(ziplist)和双向链表(linkedlist)实现,3.2之后统一使用quicklist实现。

1
2
3
       ┌─────────┐      ┌─────────┐      ┌─────────┐
head ──► ziplist ├───► ziplist ├───► ziplist ◄── tail
└─────────┘ └─────────┘ └─────────┘

quicklist是一个双向链表,链表中每个节点都是一个ziplist,兼顾了链表和压缩列表的优点:

  • 内存利用率高
  • 快速的插入和删除

应用场景

  • 消息队列:使用LPUSH+RPOP或RPUSH+LPOP
  • 最新数据:如朋友圈最新动态、新闻列表
  • 文章评论列表:按时间顺序排列的评论
  • 任务队列:结合阻塞操作实现任务分发

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
# 消息队列操作
LPUSH queue:tasks "task1"
LPUSH queue:tasks "task2"
RPOP queue:tasks

# 最新消息(限制长度)
LPUSH news:latest "news1"
LPUSH news:latest "news2"
LTRIM news:latest 0 99 # 只保留最新的100条

# 阻塞式队列
BRPOP queue:tasks 5 # 阻塞最多5秒

3. Hash(哈希)

内部实现

Hash是字段和值的映射表,内部实现可以是压缩列表(ziplist)或哈希表(hashtable),取决于元素数量和长度。

1
2
3
4
5
6
7
8
9
┌─────────────────┐
│ hashtable │
├─────────┬───────┤
│ field1 │value1 │
├─────────┼───────┤
│ field2 │value2 │
├─────────┼───────┤
│ ... │ ... │
└─────────┴───────┘

ziplist实现时,元素依次排列,每个entry存储一对field-value;当数据变大时,会转为hashtable实现。

应用场景

  • 用户信息存储:每个用户一个哈希表,字段为属性
  • 购物车:用户ID为key,商品ID为field,数量为value
  • 配置信息:集中管理系统配置参数
  • 计数统计:如文章的各种统计数据

代码示例

1
2
3
4
5
6
7
8
9
10
# 用户信息操作
HSET user:1 name "张三" age 25 city "北京"
HGET user:1 name
HMGET user:1 name age city

# 购物车操作
HSET cart:user:1 product:1001 2
HSET cart:user:1 product:1002 1
HINCRBY cart:user:1 product:1001 1 # 增加数量
HDEL cart:user:1 product:1002 # 删除商品

4. Set(集合)

内部实现

Set是String类型的无序集合,内部实现是一个值为null的哈希表(hashtable)或整数集合(intset)。

1
2
3
4
5
6
7
8
9
┌─────────────────┐
│ hashtable │
├─────────┬───────┤
│ member1 │ NULL │
├─────────┼───────┤
│ member2 │ NULL │
├─────────┼───────┤
│ ... │ NULL │
└─────────┴───────┘

当集合中只包含整数且元素数量较少时,Redis会使用intset来存储,以节省内存。

应用场景

  • 标签系统:用户、文章等的标签管理
  • 唯一性验证:跟踪独立IP、去重计数
  • 关注/粉丝系统:社交关系管理
  • 随机抽奖:使用SRANDMEMBER或SPOP

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
# 标签操作
SADD article:1:tags "Redis" "数据库" "缓存"
SMEMBERS article:1:tags

# 社交关系
SADD user:1:following 2 3 4 # 用户1关注的人
SADD user:2:followers 1 5 6 # 用户2的粉丝

# 共同关注的人
SINTER user:1:following user:2:following

# 推荐关注(关注的人也关注的人)
SDIFF user:2:following user:1:following

5. Sorted Set(有序集合)

内部实现

Sorted Set(ZSet)结合了Set和Hash的特性,内部使用skiplist(跳跃表)和hashtable实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
                         ┌───────┐
┌───────────► level 3 ├─────────────────────────────►
│ └───┬───┘
│ │
│ ┌───▼───┐ ┌───────┐
└───────────► level 2 ├───────────────► level 2├────►
│ └───┬───┘ └───┬───┘
│ │ │
┌─────┐ │ ┌───▼───┐ ┌───────┐ ┌───▼───┐
│head ├────┴───────────► level 1 ├───► level 1├───► level 1├────►
└─────┘ │ └───┬───┘ └───┬───┘ └───┬───┘
│ │ │ │
│ ┌───▼───┐ ┌───▼───┐ ┌───▼───┐
└───────────► level 0 ├───► level 0├───► level 0├────►
└───────┘ └───────┘ └───────┘

跳跃表提供了O(log N)的平均时间复杂度,用于根据分数(score)排序;hashtable用O(1)时间复杂度快速查找成员。

应用场景

  • 排行榜:游戏分数、销售排名等
  • 优先级队列:按权重处理任务
  • 范围查询:按分数段、时间段查询
  • 延迟队列:使用时间戳作为score

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
# 排行榜操作
ZADD leaderboard 3000 "玩家A"
ZADD leaderboard 2500 "玩家B"
ZADD leaderboard 3500 "玩家C"

# 获取排名前三
ZREVRANGE leaderboard 0 2 WITHSCORES

# 获取某玩家排名
ZREVRANK leaderboard "玩家B"

# 分数区间查询
ZRANGEBYSCORE leaderboard 2000 3000 WITHSCORES

高级数据结构

1. Bitmap(位图)

内部实现

Bitmap不是一个独立的数据结构,而是基于String类型的位操作。String类型最大能存储512MB,即可以设置约2^32个位。

1
2
3
4
┌───┬───┬───┬───┬───┬───┬───┬───┐
│bit│bit│bit│bit│bit│bit│bit│bit│
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │
└───┴───┴───┴───┴───┴───┴───┴───┘

应用场景

  • 用户签到记录:每一位表示一天
  • 在线状态统计:用户ID作为偏移量
  • 布隆过滤器的实现:判断元素是否可能存在
  • 统计活跃用户:活跃用户的位图计算

代码示例

1
2
3
4
5
6
7
8
9
10
11
# 用户签到
SETBIT user:1:sign:202103 18 1 # 用户1在3月18日签到

# 检查签到状态
GETBIT user:1:sign:202103 18

# 统计签到次数
BITCOUNT user:1:sign:202103

# 多用户活跃状态合并
BITOP OR result active:20210316 active:20210317 active:20210318

2. HyperLogLog

内部实现

HyperLogLog是用于基数统计的概率数据结构,只需要花费12KB内存就可以计算接近2^64个不同元素的基数。

其核心思想是通过元素的哈希值中最大连续0的个数来估算集合的基数。

应用场景

  • UV统计:网站访问用户数
  • 搜索词统计:不重复搜索词数量
  • 用户行为分析:独立用户行为数量

代码示例

1
2
3
4
5
6
7
8
9
# 添加元素
PFADD page:uv:20210318 "user1" "user2" "user3"
PFADD page:uv:20210318 "user4" "user1" # user1重复

# 获取基数统计
PFCOUNT page:uv:20210318 # 返回4

# 合并多天数据
PFMERGE page:uv:week page:uv:20210318 page:uv:20210317 page:uv:20210316

3. GEO(地理空间)

内部实现

GEO底层基于Sorted Set实现,使用GeoHash编码将二维经纬度转为一维字符串,并以此作为排序的score。

应用场景

  • 附近的人/店铺:基于用户位置查询
  • 打车/外卖应用:计算距离、范围查询
  • 地理围栏:判断用户是否在特定区域

代码示例

1
2
3
4
5
6
7
8
# 添加地理位置
GEOADD locations 116.39 39.91 "北京" 121.47 31.23 "上海" 113.26 23.13 "广州"

# 计算两地距离(单位:公里)
GEODIST locations "北京" "上海" km

# 查找附近的地点
GEORADIUS locations 116.39 39.91 500 km

4. Stream(流)

内部实现

Stream是Redis 5.0引入的新数据类型,提供了消息队列的功能,它的实现结合了List和Sorted Set的特性。内部采用了基数树(Radix Tree)数据结构,提供了消息ID的范围查询能力。

应用场景

  • 消息队列:支持消费者组模式
  • 事件溯源:记录和重放事件
  • 时序数据:如日志、传感器数据等
  • 聊天记录:存储社交应用的消息历史

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 添加消息
XADD mystream * name "张三" age 25

# 范围读取
XRANGE mystream - +

# 创建消费者组
XGROUP CREATE mystream mygroup 0

# 消费者组读取
XREADGROUP GROUP mygroup consumer1 COUNT 1 STREAMS mystream >

# 确认消息处理
XACK mystream mygroup message-id

Redis数据结构的内部编码优化

Redis为每种数据类型提供了多种内部编码实现,会根据数据量和内容特征自动选择最优的编码方式,以平衡内存消耗和性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
┌───────────────┬─────────────────────────────────┐
│ 数据类型 │ 内部编码 │
├───────────────┼─────────────────────────────────┤
│ String │ int, embstr, raw │
├───────────────┼─────────────────────────────────┤
│ List │ ziplist, linkedlist, quicklist │
├───────────────┼─────────────────────────────────┤
│ Hash │ ziplist, hashtable │
├───────────────┼─────────────────────────────────┤
│ Set │ intset, hashtable │
├───────────────┼─────────────────────────────────┤
│ Sorted Set │ ziplist, skiplist │
└───────────────┴─────────────────────────────────┘

编码转换策略

Redis会根据以下因素自动在不同编码之间转换:

  1. 元素数量:当元素数量超过特定阈值时转换
  2. 元素大小:当单个元素体积过大时转换
  3. 元素类型:例如整数集合只能保存整数

这些阈值可以通过配置文件调整:

1
2
3
4
5
6
hash-max-ziplist-entries 512    # Hash使用ziplist的最大元素数
hash-max-ziplist-value 64 # Hash ziplist的最大值长度
list-max-ziplist-size -2 # Quicklist每个节点的最大大小
set-max-intset-entries 512 # Set使用intset的最大元素数
zset-max-ziplist-entries 128 # ZSet使用ziplist的最大元素数
zset-max-ziplist-value 64 # ZSet ziplist的最大值长度

性能优化与注意事项

选择合适的数据结构

根据业务场景选择合适的数据结构至关重要:

场景 推荐数据结构 原因
缓存对象 String/Hash 直接存储,快速访问
计数器 String 原子操作支持
有序数据 Sorted Set 自动排序,范围查询
不重复集合 Set 自动去重
位级操作 Bitmap 节省空间,适合状态记录

内存优化技巧

  1. 设置过期时间:防止无限增长
  2. 压缩键和值:减少不必要的空间
  3. 使用整数:优化内存编码
  4. 共享对象池:配置maxmemory-policy
  5. 适当使用ziplist:调整配置参数

常见性能陷阱

  1. 大键问题:一次操作过多数据
  2. 集合全量操作:如KEYS, SMEMBERS, HGETALL
  3. Lua脚本过长执行:阻塞主线程
  4. 不恰当的数据结构:如用String存储复杂关系

实战应用案例

分布式锁实现

1
2
3
4
5
# 获取锁(带超时自动释放)
SET resource_lock unique_value NX PX 10000

# 释放锁(确保只释放自己的锁)
EVAL "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 resource_lock unique_value

限流器实现

1
2
3
4
5
6
7
8
# 简单计数器限流
INCR rate:limit:ip:192.168.1.1
EXPIRE rate:limit:ip:192.168.1.1 60

# 滑动窗口限流
ZADD sliding:window:ip:192.168.1.1 current_timestamp request_id
ZREMRANGEBYSCORE sliding:window:ip:192.168.1.1 0 (current_timestamp - window_size)
ZCARD sliding:window:ip:192.168.1.1

排行榜系统

1
2
3
4
5
6
7
8
# 更新得分
ZADD leaderboard:monthly 1500 "player:1"

# 获取前十名
ZREVRANGE leaderboard:monthly 0 9 WITHSCORES

# 获取玩家排名
ZREVRANK leaderboard:monthly "player:1"

社交关系图谱

1
2
3
4
5
6
7
8
9
10
11
12
# 关注操作
SADD following:user:1 2 3 4

# 粉丝操作
SADD followers:user:2 1 5 6

# 共同关注
SINTER following:user:1 following:user:2

# 好友推荐(我关注的人也关注了谁)
SUNION following:user:3 following:user:4 following:user:5
SDIFF result following:user:1

总结

Redis的强大之处在于其多样化的数据结构以及精心设计的内部实现。通过对数据结构的深入理解,我们可以更好地发挥Redis的性能优势,构建高效的应用系统。

在选择和使用Redis数据结构时,应当遵循以下原则:

  1. 理解业务需求:明确数据访问模式和查询要求
  2. 选择合适的数据结构:基于操作类型和数据特征
  3. 注意内存消耗:考虑数据量和内存限制
  4. 了解性能特性:掌握各种操作的时间复杂度
  5. 合理使用高级特性:如位操作、地理空间等

随着业务复杂度的提升,合理组合使用Redis的各种数据结构,能够解决诸多复杂场景的需求,显著提升系统性能和可扩展性。

参考资料