Redis 数据类型完全指南:String/Hash/List/Set/ZSet 深入理解
前言
Redis 提供了丰富的数据类型,每种类型都有其最佳使用场景。理解这些数据类型的底层实现(SDS、ziplist、quicklist、intset、dict、skiplist),才能真正用好 Redis。本文系统讲解五种核心类型的底层原理与实战技巧。
String 类型:最常用的数据结构
Redis 的 String 类型并非简单的 C 字符串,而是使用了 SDS(Simple Dynamic String)动态字符串。SDS 解决了 C 字符串的诸多问题:长度计算 O(n)、缓冲区溢出、二进制不安全。
SDS 的特点:
- <code>len</code> 字段记录字符串长度,strlen 为 O(1)
- 自动扩容,容量不足时翻倍扩展(小于 1MB 时)或增加 1MB(大于 1MB 时)
- 支持二进制数据(可以存储 JPEG、JSON 等任意字节序列)
- 惰性释放:字符串缩短时不立即回收内存
import redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
# 基本操作
r.set('user:1001:name', '张三')
r.set('user:1001:age', 28)
r.get('user:1001:name') -- '张三'
# SET 的高级选项
r.set('token:abc123', 'user_id_1001', ex=3600, nx=True) -- nx=True: 不存在才设置(原子)
r.set('rate:limit:api', 100, ex=60, xx=True) -- xx=True: 存在才更新
# 批量操作
r.mset({
'product:1001:price': '299.00',
'product:1001:stock': '500',
'product:1001:name': '无线蓝牙耳机'
})
r.mget(['product:1001:price', 'product:1001:stock'])
-- ['299.00', '500']
# 计数操作(原子递增/递减)
r.set('page:views:20240101', 1000)
r.incr('page:views:20240101') -- 1001
r.incrby('page:views:20240101', 50) -- 1051
r.decr('page:views:20240101') -- 1050
r.incrbyfloat('price:discount', 0.5)
# 字符串切片(Redis 6.2+)
r.setrange('log', 0, 'prefix:')
r.getrange('log', 0, 10)
String 类型的应用场景:
- 缓存序列化的 JSON 对象(但大对象推荐用 Hash)
- 计数器(访问次数、点赞数)
- 分布式锁(SET + NX + EX 组合)
- Session 存储
- 限流(令牌桶/滑动窗口)
Hash 类型:字段级操作
Hash 是一个 field-value 映射表,适合存储对象。相比 String + 序列化 JSON 的方案,Hash 可以对单个字段进行独立操作,无需反序列化整个对象。
# 存储用户对象(Hash vs String 的对比)
# String 方案:整个对象序列化存储
r.set('user:1001', json.dumps({'name': '张三', 'age': 28}))
data = json.loads(r.get('user:1001'))
data['age'] = 29 -- 需要:反序列化 -> 修改 -> 重新序列化
r.set('user:1001', json.dumps(data))
# Hash 方案:字段级操作,无需序列化
r.hset('user:1001', mapping={'name': '张三', 'age': 28, 'city': '北京'})
r.hget('user:1001', 'age') -- '28'
r.hgetall('user:1001') -- {'name': '张三', 'age': '28', 'city': '北京'}
r.hincrby('user:1001', 'age', 1) -- 原子递增:age = 29
r.hset('user:1001', 'email', 'zhangsan@example.com')
r.hdel('user:1001', 'city')
r.hexists('user:1001', 'name') -- True
# 批量操作
r.hmset('product:2001', {'name': '无线键盘', 'price': '199.00', 'stock': '200'})
r.hmget('product:2001', ['name', 'price'])
r.hlen('product:2001') -- 3
r.hkeys('product:2001')
r.hvals('product:2001')
# 游标扫描(大数据量时避免阻塞)
for key, fields in r.hscan_iter('large:hash:key', count=1000):
process(fields)
Hash 的底层实现:
- 数据量小(默认小于 512 个字段,每个字段值小于 64 字节)时使用 ziplist(压缩列表),内存紧凑
- 超过阈值后转换为 dict(哈希表),支持 O(1) 随机访问
- 配置项:<code>hash-max-ziplist-entries</code> 和 <code>hash-max-ziplist-value</code>
List 类型:有序队列
Redis List 是双向链表实现的有序序列,支持两端操作(LPUSH/RPOP 等),常用于消息队列、最新列表等场景。
# 基础操作
r.lpush('queue:tasks', 'task:001', 'task:002', 'task:003')
r.rpush('queue:tasks', 'task:004')
r.llen('queue:tasks') -- 4
r.lrange('queue:tasks', 0, -1) -- ['task:003', 'task:002', 'task:001', 'task:004']
r.lpop('queue:tasks') -- 'task:003'
r.rpop('queue:tasks') -- 'task:004'
# 阻塞操作(实现消息队列)
result = r.blpop('queue:tasks', timeout=30) -- 返回 (key, value) 或 None
# 列表切片
r.rpush('article:ids', 'a1', 'a2', 'a3', 'a4', 'a5')
r.lrange('article:ids', 0, 2)
r.ltrim('article:ids', 0, 9) -- 裁剪保留 [0, 9]
# 实现最新列表(如微博最新 100 条评论)
def add_comment(article_id, comment_id):
pipe = r.pipeline()
pipe.lpush(f'comments:{article_id}', comment_id)
pipe.ltrim(f'comments:{article_id}', 0, 99)
pipe.execute()
# 列表间移动
r.rpoplpush('queue:tasks:processing', 'queue:tasks:completed')
r.brpoplpush('queue:tasks', 'queue:tasks:backup', timeout=10)
List 的应用场景:
- 消息队列(轻量级,生产环境推荐用 Stream)
- 最新列表(最新 N 条评论/帖子)
- 时间线(用户动态列表)
- 栈和队列数据结构
Set 类型:无序去重集合
Set 是无序、去重、元素唯一的集合。底层使用 intset(整数集合,小数据量时)或 dict(哈希表)实现,支持交集、并集、差集等集合运算。
# 基础操作
r.sadd('tags:article:1001', 'Redis', 'MySQL', 'Python', '数据库')
r.scard('tags:article:1001') -- 4
r.smembers('tags:article:1001')
r.sismember('tags:article:1001', 'Redis') -- True
r.sismember('tags:article:1001', 'MongoDB') -- False
# 随机操作
r.srandmember('tags:article:1001')
r.srandmember('tags:article:1001', 2)
r.spop('tags:article:1001')
# 集合运算
r.sadd('users:viewed:20240101', 'u1', 'u2', 'u3', 'u4')
r.sadd('users:viewed:20240102', 'u3', 'u4', 'u5', 'u6')
-- 差集:昨天看了今天没看的(流失用户)
r.sdiff('users:viewed:20240101', 'users:viewed:20240102')
-- 交集:两天都看了的(活跃用户)
r.sinter('users:viewed:20240101', 'users:viewed:20240102')
-- 并集:两天合计看过的人数(去重)
r.sunion('users:viewed:20240101', 'users:viewed:20240102')
-- 将集合运算结果存储到新 key
r.sinterstore('users:active', 'users:viewed:20240101', 'users:viewed:20240102')
# 遍历大集合
for member in r.sscan_iter('large:set:key', count=1000):
process(member)
Set 的应用场景:
- 标签系统(文章标签、用户画像标签)
- 点赞、收藏、去重(用户 ID 集合)
- 关注列表(但无法按时间排序,ZSet 更适合)
- 黑名单/白名单
ZSet 类型:有序去重集合
ZSet(Sorted Set)是 Redis 最复杂的数据类型,每个元素有一个 score(浮点数),按 score 排序。底层使用 dict(哈希表)+ skiplist(跳表)双重索引,查找、插入、删除都是 O(log N)。
为什么用跳表而不是 B+ 树?
跳表实现简单,易于理解和实现。插入节点时只需局部修改指针,无需像 B+ 树那样可能触发节点分裂。跳表的平均复杂度为 O(log N),实际表现非常稳定。
# 基础操作
r.zadd('leaderboard:posts', {'post:1001': 1500, 'post:1002': 2300, 'post:1003': 800})
r.zcard('leaderboard:posts') -- 3
r.zscore('leaderboard:posts', 'post:1001') -- 1500.0
r.zrank('leaderboard:posts', 'post:1001') -- 2(0-indexed)
r.zrevrank('leaderboard:posts', 'post:1001') -- 0
# 按排名范围查询
r.zrange('leaderboard:posts', 0, 9, withscores=True)
r.zrevrange('leaderboard:posts', 0, 9, withscores=True)
# 按分数范围查询
r.zrangebyscore('leaderboard:posts', 1000, 2000, withscores=True)
r.zcount('leaderboard:posts', 1000, 2000)
# 更新分数(原子操作)
r.zincrby('leaderboard:posts', 100, 'post:1001')
# 删除元素
r.zrem('leaderboard:posts', 'post:1003')
# 按排名删除(分页场景)
r.zremrangebyrank('leaderboard:posts', 0, -11)
# 按分数删除
r.zremrangebyscore('leaderboard:posts', 0, 500)
# 应用:微博热搜排行榜
def on_post_like(post_id):
r.zincrby('hot:posts:daily', 1, str(post_id))
r.expire('hot:posts:daily', 86400)
def get_hot_posts(limit=10):
return r.zrevrange('hot:posts:daily', 0, limit - 1, withscores=True)
# 应用:延时队列
import time
def add_delay_task(task_id, delay_seconds):
r.zadd('delay:queue', {task_id: time.time() + delay_seconds})
def poll_delay_task():
now = time.time()
tasks = r.zrangebyscore('delay:queue', 0, now)
if tasks:
r.zrem('delay:queue', *tasks)
return tasks
常见问题
Q1:String 类型的值最大能存多大?
Redis String 类型最大支持 512MB。存储大文件(如图片 Base64)应谨慎,建议上传到对象存储,将 URL 存入 Redis。
Q2:Hash 有多少字段会触发 ziplist -> dict 的转换?
默认配置下,当字段数超过 hash-max-ziplist-entries(默认 512)或任一字段值超过 hash-max-ziplist-value(默认 64 字节)时,会转换为 dict。
Q3:ZSet 的 score 相同时如何排序?
当 score 相同时,Redis 按字典序(lexicographical order)排列元素。这使得 ZSet 可以实现字典序范围查询(ZRANGEBYLEX),常用于实现拼音搜索、电话号码簿等场景。
延伸阅读
- Redis 持久化机制:RDB/AOF 与混合持久化
- Redis 分布式锁实现
- Redis 缓存策略与实战
- Redis 官方文档:Data Types https://redis.io/docs/data-types/