Redis 数据结构与底层实现
Redis 数据结构与底层实现原理详解
概述
Redis(Remote Dictionary Server)是一个开源的、基于内存的数据结构存储系统,它支持多种数据结构,每种数据结构都有其特定的底层实现和优化策略。本文将详细介绍Redis的核心数据结构及其底层实现原理。
1. String(字符串)
1.1 基本概念
String是Redis最基础的数据类型,可以存储字符串、整数或浮点数。在Redis中,String实际上是二进制安全的字节数组。
1.2 底层实现:Simple Dynamic String (SDS)
Redis使用自定义的SDS(Simple Dynamic String)数据结构来实现字符串:
struct sdshdr {
unsigned int len; // 字符串长度
unsigned int free; // 未使用空间长度
char buf[]; // 字节数组,保存字符串
};
SDS的优势:
- 常数时间复杂度获取长度:len字段直接记录长度,O(1)复杂度
- 杜绝缓冲区溢出:API会检查空间是否足够,自动扩展
- 减少修改字符串时的内存重分配次数:
- 空间预分配:扩展时预分配额外空间
- 惰性空间释放:缩短时不立即释放空间
- 二进制安全:使用len判断结束,而非'\0'
- 兼容C字符串函数:以'\0'结尾,可复用C函数
1.3 编码方式
根据存储内容,String有三种编码:
- INT编码:存储整数值,直接用long类型存储
- EMBSTR编码:短字符串(≤44字节),对象头和SDS连续存储
- RAW编码:长字符串,对象头和SDS分别分配内存
1.4 常用命令和应用场景
# 基本操作
SET key value
GET key
MSET key1 value1 key2 value2
MGET key1 key2
# 计数器
INCR counter
INCRBY counter 5
DECR counter
# 应用场景
- 缓存:存储序列化的对象、HTML片段
- 计数器:页面访问量、点赞数
- 分布式锁:SETNX + EXPIRE
- Session存储:用户会话信息
2. Hash(哈希表)
2.1 基本概念
Hash是一个键值对集合,类似于编程语言中的Map或Dictionary。适合存储对象信息。
2.2 底层实现
Hash有两种编码方式:
2.2.1 ZipList编码(压缩列表)
当Hash满足以下条件时使用:
- 键值对数量小于
hash-max-ziplist-entries(默认512) - 所有键和值的长度都小于
hash-max-ziplist-value(默认64字节)
ZipList结构:
<zlbytes><zltail><zllen><entry1><entry2>...<entryN><zlend>
zlbytes:整个压缩列表占用字节数zltail:尾节点偏移量zllen:节点数量entry:存储键值对zlend:结束标记(0xFF)
优势:内存紧凑,缓存友好
劣势:插入删除需要内存移动,时间复杂度O(N)
2.2.2 HashTable编码(字典)
当不满足ZipList条件时,使用HashTable:
typedef struct dictht {
dictEntry **table; // 哈希表数组
unsigned long size; // 哈希表大小
unsigned long sizemask; // 大小掩码,用于计算索引
unsigned long used; // 已使用节点数量
} dictht;
typedef struct dict {
dictType *type; // 类型特定函数
void *privdata; // 私有数据
dictht ht[2]; // 两个哈希表,用于渐进式rehash
long rehashidx; // rehash索引,-1表示没有进行rehash
} dict;
渐进式Rehash:
- 为ht[1]分配空间,大小为第一个大于等于ht[0].used*2的2^n
- 将rehashidx设置为0,表示rehash开始
- 每次对字典操作时,顺便将ht[0]在rehashidx索引上的键值对rehash到ht[1]
- 随着操作进行,最终ht[0]的所有键值对都会被rehash到ht[1]
- 释放ht[0],将ht[1]设置为ht[0],并创建新的空白ht[1]
2.3 常用命令和应用场景
# 基本操作
HSET user:1 name "张三" age 25 city "北京"
HGET user:1 name
HMGET user:1 name age
HGETALL user:1
HDEL user:1 city
# 应用场景
- 存储对象:用户信息、商品信息
- 购物车:以用户ID为key,商品ID和数量为field和value
- 缓存对象的部分字段:避免序列化整个对象
3. List(列表)
3.1 基本概念
List是一个双向链表,支持在两端进行插入和删除操作。元素可以重复,有序。
3.2 底层实现
List有两种编码方式:
3.2.1 ZipList编码
当List满足以下条件时使用:
- 列表元素个数小于
list-max-ziplist-size(默认-2) - 列表中每个元素长度小于
list-max-ziplist-value(默认64字节)
3.2.2 QuickList编码(Redis 3.2+)
QuickList是ZipList和双向链表的混合体:
typedef struct quicklist {
quicklistNode *head; // 头节点
quicklistNode *tail; // 尾节点
unsigned long count; // 所有ziplist中的总元素个数
unsigned long len; // quicklistNode节点个数
int fill : QL_FILL_BITS; // ziplist大小限制
unsigned int compress : QL_COMP_BITS; // 压缩算法深度
unsigned int bookmark_count: QL_BM_BITS;
quicklistBookmark bookmarks[];
} quicklist;
typedef struct quicklistNode {
struct quicklistNode *prev; // 前驱节点
struct quicklistNode *next; // 后继节点
unsigned char *zl; // ziplist
unsigned int sz; // ziplist的字节大小
unsigned int count : 16; // ziplist中的元素个数
unsigned int encoding : 2; // 编码方式
unsigned int container : 2; // 容器类型
unsigned int recompress : 1; // 是否被压缩
unsigned int attempted_compress : 1; // 测试用
unsigned int extra : 10; // 额外的数据位
} quicklistNode;
QuickList的优势:
- 结合了数组和链表的优点
- 减少了内存碎片
- 支持数据压缩(LZF算法)
3.3 常用命令和应用场景
# 基本操作
LPUSH mylist "world" "hello" # 左侧插入
RPUSH mylist "redis" # 右侧插入
LPOP mylist # 左侧弹出
RPOP mylist # 右侧弹出
LRANGE mylist 0 -1 # 范围查询
# 阻塞操作
BLPOP mylist 10 # 阻塞弹出,超时10秒
# 应用场景
- 消息队列:LPUSH + BRPOP
- 栈:LPUSH + LPOP
- 队列:LPUSH + RPOP
- 最新消息:LPUSH + LRANGE
- 分页:LRANGE
4. Set(集合)
4.1 基本概念
Set是无序的字符串集合,元素唯一,支持集合运算。
4.2 底层实现
Set有两种编码方式:
4.2.1 IntSet编码
当Set满足以下条件时使用:
- 集合中所有元素都是整数
- 集合元素个数不超过
set-max-intset-entries(默认512)
typedef struct intset {
uint32_t encoding; // 编码方式
uint32_t length; // 集合包含的元素数量
int8_t contents[]; // 保存元素的数组
} intset;
IntSet特点:
- 元素按从小到大排序
- 不包含重复元素
- 支持三种编码:int16_t、int32_t、int64_t
- 支持类型升级,但不支持降级
4.2.2 HashTable编码
当不满足IntSet条件时,使用HashTable。
4.3 常用命令和应用场景
# 基本操作
SADD myset "hello" "world" "redis"
SMEMBERS myset
SISMEMBER myset "hello"
SCARD myset
SREM myset "hello"
# 集合运算
SUNION set1 set2 # 并集
SINTER set1 set2 # 交集
SDIFF set1 set2 # 差集
# 应用场景
- 标签系统:用户标签、文章标签
- 好友关系:共同好友(交集)
- 黑白名单:IP黑名单
- 去重:利用Set的唯一性
- 抽奖系统:SPOP随机弹出
5. Sorted Set(有序集合)
5.1 基本概念
Sorted Set是有序的字符串集合,每个元素关联一个分数(score),根据分数排序。
5.2 底层实现
Sorted Set有两种编码方式:
5.2.1 ZipList编码
当Sorted Set满足以下条件时使用:
- 元素个数小于
zset-max-ziplist-entries(默认128) - 所有元素长度小于
zset-max-ziplist-value(默认64字节)
在ZipList中,元素按分数从小到大排序存储。
5.2.2 SkipList + HashTable编码
当不满足ZipList条件时,使用跳跃表和字典:
typedef struct zset {
dict *dict; // 字典,保存成员到分数的映射
zskiplist *zsl; // 跳跃表,保存所有集合元素
} zset;
typedef struct zskiplist {
struct zskiplistNode *header, *tail; // 头尾节点
unsigned long length; // 节点数量
int level; // 最高层数
} zskiplist;
typedef struct zskiplistNode {
sds ele; // 成员对象
double score; // 分数
struct zskiplistNode *backward; // 后退指针
struct zskiplistLevel {
struct zskiplistNode *forward; // 前进指针
unsigned long span; // 跨度
} level[]; // 层
} zskiplistNode;
跳跃表(Skip List)原理:
- 是一种随机化数据结构
- 通过多层链表实现快速查找
- 平均时间复杂度O(log N)
- 支持范围查询
为什么使用跳跃表而不是红黑树:
- 跳跃表实现简单,易于调试
- 跳跃表的范围查询更高效
- 跳跃表的插入删除操作不需要旋转
- 跳跃表在并发环境下更容易实现无锁操作
5.3 常用命令和应用场景
# 基本操作
ZADD leaderboard 100 "player1" 200 "player2" 150 "player3"
ZRANGE leaderboard 0 -1 WITHSCORES
ZREVRANGE leaderboard 0 2 # 前三名
ZSCORE leaderboard "player1"
ZRANK leaderboard "player1" # 排名(从0开始)
# 范围操作
ZRANGEBYSCORE leaderboard 100 200 # 分数范围
ZCOUNT leaderboard 100 200 # 统计分数范围内的元素
# 应用场景
- 排行榜:游戏积分、销售排行
- 延时任务:以时间戳为分数
- 限流:滑动窗口计数
- 搜索建议:以权重为分数
- 时间线:以时间为分数的消息流
6. 新增数据类型(Redis 5.0+)
6.1 Stream(流)
Stream是Redis 5.0引入的数据类型,主要用于消息队列:
# 基本操作
XADD mystream * field1 value1 field2 value2
XREAD STREAMS mystream 0
XGROUP CREATE mystream mygroup $ MKSTREAM
XREADGROUP GROUP mygroup consumer1 STREAMS mystream >
# 应用场景
- 消息队列:支持消费者组
- 事件溯源:完整的事件日志
- 日志收集:结构化日志存储
6.2 Geospatial(地理位置)
基于Sorted Set实现,使用GeoHash算法:
# 基本操作
GEOADD locations 116.48105 39.996794 "天安门"
GEORADIUS locations 116.48105 39.996794 1000 m
GEODIST locations "天安门" "故宫"
# 应用场景
- LBS服务:附近的人、附近的店
- 地理围栏:基于位置的告警
- 物流追踪:配送路径优化
6.3 HyperLogLog
用于基数统计的概率数据结构:
# 基本操作
PFADD hll "user1" "user2" "user3"
PFCOUNT hll
PFMERGE hll1 hll2 hll3
# 应用场景
- UV统计:独立访客数量
- 去重计数:在内存有限的情况下
6.4 Bitmap
基于String实现的位图:
# 基本操作
SETBIT user:sign:1001 1 1 # 第1天签到
GETBIT user:sign:1001 1
BITCOUNT user:sign:1001 # 统计签到天数
# 应用场景
- 用户签到:每天一位
- 布隆过滤器:减少误判
- 实时统计:在线用户数
7. 内存优化和过期策略
7.1 内存优化
- 压缩编码:小对象使用压缩编码
- 内存对齐:减少内存碎片
- 共享对象:小整数对象池(0-9999)
- 惰性删除:大对象异步删除
7.2 过期策略
- 定时删除:设置timer,到期立即删除
- 惰性删除:访问时检查是否过期
- 定期删除:定期随机检查部分key
Redis采用惰性删除 + 定期删除的组合策略。
7.3 内存淘汰策略
当内存不足时,Redis支持以下淘汰策略:
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
8. 总结
8.1 数据结构选择指南
| 场景 | 推荐数据结构 | 原因 |
|---|---|---|
| 缓存 | String | 简单高效,支持序列化对象 |
| 计数器 | String | 原子递增,支持过期 |
| 会话存储 | Hash | 结构化存储,部分更新 |
| 消息队列 | List/Stream | 顺序处理,支持阻塞 |
| 去重 | Set | 天然去重,支持集合运算 |
| 排行榜 | Sorted Set | 自动排序,范围查询 |
| 地理位置 | Geo | 空间索引,距离计算 |
| 统计 | HyperLogLog/Bitmap | 内存高效,近似计算 |
8.2 性能特点总结
- String:最快的读写速度,内存开销最小
- Hash:适合存储对象,比多个String key更节省内存
- List:插入删除快(O(1)),随机访问慢(O(N))
- Set:快速的成员检测(O(1)),支持集合运算
- Sorted Set:有序访问(O(log N)),支持范围查询
- 新数据类型:特定场景下的性能优化
8.3 最佳实践
- 选择合适的数据结构:根据使用场景选择最适合的数据结构
- 控制key的数量:避免大量小key,考虑使用Hash聚合
- 设置合理的过期时间:避免内存泄漏
- 使用压缩编码:合理设置压缩参数
- 监控内存使用:定期检查内存使用情况和碎片率
- 选择合适的持久化策略:根据数据重要性选择RDB或AOF
Redis的数据结构设计精妙,每种结构都针对特定场景进行了优化。理解其底层实现有助于在实际应用中做出正确的技术选择,充分发挥Redis的性能优势。
本文来自博客园,作者:MadLongTom,转载请注明原文链接:https://www.cnblogs.com/madtom/p/19044671
浙公网安备 33010602011771号