<返回目录     Powered by claud/xia兄

第2课: Redis数据类型

String、List、Set、Hash、Sorted Set详解

Redis五大数据类型概览

Redis不仅仅是简单的键值存储,它支持五种核心数据类型,每种类型都有其特定的应用场景和操作命令。理解这些数据类型是掌握Redis的关键。

Redis数据类型的设计哲学: Redis的数据类型设计遵循"数据结构即服务"的理念,每种数据类型都针对特定的使用场景进行了优化,提供了丰富的原子操作,避免了应用层复杂的逻辑处理。
数据类型 描述 典型应用场景 底层实现 时间复杂度
String 字符串、数字 缓存、计数器、分布式锁 SDS(简单动态字符串) O(1)
List 有序列表 消息队列、时间线、最新动态 双向链表/压缩列表 O(1)~O(N)
Set 无序集合 标签、共同好友、去重 哈希表/整数集合 O(1)
Hash 键值对集合 对象存储、用户信息 哈希表/压缩列表 O(1)
Sorted Set 有序集合 排行榜、优先队列 跳表+哈希表 O(log N)

Redis对象系统

Redis使用统一的对象系统来管理所有数据类型,每个Redis对象(robj)包含:

深入理解: Redis的对象系统允许同一数据类型使用不同的底层编码,根据数据大小和特性自动选择最优的实现方式,这种设计既保证了性能又节省了内存。

String(字符串)

String是Redis最基本的数据类型,一个键最大能存储512MB的数据。String类型是二进制安全的,可以存储任何数据,如文本、数字、序列化对象、图片等。

String的特点

SDS(简单动态字符串)底层实现

SDS是Redis自定义的字符串实现,相比C字符串有以下优势:

// SDS数据结构定义(简化版)
struct sdshdr {
    int len;        // 已使用的字节数
    int free;       // 未使用的字节数
    char buf[];     // 字节数组
};

SDS的优势:

String编码方式

Redis的String类型支持多种编码方式,根据存储内容自动选择:

编码转换: Redis会根据操作自动进行编码转换。例如,对int编码的字符串执行append操作时,会自动转换为raw编码。

String应用场景示例

# 1. 缓存对象(JSON序列化)
127.0.0.1:6379> SET user:1001 '{"name":"张三","age":25,"city":"北京"}'
OK

# 2. 计数器(网站访问量)
127.0.0.1:6379> SET page:home:views 0
127.0.0.1:6379> INCR page:home:views
(integer) 1

# 3. 分布式锁
127.0.0.1:6379> SET lock:order:123 "uuid-abc" EX 30 NX
OK

# 4. 限流器(记录请求次数)
127.0.0.1:6379> SET rate:user:1001 0 EX 60
127.0.0.1:6379> INCR rate:user:1001
(integer) 1

# 5. Session存储
127.0.0.1:6379> SETEX session:token123 3600 "user_data"
OK

List(列表)

List是简单的字符串列表,按照插入顺序排序。可以在列表的头部或尾部添加元素,最多可以包含2^32-1个元素(超过40亿)。

List的特点

List底层实现原理

Redis的List使用两种底层编码方式,根据元素数量和大小自动选择:

1. 压缩列表(ziplist)

当列表元素较少且元素较小时,使用压缩列表存储:

2. 双向链表(linkedlist)

当列表元素较多或较大时,使用双向链表存储:

// 双向链表节点结构(简化版)
typedef struct listNode {
    struct listNode *prev;  // 前驱节点
    struct listNode *next;  // 后继节点
    void *value;            // 节点值
} listNode;

// 链表结构
typedef struct list {
    listNode *head;         // 头节点
    listNode *tail;         // 尾节点
    unsigned long len;      // 节点数量
    // ... 其他字段
} list;
编码转换: Redis会根据配置的阈值自动在压缩列表和双向链表之间转换。当压缩列表的元素数量或大小超过阈值时,会自动转换为双向链表。

List应用场景示例

# 1. 消息队列(生产者-消费者模式)
# 生产者推送消息
127.0.0.1:6379> LPUSH queue:tasks "task1"
(integer) 1
127.0.0.1:6379> LPUSH queue:tasks "task2"
(integer) 2

# 消费者获取消息
127.0.0.1:6379> RPOP queue:tasks
"task1"

# 2. 最新动态(微博时间线)
127.0.0.1:6379> LPUSH timeline:user:1001 "发布了新微博"
127.0.0.1:6379> LPUSH timeline:user:1001 "点赞了一条微博"
127.0.0.1:6379> LRANGE timeline:user:1001 0 9
# 获取最新10条动态

# 3. 文章列表(分页)
127.0.0.1:6379> RPUSH articles "article1" "article2" "article3"
127.0.0.1:6379> LRANGE articles 0 1
# 第一页,显示2条

# 4. 栈操作(后进先出)
127.0.0.1:6379> LPUSH stack "A"
127.0.0.1:6379> LPUSH stack "B"
127.0.0.1:6379> LPOP stack
"B"

Set(集合)

Set是String类型的无序集合,集合成员是唯一的,不能重复。Redis的Set是通过哈希表实现的,添加、删除、查找的时间复杂度都是O(1)。

Set的特点

Set底层实现原理

Redis的Set使用两种底层编码方式:

1. 整数集合(intset)

当集合元素都是整数且数量较少时,使用整数集合:

2. 哈希表(hashtable)

当集合元素包含非整数或数量较多时,使用哈希表:

// 哈希表节点结构
typedef struct dictEntry {
    void *key;              // 键
    union {
        void *val;          // 值(对于Set,val为NULL)
        uint64_t u64;
        int64_t s64;
    } v;
    struct dictEntry *next; // 下一个节点(解决哈希冲突)
} dictEntry;

// 哈希表结构
typedef struct dictht {
    dictEntry **table;      // 哈希表数组
    unsigned long size;     // 表大小
    unsigned long sizemask; // 大小掩码
    unsigned long used;     // 已使用节点数
} dictht;
集合运算原理: Redis的集合运算(交集、并集、差集)使用不同的算法优化。交集使用最小的集合作为基准,并集使用合并算法,差集使用遍历算法,确保在大数据集下也有良好的性能。

Set应用场景示例

# 1. 标签系统
127.0.0.1:6379> SADD article:100:tags "Python" "Redis" "数据库"
(integer) 3
127.0.0.1:6379> SMEMBERS article:100:tags
1) "Python"
2) "Redis"
3) "数据库"

# 2. 共同好友
127.0.0.1:6379> SADD user:1001:friends "user:1002" "user:1003" "user:1004"
127.0.0.1:6379> SADD user:1005:friends "user:1003" "user:1004" "user:1006"
127.0.0.1:6379> SINTER user:1001:friends user:1005:friends
1) "user:1003"
2) "user:1004"

# 3. 点赞用户列表(去重)
127.0.0.1:6379> SADD post:200:likes "user:1001"
127.0.0.1:6379> SADD post:200:likes "user:1002"
127.0.0.1:6379> SADD post:200:likes "user:1001"  # 重复添加无效
(integer) 0
127.0.0.1:6379> SCARD post:200:likes
(integer) 2

# 4. 抽奖系统(随机抽取)
127.0.0.1:6379> SADD lottery:users "user1" "user2" "user3" "user4"
127.0.0.1:6379> SRANDMEMBER lottery:users 2
1) "user3"
2) "user1"

# 5. IP黑名单
127.0.0.1:6379> SADD blacklist:ip "192.168.1.100" "10.0.0.50"
127.0.0.1:6379> SISMEMBER blacklist:ip "192.168.1.100"
(integer) 1

Hash(哈希)

Hash是一个键值对集合,特别适合存储对象。每个Hash可以存储2^32-1个键值对。相比String存储对象(JSON序列化),Hash可以只修改某个字段,更加灵活高效。

Hash的特点

Hash底层实现原理

Redis的Hash使用两种底层编码方式:

1. 压缩列表(ziplist)

当Hash字段较少且字段值较小时,使用压缩列表:

2. 哈希表(hashtable)

当Hash字段较多或字段值较大时,使用哈希表:

内存优化: 使用Hash存储对象比使用多个String键节省内存,因为Hash只需要存储一次键名,而多个String键需要重复存储键名。同时,压缩列表编码进一步减少了内存开销。

Hash应用场景示例

# 1. 用户信息存储
127.0.0.1:6379> HSET user:1001 name "张三" age 25 city "北京" job "工程师"
(integer) 4
127.0.0.1:6379> HGET user:1001 name
"张三"
127.0.0.1:6379> HGETALL user:1001
1) "name"
2) "张三"
3) "age"
4) "25"
5) "city"
6) "北京"
7) "job"
8) "工程师"

# 2. 商品信息
127.0.0.1:6379> HMSET product:2001 name "iPhone15" price 5999 stock 100 category "手机"
OK
127.0.0.1:6379> HINCRBY product:2001 stock -1
(integer) 99

# 3. 购物车
127.0.0.1:6379> HSET cart:user:1001 product:2001 2
127.0.0.1:6379> HSET cart:user:1001 product:2002 1
127.0.0.1:6379> HGETALL cart:user:1001
1) "product:2001"
2) "2"
3) "product:2002"
4) "1"

# 4. 网站配置
127.0.0.1:6379> HMSET config:site title "我的网站" domain "example.com" email "admin@example.com"
OK

# 5. 统计数据(多维度计数)
127.0.0.1:6379> HINCRBY stats:2024-01-01 page_views 100
127.0.0.1:6379> HINCRBY stats:2024-01-01 unique_visitors 50
127.0.0.1:6379> HGETALL stats:2024-01-01
1) "page_views"
2) "100"
3) "unique_visitors"
4) "50"

Sorted Set(有序集合)

Sorted Set是Set的升级版,每个成员都关联一个分数(score),Redis通过分数为集合成员排序。成员唯一,但分数可以重复。

Sorted Set的特点

Sorted Set底层实现原理

Redis的Sorted Set使用两种数据结构的组合:

1. 跳表(Skip List)

用于维护有序的成员列表,支持快速的范围查询和排名操作:

2. 哈希表(hashtable)

用于快速查找成员对应的分数:

// 跳表节点结构(简化版)
typedef struct zskiplistNode {
    robj *obj;                      // 成员对象
    double score;                   // 分数
    struct zskiplistNode *backward; // 后退指针
    struct zskiplistLevel {
        struct zskiplistNode *forward;  // 前进指针
        unsigned int span;              // 跨度
    } level[];                       // 层级数组
} zskiplistNode;

// 跳表结构
typedef struct zskiplist {
    struct zskiplistNode *header, *tail;  // 头尾节点
    unsigned long length;                 // 节点数量
    int level;                            // 最大层数
} zskiplist;
跳表原理: 跳表通过随机化层级来平衡性能,每个节点有1/2的概率提升到下一层。这种设计使得跳表在保持有序性的同时,提供了接近平衡树的查询性能,但实现更简单。

Sorted Set应用场景示例

# 1. 排行榜(游戏积分)
127.0.0.1:6379> ZADD leaderboard 1000 "player1" 1500 "player2" 1200 "player3"
(integer) 3
127.0.0.1:6379> ZREVRANGE leaderboard 0 2 WITHSCORES
1) "player2"
2) "1500"
3) "player3"
4) "1200"
5) "player1"
6) "1000"

# 2. 热搜榜(按热度排序)
127.0.0.1:6379> ZADD hot:topics 5000 "话题A" 8000 "话题B" 3000 "话题C"
127.0.0.1:6379> ZINCRBY hot:topics 1000 "话题A"
"6000"
127.0.0.1:6379> ZREVRANK hot:topics "话题A"
(integer) 1

# 3. 延迟队列(按时间戳排序)
127.0.0.1:6379> ZADD delay:queue 1675234567 "task1" 1675234600 "task2"
127.0.0.1:6379> ZRANGEBYSCORE delay:queue 0 1675234580
1) "task1"

# 4. 成绩排名
127.0.0.1:6379> ZADD exam:scores 95 "张三" 88 "李四" 92 "王五"
127.0.0.1:6379> ZRANK exam:scores "李四"
(integer) 0  # 排名第1(从0开始)

# 5. 商品销量排行
127.0.0.1:6379> ZADD sales:ranking 1000 "product:A" 1500 "product:B" 800 "product:C"
127.0.0.1:6379> ZINCRBY sales:ranking 100 "product:A"
"1100"
127.0.0.1:6379> ZREVRANGE sales:ranking 0 9 WITHSCORES
# 获取销量前10的商品

数据类型选择指南

如何选择合适的数据类型?

# 决策树:
1. 需要存储单个值?
   → 使用 String

2. 需要存储对象(多个字段)?
   → 使用 Hash

3. 需要存储列表(有序、可重复)?
   → 使用 List

4. 需要存储集合(无序、不重复)?
   → 不需要排序 → 使用 Set
   → 需要排序 → 使用 Sorted Set

5. 需要集合运算(交集、并集、差集)?
   → 使用 Set

6. 需要排行榜、优先队列?
   → 使用 Sorted Set

性能对比

操作 String List Set Hash Sorted Set
添加元素 O(1) O(1) O(1) O(1) O(log N)
删除元素 O(1) O(N) O(1) O(1) O(log N)
查找元素 O(1) O(N) O(1) O(1) O(log N)
范围查询 - O(N) - - O(log N + M)

内存占用对比

# 存储1000个用户信息的内存占用对比

# 方式1: String(JSON序列化)
SET user:1 '{"name":"张三","age":25}'
SET user:2 '{"name":"李四","age":30}'
...
# 内存占用: ~150KB

# 方式2: Hash(推荐)
HSET user:1 name "张三" age 25
HSET user:2 name "李四" age 30
...
# 内存占用: ~80KB(节省约47%)

# 结论: Hash在存储对象时更节省内存
重要提示:

实践练习

练习任务:
  1. String练习:创建一个网站访问计数器,模拟100次访问,使用INCR命令
  2. List练习:创建一个消息队列,添加10条消息,然后依次取出前5条
  3. Set练习:创建两个用户的好友列表,使用SINTER找出共同好友
  4. Hash练习:使用Hash存储3个商品信息(名称、价格、库存),然后修改其中一个商品的库存
  5. Sorted Set练习:创建一个学生成绩排行榜,添加10个学生及其分数,查询前3名和某个学生的排名
  6. 综合练习:设计一个简单的博客系统,使用不同数据类型存储文章、评论、标签、点赞等信息

总结

本课程详细介绍了Redis的五大核心数据类型:

理解每种数据类型的特点和应用场景,是高效使用Redis的基础。在实际开发中,要根据业务需求选择最合适的数据类型,以达到最佳的性能和内存效率。