Redis 数据类型
Redis 是一个数据结构服务器,支持多种原生数据类型。理解这些数据类型及其底层实现原理,是高效使用 Redis 的基础。本章将深入讲解五种基本数据类型的原理、命令和最佳实践。
概述
Redis 支持以下核心数据类型:
| 数据类型 | 说明 | 典型应用场景 |
|---|---|---|
| String | 字符串/二进制数据 | 缓存、计数器、分布式锁 |
| Hash | 字段-值对集合 | 对象存储、购物车 |
| List | 有序字符串列表 | 消息队列、时间线 |
| Set | 无序唯一集合 | 标签、共同好友 |
| Sorted Set | 有序唯一集合 | 排行榜、延迟队列 |
字符串(String)
String 是 Redis 最基本的数据类型,可以存储字符串、整数或二进制数据。底层使用简单动态字符串(SDS)实现。
底层实现原理
Redis 的字符串没有直接使用 C 语言的字符串,而是自己构建了简单动态字符串(Simple Dynamic String,SDS):
struct sdshdr {
int len; // 已使用长度
int free; // 剩余可用长度
char buf[]; // 字节数组
};
SDS 相比 C 字符串的优势:
- O(1) 获取长度:直接读取 len 属性,无需遍历
- 防止缓冲区溢出:修改前检查空间,自动扩容
- 减少内存分配次数:空间预分配和惰性释放
- 二进制安全:可以存储任意二进制数据
内存编码
String 有三种内部编码方式:
| 编码 | 使用条件 | 特点 |
|---|---|---|
| int | 整数值 | 节省内存 |
| embstr | 字符串长度 ≤ 44 字节 | 一次内存分配,紧凑存储 |
| raw | 字符串长度 > 44 字节 | SDS 实现,可修改 |
# 查看编码类型
127.0.0.1:6379> SET num 100
OK
127.0.0.1:6379> OBJECT ENCODING num
"int"
127.0.0.1:6379> SET short "hello"
OK
127.0.0.1:6379> OBJECT ENCODING short
"embstr"
127.0.0.1:6379> SET long "this is a very long string that exceeds 44 bytes..."
OK
127.0.0.1:6379> OBJECT ENCODING long
"raw"
基本操作
# 设置值
SET key value
SET user:1 "张三"
# 获取值
GET key
GET user:1 # "张三"
# 设置值并设置过期时间(秒)
SETEX key seconds value
SETEX session:abc 3600 "user_data"
# 设置值,仅当 key 不存在时
SETNX key value
SETNX lock:resource "locked" # 返回 1 表示设置成功
# 批量设置
MSET key1 value1 key2 value2
MSET user:1:name "张三" user:1:age "25"
# 批量获取
MGET key1 key2
MGET user:1:name user:1:age
SET 命令的高级选项
SET 命令支持多种选项,这些选项让 SET 可以替代 SETNX、SETEX 等命令:
# NX:仅当 key 不存在时设置
SET key value NX
# XX:仅当 key 存在时设置
SET key value XX
# EX:设置过期时间(秒)
SET key value EX 60
# PX:设置过期时间(毫秒)
SET key value PX 60000
# 组合使用:分布式锁的推荐写法
SET lock:order:123 "uuid" NX PX 30000
数值操作
# 设置数值
SET counter 100
# 递增(原子操作)
INCR counter # 101
INCRBY counter 10 # 111
# 递减
DECR counter # 110
DECRBY counter 10 # 100
# 浮点数递增
SET price 10.50
INCRBYFLOAT price 2.30 # "12.8"
原子性说明:INCR 命令是原子的,即使多个客户端同时对同一个 key 执行 INCR,也不会产生竞态条件。Redis 会保证读取、递增、写入这三个操作作为一个原子操作执行。
字符串操作
# 追加字符串
SET msg "Hello"
APPEND msg " World" # 返回长度 11
GET msg # "Hello World"
# 获取字符串长度
STRLEN msg # 11
# 获取子串(O(N) 操作,慎用长字符串)
GETRANGE msg 0 4 # "Hello"
# 设置子串
SETRANGE msg 6 "Redis" # "Hello Redis"
应用场景详解
1. 缓存用户信息
# 存储 JSON 格式的用户信息
SET user:1001 '{"name":"张三","age":25,"city":"北京"}'
# 读取并反序列化
GET user:1001
2. 计数器
# 文章阅读量
INCR article:123:views
# 点赞数
INCR article:123:likes
# 获取当前计数
GET article:123:views
3. 分布式锁
# 加锁(推荐方式)
SET lock:order:123 "uuid-xxx" NX PX 30000
# 释放锁(使用 Lua 脚本保证原子性)
EVAL "
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
" 1 lock:order:123 "uuid-xxx"
4. 分布式 ID 生成
# 生成订单号
INCR global:order:id # 返回唯一递增 ID
# 按日期分组的 ID
INCR order:id:2024-01-15
性能考虑
- 大多数 String 操作是 O(1),非常高效
- GETRANGE、SETRANGE 是 O(N),处理长字符串时需注意
- 单个 String 最大 512MB,但不建议存储过大的值
- 批量操作 MSET/MGET 可以减少网络往返
哈希(Hash)
Hash 是一个键值对集合,适合存储对象。它类似于 Python 的字典、Java 的 HashMap。
底层实现原理
Hash 有两种内部编码:
| 编码 | 使用条件 | 特点 |
|---|---|---|
| ziplist(压缩列表) | 字段数量 ≤ 512,值长度 ≤ 64 字节 | 连续内存,紧凑存储 |
| hashtable(哈希表) | 不满足 ziplist 条件 | 标准哈希表,O(1) 访问 |
Redis 会根据数据量自动在两种编码间切换:
# 小数据量使用 ziplist
127.0.0.1:6379> HSET user:1 name "张三" age 25
(integer) 2
127.0.0.1:6379> OBJECT ENCODING user:1
"ziplist"
# 数据量增大后自动转为 hashtable
# (需要大量字段才会触发)
基本操作
# 设置单个字段
HSET user:1 name "张三"
HSET user:1 age 25
# 设置多个字段
HMSET user:2 name "李四" age 30 city "上海"
# 获取单个字段
HGET user:1 name # "张三"
# 获取多个字段
HMGET user:1 name age
# 1) "张三"
# 2) "25"
# 获取所有字段和值
HGETALL user:1
# 1) "name"
# 2) "张三"
# 3) "age"
# 4) "25"
# 获取所有字段名
HKEYS user:1 # 1) "name" 2) "age"
# 获取所有值
HVALS user:1 # 1) "张三" 2) "25"
# 检查字段是否存在
HEXISTS user:1 name # 1
# 删除字段
HDEL user:1 age
# 获取字段数量
HLEN user:1
数值操作
# 字段值递增
HSET user:1 score 100
HINCRBY user:1 score 10 # 110
HINCRBYFLOAT user:1 score 5.5 # "115.5"
条件设置
# 仅当字段不存在时设置
HSETNX user:1 name "王五" # 0,因为 name 已存在
HSETNX user:1 email "[email protected]" # 1
字段过期(Redis 7.4+)
Redis 7.4 引入了 Hash 字段过期功能,这是用户期待已久的重要特性。它允许为 Hash 中的单个字段设置过期时间,而不是只能为整个 Hash 设置过期。
为什么需要字段过期?
在 Redis 7.4 之前,如果需要为 Hash 中的不同字段设置不同的过期时间,只能采用以下变通方案:
# 方案一:为每个字段使用单独的 Key(内存开销大)
SET user:1001:name "张三" EX 3600
SET user:1001:session "abc123" EX 1800
# 方案二:使用 Sorted Set 实现延迟过期(复杂且不直观)
ZADD user:1001:expires <timestamp> name
字段过期功能解决了这个问题,让 Hash 更加灵活实用。
设置过期时间
HEXPIRE:设置字段的剩余 TTL(秒)
# 完整语法
HEXPIRE key seconds [NX | XX | GT | LT] FIELDS numfields field [field ...]
# 为单个字段设置 60 秒过期
127.0.0.1:6379> HEXPIRE user:1 60 FIELDS 1 name
1) (integer) 1 # 1 表示成功
# 为多个字段设置相同的过期时间
127.0.0.1:6379> HEXPIRE user:1 300 FIELDS 2 name session
1) (integer) 1
2) (integer) 1
条件选项说明:
| 选项 | 说明 |
|---|---|
NX | 仅当字段没有过期时间时才设置 |
XX | 仅当字段已有过期时间时才设置 |
GT | 仅当新的过期时间大于当前过期时间时才设置 |
LT | 仅当新的过期时间小于当前过期时间时才设置 |
# 仅当字段没有过期时间时设置(NX)
127.0.0.1:6379> HEXPIRE user:1 300 NX FIELDS 1 session
1) (integer) 1 # 设置成功
# 再次使用 NX 会失败
127.0.0.1:6379> HEXPIRE user:1 600 NX FIELDS 1 session
1) (integer) 0 # 字段已有过期时间,设置失败
# 使用 GT 延长过期时间
127.0.0.1:6379> HEXPIRE user:1 600 GT FIELDS 1 session
1) (integer) 1 # 新时间更长,设置成功
# 使用 LT 缩短过期时间
127.0.0.1:6379> HEXPIRE user:1 60 LT FIELDS 1 session
1) (integer) 1 # 新时间更短,设置成功
HPEXPIRE:设置字段的剩余 TTL(毫秒)
# 设置 5000 毫秒过期
127.0.0.1:6379> HPEXPIRE user:1 5000 FIELDS 1 session
1) (integer) 1
HEXPIREAT:设置字段的过期时间戳(秒)
# 设置过期时间为 2024-12-31 23:59:59(Unix 时间戳)
127.0.0.1:6379> HEXPIREAT user:1 1735689599 FIELDS 1 temp_data
1) (integer) 1
HPEXPIREAT:设置字段的过期时间戳(毫秒)
# 使用毫秒精度的时间戳
127.0.0.1:6379> HPEXPIREAT user:1 1735689599000 FIELDS 1 temp_data
1) (integer) 1
查询过期时间
HTTL:获取字段的剩余 TTL(秒)
127.0.0.1:6379> HTTL user:1 FIELDS 2 name session
1) (integer) 58
2) (integer) 298
HPTTL:获取字段的剩余 TTL(毫秒)
127.0.0.1:6379> HPTTL user:1 FIELDS 1 session
1) (integer) 4892
HEXPIRETIME:获取字段的过期时间戳(秒)
127.0.0.1:6379> HEXPIRETIME user:1 FIELDS 1 name
1) (integer) 1735689599
HPEXPIRETIME:获取字段的过期时间戳(毫秒)
127.0.0.1:6379> HPEXPIRETIME user:1 FIELDS 1 name
1) (integer) 1735689599000
移除过期时间
HPERSIST:移除字段的过期时间
127.0.0.1:6379> HPERSIST user:1 FIELDS 1 name
1) (integer) 1 # 1 表示成功移除
Redis 8.0 新增命令
Redis 8.0 在 Hash 字段过期功能的基础上,新增了三个便捷命令,将"获取/设置"与"过期操作"合并为原子操作。
HGETEX:获取字段值并设置过期时间
# 语法:HGETEX key [FX] [EX seconds | PX milliseconds | EXAT timestamp | PXAT timestamp] FIELDS numfields field [field ...]
# 获取字段并同时设置 60 秒过期
127.0.0.1:6379> HGETEX user:1 EX 60 FIELDS 1 session
"abc123"
# 获取后保持过期时间不变
127.0.0.1:6379> HGETEX user:1 FIELDS 1 session
"abc123"
# 获取后移除过期时间(持久化)
127.0.0.1:6379> HGETEX user:1 PERSIST FIELDS 1 session
"abc123"
使用场景:实现"滑动过期"的缓存读取——每次读取时自动续期。
import redis
r = redis.Redis()
def get_session_with_refresh(user_id, ttl=1800):
"""获取会话并自动续期"""
key = f"session:{user_id}"
# 原子操作:获取并续期
session = r.execute_command('HGETEX', key, 'EX', ttl, 'FIELDS', 1, 'data')
return session
HSETEX:设置字段值并设置过期时间
# 语法:HSETEX key [FX] [EX seconds | PX milliseconds | EXAT timestamp | PXAT timestamp] FIELDS numfields field value [field value ...]
# 设置字段并设置 300 秒过期
127.0.0.1:6379> HSETEX cache:page EX 300 FIELDS 1 content "<html>...</html>"
OK
# 设置多个字段并设置过期
127.0.0.1:6379> HSETEX user:temp EX 60 FIELDS 2 token "abc" verify_code "123456"
OK
使用场景:一次性完成写入和过期设置,无需两条命令。
def cache_page(url, content, ttl=300):
"""缓存页面内容并设置过期"""
key = f"cache:page:{url}"
# 原子操作:设置值和过期时间
r.execute_command('HSETEX', key, 'EX', ttl, 'FIELDS', 1, 'content', content)
HGETDEL:获取字段值并删除
# 语法:HGETDEL key FIELDS numfields field [field ...]
# 获取并删除字段(一次性操作)
127.0.0.1:6379> HGETDEL user:1 FIELDS 1 one_time_token
"token_abc123"
# 字段已删除,再次获取返回 nil
127.0.0.1:6379> HGET user:1 one_time_token
(nil)
使用场景:实现"一次性令牌"或"消费型数据"。
def consume_one_time_token(token_id):
"""消费一次性令牌(只能使用一次)"""
key = f"tokens:{token_id}"
# 原子操作:获取并删除
token = r.execute_command('HGETDEL', key, 'FIELDS', 1, 'value')
if token:
return token # 令牌有效,返回值
return None # 令牌已使用或不存在
三个命令的对比:
| 命令 | 功能 | 典型场景 |
|---|---|---|
| HGETEX | 获取 + 设置/修改过期 | 滑动过期缓存 |
| HSETEX | 设置 + 设置过期 | 带过期的新写入 |
| HGETDEL | 获取 + 删除 | 一次性令牌消费 |
返回值说明
过期相关命令返回一个数组,每个元素对应一个字段:
| 返回值 | 说明 |
|---|---|
1 | 过期时间设置/移除成功 |
0 | 字段不存在 |
-1 | 字段存在但没有设置过期时间 |
-2 | 字段不存在或已过期 |
# 示例:查看返回值
127.0.0.1:6379> HTTL user:1 FIELDS 3 name age nonexistent
1) (integer) 58 # 有过期时间,剩余 58 秒
2) (integer) -1 # 存在但没有过期时间
3) (integer) -2 # 字段不存在
实际应用示例
场景一:事件追踪
import redis
import time
r = redis.Redis()
def track_event(user_id, event_type):
"""追踪用户事件,自动过期"""
key = f"events:{user_id}"
# 添加事件
r.hset(key, event_type, str(time.time()))
# 设置 1 小时后过期
r.execute_command('HEXPIRE', key, 3600, 'FIELDS', 1, event_type)
def get_event_count(user_id):
"""获取过去 1 小时内的事件数"""
key = f"events:{user_id}"
return r.hlen(key)
# 使用
track_event('user_001', 'login')
track_event('user_001', 'click')
track_event('user_001', 'purchase')
print(f"最近事件数: {get_event_count('user_001')}")
场景二:欺诈检测
import redis
import time
r = redis.Redis()
def record_transaction(user_id, hour_timestamp):
"""记录每小时交易数,48 小时后过期"""
key = f"transactions:{user_id}"
field = str(hour_timestamp)
# 递增交易计数
r.hincrby(key, field, 1)
# 设置 48 小时后过期
r.execute_command('HEXPIRE', key, 48 * 3600, 'FIELDS', 1, field)
def get_hourly_transactions(user_id, hours=24):
"""获取过去 N 小时的交易统计"""
key = f"transactions:{user_id}"
current_hour = int(time.time() // 3600)
result = {}
for i in range(hours):
hour = current_hour - i
count = r.hget(key, str(hour))
if count:
result[hour] = int(count)
return result
场景三:活跃会话追踪
import redis
r = redis.Redis()
def add_session(user_id, session_id, ttl=1800):
"""添加活跃会话"""
key = f"sessions:{user_id}"
# 添加会话
r.hset(key, session_id, "active")
# 设置会话 30 分钟后过期
r.execute_command('HEXPIRE', key, ttl, 'FIELDS', 1, session_id)
def get_active_session_count(user_id):
"""获取活跃会话数"""
key = f"sessions:{user_id}"
return r.hlen(key)
def refresh_session(user_id, session_id, ttl=1800):
"""刷新会话过期时间"""
key = f"sessions:{user_id}"
r.execute_command('HEXPIRE', key, ttl, 'FIELDS', 1, session_id)
场景四:客户会话管理
import redis
import json
r = redis.Redis()
def create_session(customer_id, session_id, session_data):
"""创建客户会话"""
# 主会话存储
session_key = f"session:{session_id}"
r.set(session_key, json.dumps(session_data), ex=3600)
# 客户的会话索引
customer_key = f"customer:{customer_id}:sessions"
r.hset(customer_key, session_id, "active")
# 会话字段 1 小时后过期
r.execute_command('HEXPIRE', customer_key, 3600, 'FIELDS', 1, session_id)
def get_customer_sessions(customer_id):
"""获取客户的所有活跃会话"""
customer_key = f"customer:{customer_id}:sessions"
return r.hkeys(customer_key)
注意事项
-
惰性过期:字段过期采用惰性删除策略,在访问时检查是否过期。这意味着过期的字段可能还会占用内存一段时间,直到被访问或被后台清理。
-
过期精度:过期时间精度为毫秒级,但实际过期可能略有延迟。
-
命令支持:目前 Redis 客户端库对字段过期的支持还在逐步完善中,可能需要使用
execute_command方法调用。
应用场景详解
1. 存储用户信息
相比使用 String 存储 JSON,Hash 有以下优势:
- 可以单独访问和修改某个字段
- 内存效率更高(小数据量时)
- 支持字段级别的操作
# 存储用户信息
HSET user:1001 name "张三" age 25 email "[email protected]"
# 只更新年龄
HSET user:1001 age 26
# 只获取姓名
HGET user:1001 name
2. 购物车
# 添加商品到购物车
HSET cart:user:1001 product:2001 2 product:2002 1
# 修改商品数量
HINCRBY cart:user:1001 product:2001 1
# 删除商品
HDEL cart:user:1001 product:2002
# 获取购物车所有商品
HGETALL cart:user:1001
3. 计数器组
# 存储多个计数器
HSET stats:2024-01-01 page_views 0 unique_visitors 0
# 递增计数器
HINCRBY stats:2024-01-01 page_views 1
HINCRBY stats:2024-01-01 unique_visitors 1
Hash vs String JSON 对比
| 特性 | Hash | String JSON |
|---|---|---|
| 内存占用 | 小数据量时更优 | 大数据量时更优 |
| 字段访问 | 单独访问任意字段 | 需要读取整个 JSON |
| 字段修改 | 直接修改 | 需要读取、解析、修改、写回 |
| 复杂结构 | 不支持嵌套 | 支持嵌套对象 |
| 适用场景 | 简单对象、频繁字段访问 | 复杂对象、整体读写 |
列表(List)
List 是一个有序的字符串列表,按插入顺序排序。底层使用 quicklist(快速列表)实现。
底层实现原理
Redis 3.2 之后,List 统一使用 quicklist(快速列表)实现:
quicklist = ziplist 的双向链表
+--------+--------+--------+
| ziplist | ziplist | ziplist |
+--------+--------+--------+
↓ ↓ ↓
[a,b,c] [d,e,f] [g,h,i]
quicklist 的优势:
- 结合了 ziplist 的内存紧凑和 linkedlist 的修改高效
- 两端操作 O(1)
- 中间操作需要遍历,时间复杂度较高
基本操作
# 左侧插入(头部)
LPUSH mylist "a" "b" "c" # 返回列表长度 3
# 列表内容: ["c", "b", "a"]
# 右侧插入(尾部)
RPUSH mylist "d" "e" # 返回列表长度 5
# 列表内容: ["c", "b", "a", "d", "e"]
# 获取列表长度
LLEN mylist # 5
# 获取范围内的元素
LRANGE mylist 0 -1 # 获取所有元素
LRANGE mylist 0 2 # 获取前三个元素
# 获取指定索引的元素
LINDEX mylist 0 # "c"
LINDEX mylist -1 # "e"
# 左侧弹出
LPOP mylist # "c"
# 右侧弹出
RPOP mylist # "e"
# 阻塞弹出(用于消息队列)
BLPOP mylist 5 # 5秒超时
BRPOP mylist 5
修改操作
# 设置指定索引的值
LSET mylist 0 "new_value"
# 在指定元素前/后插入
LINSERT mylist BEFORE "a" "before_a"
LINSERT mylist AFTER "a" "after_a"
# 删除指定数量的元素
LREM mylist 2 "a" # 删除 2 个 "a"
# 保留指定范围内的元素(常用于限制列表长度)
LTRIM mylist 0 9 # 只保留前 10 个元素
应用场景详解
1. 消息队列
# 生产者:从左侧推入消息
LPUSH queue:email "email_data_1"
LPUSH queue:email "email_data_2"
# 消费者:从右侧弹出消息
RPOP queue:email # "email_data_1"
# 阻塞消费(推荐)
BRPOP queue:email 5 # 没有消息时阻塞等待
2. 最新消息列表
# 添加新消息
LPUSH news:latest "news_id_1"
LPUSH news:latest "news_id_2"
# 只保留最新 100 条
LTRIM news:latest 0 99
# 获取最新 10 条
LRANGE news:latest 0 9
3. 时间线(类似 Twitter)
# 发布动态时,推送到粉丝的时间线
LPUSH timeline:user:1 "post:100"
LPUSH timeline:user:1 "post:101"
# 获取用户时间线
LRANGE timeline:user:1 0 20 # 最近 20 条动态
集合(Set)
Set 是无序的唯一字符串集合,类似于数学中的集合概念。
底层实现原理
Set 有两种内部编码:
| 编码 | 使用条件 | 特点 |
|---|---|---|
| intset(整数集合) | 所有元素都是整数,数量 ≤ 512 | 有序、紧凑 |
| hashtable(哈希表) | 不满足 intset 条件 | 无序、O(1) 查找 |
基本操作
# 添加元素
SADD myset "a" "b" "c" # 返回添加的元素数量
# 获取所有元素
SMEMBERS myset # 1) "a" 2) "b" 3) "c"
# 检查元素是否存在
SISMEMBER myset "a" # 1
# 删除元素
SREM myset "a"
# 获取集合大小
SCARD myset
# 随机获取元素
SRANDMEMBER myset # 随机返回一个元素
SRANDMEMBER myset 2 # 随机返回 2 个元素(不删除)
# 随机弹出元素
SPOP myset
集合运算
# 创建两个集合
SADD set1 "a" "b" "c"
SADD set2 "b" "c" "d"
# 交集(同时存在于两个集合的元素)
SINTER set1 set2 # 1) "b" 2) "c"
# 并集(两个集合的所有元素)
SUNION set1 set2 # 1) "a" 2) "b" 3) "c" 4) "d"
# 差集(在 set1 但不在 set2 的元素)
SDIFF set1 set2 # 1) "a"
# 将结果存储到新集合
SINTERSTORE result set1 set2
SUNIONSTORE result set1 set2
SDIFFSTORE result set1 set2
应用场景详解
1. 标签系统
# 为文章添加标签
SADD article:1:tags "redis" "database" "nosql"
SADD article:2:tags "redis" "cache"
# 获取文章的所有标签
SMEMBERS article:1:tags
# 查找共同标签(推荐系统)
SINTER article:1:tags article:2:tags # "redis"
2. 用户关注系统
# 用户关注的人
SADD user:1:following user:2 user:3
SADD user:2:following user:1 user:3
# 用户的粉丝
SADD user:2:followers user:1
# 共同关注
SINTER user:1:following user:2:following # user:3
# 我关注的人也关注了(推荐关注)
SDIFF user:2:following user:1:following # user:1
3. 抽奖系统
# 添加参与者
SADD lottery:2024 user:1 user:2 user:3 user:4 user:5
# 随机抽取中奖者(不重复)
SPOP lottery:2024 # 随机移除一个用户
# 随机抽取但不移除
SRANDMEMBER lottery:2024 2 # 随机选取 2 个用户
4. 签到系统
# 用户签到
SADD sign:2024-01-01 user:1 user:2 user:3
# 检查是否签到
SISMEMBER sign:2024-01-01 user:1 # 1
# 当日签到人数
SCARD sign:2024-01-01
# 连续签到(取交集)
SINTER sign:2024-01-01 sign:2024-01-02 sign:2024-01-03
有序集合(Sorted Set)
Sorted Set 是带分数的有序集合,每个元素关联一个分数,按分数排序。
底层实现原理
Sorted Set 使用 skiplist + dict 的组合实现:
- dict:存储元素到分数的映射,O(1) 查找分数
- skiplist(跳表):按分数排序,支持范围查询
跳表结构示意:
Level 3: [head]--------------------->[tail]
Level 2: [head]------>[B]----------->[tail]
Level 1: [head]->[A]->[B]->[C]->[D]->[tail]
Level 0: [head]->[A]->[B]->[C]->[D]->[tail]
分数: 1 3 5 8
基本操作
# 添加元素
ZADD leaderboard 100 "user1" 200 "user2" 150 "user3"
# 获取元素分数
ZSCORE leaderboard "user1" # "100"
# 获取排名(从 0 开始,分数从低到高)
ZRANK leaderboard "user1" # 0
# 获取排名(分数从高到低)
ZREVRANK leaderboard "user2" # 0
# 递增分数
ZINCRBY leaderboard 50 "user1" # "150"
# 获取集合大小
ZCARD leaderboard
# 获取分数范围内的元素数量
ZCOUNT leaderboard 100 200
范围查询
# 按排名范围获取(分数从低到高)
ZRANGE leaderboard 0 2 # 前 3 名(分数最低)
ZRANGE leaderboard 0 -1 # 所有元素
# 按排名范围获取(分数从高到低)
ZREVRANGE leaderboard 0 2 # 前 3 名(分数最高)
# 按分数范围获取
ZRANGEBYSCORE leaderboard 100 200
ZRANGEBYSCORE leaderboard (100 200 # 不包含 100
ZRANGEBYSCORE leaderboard -inf +inf # 所有元素
# 带分数返回
ZRANGE leaderboard 0 -1 WITHSCORES
ZREVRANGE leaderboard 0 -1 WITHSCORES
删除操作
# 删除元素
ZREM leaderboard "user1"
# 按排名范围删除
ZREMRANGEBYRANK leaderboard 0 2
# 按分数范围删除
ZREMRANGEBYSCORE leaderboard 0 100
集合运算
# 创建两个有序集合
ZADD set1 1 "a" 2 "b" 3 "c"
ZADD set2 2 "b" 3 "c" 4 "d"
# 并集(合并分数)
ZUNIONSTORE result 2 set1 set2
# 并集(自定义权重)
ZUNIONSTORE result 2 set1 set2 WEIGHTS 1 2
# 并集(自定义聚合方式)
ZUNIONSTORE result 2 set1 set2 AGGREGATE SUM|MIN|MAX
# 交集
ZINTERSTORE result 2 set1 set2
应用场景详解
1. 排行榜
# 更新用户分数
ZADD game:leaderboard 10000 "player1"
ZADD game:leaderboard 8500 "player2"
ZINCRBY game:leaderboard 500 "player1" # 增加分数
# 获取 Top 10
ZREVRANGE game:leaderboard 0 9 WITHSCORES
# 获取用户排名
ZREVRANK game:leaderboard "player1"
# 获取分数段用户
ZRANGEBYSCORE game:leaderboard 9000 10000
2. 延迟队列
# 添加任务(时间戳作为分数)
ZADD delay:queue 1704067200 "task:1" # 到期时间戳
# 获取到期的任务
ZRANGEBYSCORE delay:queue 0 1704067200
# 删除已处理的任务
ZREM delay:queue "task:1"
3. 热榜
# 增加热度
ZINCRBY hot:topics 1 "topic:1"
# 获取热门话题 Top 10
ZREVRANGE hot:topics 0 9
# 获取热度衰减(定时任务)
ZINCRBY hot:topics -1 "topic:1"
4. 带权重的标签
# 存储用户兴趣(分数表示兴趣程度)
ZADD user:1:interests 10 "redis" 8 "mysql" 5 "python"
# 获取用户最感兴趣的标签
ZREVRANGE user:1:interests 0 4 # 前 5 个兴趣
数据类型选择指南
| 场景 | 推荐类型 | 说明 |
|---|---|---|
| 简单缓存 | String | 直接存储序列化数据 |
| 对象存储 | Hash | 字段级别操作,内存效率高 |
| 消息队列 | List | 有序,支持阻塞操作 |
| 标签/去重 | Set | 自动去重,支持集合运算 |
| 排行榜 | Sorted Set | 有序,支持范围查询 |
| 计数器 | String | INCR/DECR 操作 |
| 购物车 | Hash | 商品 ID 作为字段 |
| 时间线 | List/Sorted Set | 按时间排序 |
| 签到 | Set/Bitmap | Set 简单,Bitmap 省内存 |
| 延迟任务 | Sorted Set | 时间戳作为分数 |
性能对比
| 操作 | String | Hash | List | Set | Sorted Set |
|---|---|---|---|---|---|
| 单元素读写 | O(1) | O(1) | O(1) 两端 | O(1) | O(log N) |
| 范围查询 | N/A | N/A | O(N) | N/A | O(log N + M) |
| 集合运算 | N/A | N/A | N/A | O(N) | O(N log N) |
小结
- String:最基本类型,适合缓存、计数器、分布式锁。注意 SET 命令的高级选项。
- Hash:字段-值对,适合存储对象。小数据量时使用 ziplist 编码节省内存。
- List:有序列表,适合消息队列、时间线。两端操作 O(1),中间操作 O(N)。
- Set:无序集合,适合标签、关注关系。支持集合运算,元素自动去重。
- Sorted Set:有序集合,适合排行榜、延迟队列。使用跳表实现范围查询。
练习
- 使用 String 实现一个文章阅读计数器,支持自增和获取
- 使用 Hash 存储用户信息,并实现字段级别的更新和删除
- 使用 List 实现一个简单的消息队列,支持生产和消费
- 使用 Set 实现用户标签系统,并计算两个用户的共同标签
- 使用 Sorted Set 实现一个游戏排行榜,支持更新分数和获取排名