Redis中的数据结构
1.五种常见的数据结构类型
1. 字符串(String)
特点
- 字符串是 Redis 最基本的数据类型,可以存储字符串、整数或浮点数。
- 一个字符串最多可以存储 512 MB 的数据。
使用场景
- 缓存简单数据:比如存储用户的登录状态、会话信息或配置信息。
- 计数器:字符串支持自增和自减,可以方便地用来实现计数器,如网站访问次数统计。
- 分布式锁:通过
SETNX
命令可以实现分布式锁。
2. 列表(List)
特点
- 列表是一个有序的字符串序列,可以从两端添加或移除元素。
- 支持常见的栈和队列操作(如 LIFO 和 FIFO)。
使用场景
- 消息队列:列表支持从头部或尾部推入和弹出元素,因此常用于简单的消息队列。
- 任务列表:可以用来存储一系列需要按顺序处理的任务。
- 时间轴:社交媒体的时间轴可以用列表来实现,记录用户的操作历史等。
3. 集合(Set)
特点
- 集合是无序且唯一的字符串集合,元素不重复。
- 支持交集、并集和差集等集合运算。
使用场景
- 标签和关注关系:如用于存储用户的关注列表、标签等,避免重复。
- 抽奖系统:用户抽奖时,可以使用集合确保每位用户只参与一次。
- 共同好友推荐:通过交集运算,找出两个用户共同关注的好友。
4. 有序集合(Sorted Set)
特点
- 有序集合中的每个元素都有一个分数(score),Redis 会根据分数对元素进行排序。
- 支持按分数范围查询元素,也可以按分数排名来访问元素。
使用场景
- 排行榜:常用于实现游戏或应用的排行榜,按得分高低排序。
- 任务调度:可以根据任务的优先级来安排执行顺序。
- 带权重的数据:例如评分系统,可以根据评分来展示内容的排名。
5. 哈希(Hash)
特点
- 哈希是一个键值对集合,适合存储对象或数据结构。
- 可以把多个字段及其值存储在一个键下,通过字段名称快速访问字段的值。
使用场景
- 用户信息存储:可以用来存储用户信息(如用户名、年龄、地址等),并快速访问特定字段。
- 缓存对象数据:适合存储一些需要快速访问的对象或数据结构。
- 配置项管理:适合存储分模块的配置信息。
类型对比
数据类型 | 是否有序 | 是否唯一 | 适用场景 |
---|---|---|---|
字符串 | 否 | 否 | 简单数据、计数器、锁 |
列表 | 是 | 否 | 消息队列、任务列表 |
集合 | 否 | 是 | 标签、抽奖系统 |
有序集合 | 是 | 是 | 排行榜、带权重数据 |
哈希 | 否 | 否 | 用户信息、配置项 |
2.后续引入的四种数据类型
1. BitMap
版本
- Redis 2.2 版引入。
特点
- BitMap 并不是 Redis 独立的数据类型,而是一种基于字符串类型的位操作方法。
- BitMap 允许我们将一个大的字符串值视为一系列的位(bit),并可以对每一位进行单独操作。
- BitMap 中的每一位可以是 0 或 1,能高效地进行位操作,例如设置某一位的值、获取某一位的值,或者统计多个位中的 1 的个数。
使用场景
- 用户签到:可以用一个 BitMap 表示每个用户在一年中的签到情况。假设有 365 天,用 365 位表示即可。
- 活跃用户统计:在每一位表示一个用户的活跃状态,0 表示不活跃,1 表示活跃。通过位操作快速统计活跃用户。
- 二进制标记:用于对大量用户的某些特定标记进行标识和统计。
示例
假设我们想记录一个用户在一个月中的签到情况,可以用 BitMap 操作设置某天为签到:
# 设置用户在第 5 天签到
SETBIT user:1001:checkin 5 1
# 获取用户第 5 天的签到状态
GETBIT user:1001:checkin 5 # 返回 1
# 统计这个用户的签到天数
BITCOUNT user:1001:checkin
2. HyperLogLog
版本
- Redis 2.8 版引入。
特点
- HyperLogLog 是一种基数估计算法,可以高效地计算大规模数据的基数(独特元素的数量)。
- HyperLogLog 的存储空间是固定的,只占用 12 KB 内存,即使是十亿级的去重数据,也不会增加内存占用。
- HyperLogLog 通过概率算法近似计算基数,因此有误差(标准误差约 0.81%),但通常可接受。
使用场景
- UV(独立访问用户)统计:在大数据量的情况下,用 HyperLogLog 统计页面的独立访问用户数量。
- 去重计数:如在电商网站中统计特定时间内访问商品详情页的独立用户数,HyperLogLog 非常适合这种场景。
示例
使用 HyperLogLog 统计每天访问某网站的用户数量:
# 将用户 ID 添加到 HyperLogLog
PFADD page:1001:user_set user1 user2 user3
# 获取 HyperLogLog 基数
PFCOUNT page:1001:user_set # 返回基数估算值
# 合并多个 HyperLogLog 的基数
PFMERGE combined_users page:1001:user_set page:1002:user_set
3. GEO
版本
- Redis 3.2 版引入。
特点
- GEO 是 Redis 的地理位置数据类型,基于有序集合实现,能够存储地理位置信息(经纬度)并支持地理操作。
- Redis 提供了一系列 GEO 命令,如添加位置、计算两地距离、查询指定范围内的位置等。
使用场景
- 附近的人:在社交或打车应用中,可以快速查找某个范围内的用户或服务提供者。
- POI(兴趣点)查询:例如餐馆、加油站等可以存储在 Redis 中,用户可以根据当前地点查询附近的 POI。
示例
假设我们要在 Redis 中存储和查询城市的地理位置:
# 添加地理位置(城市名称和经纬度)
GEOADD cities 13.361389 38.115556 "Palermo"
GEOADD cities 15.087269 37.502669 "Catania"
# 查询两个城市之间的距离
GEODIST cities "Palermo" "Catania" km # 返回距离,单位为公里
# 获取指定地点附近的城市
GEORADIUS cities 15 37 200 km # 返回 200 公里范围内的地点
4. Stream
版本
- Redis 5.0 版引入。
特点
- Stream 是一种可无限增长的日志结构数据类型,支持队列和发布-订阅模式,适合处理实时数据流。
- Stream 提供了对消息的追加、读取、分组消费等操作,并可以按 ID 或时间戳查询数据。
- Stream 支持消费者分组,允许不同消费者组读取相同的数据流。
使用场景
- 日志和事件存储:可以用于存储系统日志、用户行为数据等,可以在需要时实时读取。
- 消息队列:Stream 的消费者分组特性使其适合构建简单的消息队列系统。
- 实时数据处理:如 IoT 设备传回的数据流处理,Redis Stream 可以充当数据缓冲区。
示例
使用 Stream 存储和读取事件:
# 添加一条新事件(Stream 会自动生成 ID)
XADD mystream * temperature 22.5 humidity 60
# 读取最新的事件
XRANGE mystream - + # 返回按时间排序的所有事件
# 创建消费者组
XGROUP CREATE mystream mygroup $ # $ 表示从最新消息开始消费
# 消费者组读取消息
XREADGROUP GROUP mygroup consumer1 COUNT 2 STREAMS mystream >
类型对比
数据类型 | 引入版本 | 特点 | 使用场景 |
---|---|---|---|
BitMap | 2.2 | 位操作,高效标记和统计 | 用户签到、活跃用户统计 |
HyperLogLog | 2.8 | 基数估计,固定存储 | UV统计、大数据去重计数 |
GEO | 3.2 | 地理位置存储和查询 | 附近的人、POI查询 |
Stream | 5.0 | 实时数据流、消费者分组 | 消息队列、实时数据处理 |
3.五种常见数据类型的实现
1. 字符串(String)
实现方式
- Redis 的字符串是最基础的数据类型,主要是用简单动态字符串(SDS)来实现的。SDS 是一种类似于 C 语言的字符串,但在结构和管理方式上更为灵活。
- SDS 包括两部分:实际存储的字符串内容和长度信息,其中长度信息可以让 Redis 快速获取字符串长度。
结构细节
- 空间管理:SDS 会自动扩容。比如新增内容超出当前空间时,SDS 会一次性增加空间,减少多次扩容带来的开销。
- 安全性:SDS 不用像 C 字符串那样依赖终止符
\0
。它能直接记录长度,避免越界问题。 - 灵活性:SDS 允许存储二进制数据,可以不仅仅是字符串,甚至可以存放图片、视频等。
内部操作
- Redis 会根据字符串的长度自动选择存储形式,比如短字符串直接存储,而长字符串可以使用压缩或对象来节省内存。
2. 列表(List)
实现方式
Redis 的列表有两种实现结构:
- 双向链表(Linked List):当列表元素较少且不需要快速访问时,Redis 会使用双向链表。这种结构非常适合频繁在两端插入和删除元素的场景。
- 紧凑列表(ziplist):当列表的元素数量少或数据量不大时,Redis 使用压缩存储的 ziplist,它将所有元素压缩成一个连续的内存块。这样不仅节省空间,还能加速遍历。
结构细节
- 双向链表:每个节点包含指向前后两个节点的指针,因此可以从两端快速插入和删除。双向链表适合处理大型列表或频繁插入删除操作。
- 紧凑列表:由于没有指针,节省了空间,并且通过连续的内存区域存储元素,提高了读取效率。但随着元素变多,ziplist 的插入和删除会变慢。
内部操作
- Redis 会根据列表的元素数量和大小动态选择链表或紧凑列表作为实现。比如,当元素很少且较小时,使用 ziplist;反之则使用双向链表。
3. 集合(Set)
实现方式
Redis 的集合有两种实现:
- 字典(hash table):当集合中的元素较多时,Redis 使用哈希表来存储。哈希表的结构允许快速增删查改元素。
- 整数集合(intset):当集合中的元素数量少并且全为整数时,Redis 会使用 intset 存储。整数集合通过连续的内存区域存储每个整数,适合小型集合。
结构细节
- 字典:使用哈希表存储,每个元素的值通过散列函数分布在不同的桶中,可以快速进行查找、插入和删除。
- 整数集合:是一种小而紧凑的数据结构,特别适合存储小数量的整数集合。通过二分查找提高查询速度。
内部操作
- Redis 会根据集合的大小和内容类型选择合适的数据结构。开始时如果集合小且是整数,就使用整数集合;随着数据的增加或类型变化,再切换到哈希表。
4. 有序集合(Sorted Set)
实现方式
有序集合通过跳表(skip list)和哈希表结合来实现。
- 跳表:是一种分层链表结构,允许元素在链表中分层跳跃,从而实现高效的增删查操作。
- 哈希表:用来实现元素值到分数的快速映射。
结构细节
- 跳表的实现:有序集合中的每个元素都被存储在跳表中,跳表的每一层可以包含多个节点,而高层节点更少,使得查找效率接近于二分查找。
- 哈希表的作用:哈希表帮助 Redis 快速定位元素,确保在修改或删除元素时,跳表和哈希表同步更新。
内部操作
- 跳表的主要作用是保持元素按分数排序,而哈希表则帮助 Redis 快速访问元素分数。通过两者的结合,有序集合既支持快速查询特定分数的元素,又能按分数排序输出。
5. 哈希(Hash)
实现方式
Redis 的哈希表使用两种结构:
- 压缩列表(ziplist):当哈希表的字段少且数据量小,Redis 使用 ziplist 存储,这样可以节省内存空间。
- 字典(hash table):当哈希表字段多时,Redis 使用哈希表来实现,便于快速查找和更新。
结构细节
- 压缩列表:ziplist 把字段和值紧凑存储在一个连续的内存区域,适合小型哈希表。这样存储的方式减少了内存碎片,但不适合频繁插入和删除。
- 哈希表:当哈希表变大或数据复杂时,Redis 转为用哈希表来管理。这种结构有助于快速访问每个字段。
内部操作
- Redis 会根据哈希表的大小选择合适的实现。如果字段较少则用压缩列表,而随着字段增多或变大,则切换为哈希表,从而提升效率。
总结
数据类型 | 实现结构 | 适用场景 |
---|---|---|
字符串 | SDS | 简单字符串存储、计数器等 |
列表 | 双向链表/紧凑列表 | 消息队列、任务列表等 |
集合 | 哈希表/整数集合 | 去重、标签管理等 |
有序集合 | 跳表+哈希表 | 排行榜、优先级队列等 |
哈希 | 压缩列表/哈希表 | 用户信息存储、对象属性管理等 |
Redis 的这种“结构适应性”设计,让它可以根据数据特点自动调整底层实现,在保证数据存储灵活性的同时,保持高效的查询和操作速度。