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的优势

  1. 常数时间复杂度获取长度:len字段直接记录长度,O(1)复杂度
  2. 杜绝缓冲区溢出:API会检查空间是否足够,自动扩展
  3. 减少修改字符串时的内存重分配次数
    • 空间预分配:扩展时预分配额外空间
    • 惰性空间释放:缩短时不立即释放空间
  4. 二进制安全:使用len判断结束,而非'\0'
  5. 兼容C字符串函数:以'\0'结尾,可复用C函数

1.3 编码方式

根据存储内容,String有三种编码:

  1. INT编码:存储整数值,直接用long类型存储
  2. EMBSTR编码:短字符串(≤44字节),对象头和SDS连续存储
  3. 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

  1. 为ht[1]分配空间,大小为第一个大于等于ht[0].used*2的2^n
  2. 将rehashidx设置为0,表示rehash开始
  3. 每次对字典操作时,顺便将ht[0]在rehashidx索引上的键值对rehash到ht[1]
  4. 随着操作进行,最终ht[0]的所有键值对都会被rehash到ht[1]
  5. 释放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)
  • 支持范围查询

为什么使用跳跃表而不是红黑树

  1. 跳跃表实现简单,易于调试
  2. 跳跃表的范围查询更高效
  3. 跳跃表的插入删除操作不需要旋转
  4. 跳跃表在并发环境下更容易实现无锁操作

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 内存优化

  1. 压缩编码:小对象使用压缩编码
  2. 内存对齐:减少内存碎片
  3. 共享对象:小整数对象池(0-9999)
  4. 惰性删除:大对象异步删除

7.2 过期策略

  1. 定时删除:设置timer,到期立即删除
  2. 惰性删除:访问时检查是否过期
  3. 定期删除:定期随机检查部分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 性能特点总结

  1. String:最快的读写速度,内存开销最小
  2. Hash:适合存储对象,比多个String key更节省内存
  3. List:插入删除快(O(1)),随机访问慢(O(N))
  4. Set:快速的成员检测(O(1)),支持集合运算
  5. Sorted Set:有序访问(O(log N)),支持范围查询
  6. 新数据类型:特定场景下的性能优化

8.3 最佳实践

  1. 选择合适的数据结构:根据使用场景选择最适合的数据结构
  2. 控制key的数量:避免大量小key,考虑使用Hash聚合
  3. 设置合理的过期时间:避免内存泄漏
  4. 使用压缩编码:合理设置压缩参数
  5. 监控内存使用:定期检查内存使用情况和碎片率
  6. 选择合适的持久化策略:根据数据重要性选择RDB或AOF

Redis的数据结构设计精妙,每种结构都针对特定场景进行了优化。理解其底层实现有助于在实际应用中做出正确的技术选择,充分发挥Redis的性能优势。

posted @ 2025-08-18 14:45  MadLongTom  阅读(233)  评论(0)    收藏  举报