跳到主要内容

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)

注意事项

  1. 惰性过期:字段过期采用惰性删除策略,在访问时检查是否过期。这意味着过期的字段可能还会占用内存一段时间,直到被访问或被后台清理。

  2. 过期精度:过期时间精度为毫秒级,但实际过期可能略有延迟。

  3. 命令支持:目前 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 对比

特性HashString 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有序,支持范围查询
计数器StringINCR/DECR 操作
购物车Hash商品 ID 作为字段
时间线List/Sorted Set按时间排序
签到Set/BitmapSet 简单,Bitmap 省内存
延迟任务Sorted Set时间戳作为分数

性能对比

操作StringHashListSetSorted Set
单元素读写O(1)O(1)O(1) 两端O(1)O(log N)
范围查询N/AN/AO(N)N/AO(log N + M)
集合运算N/AN/AN/AO(N)O(N log N)

小结

  1. String:最基本类型,适合缓存、计数器、分布式锁。注意 SET 命令的高级选项。
  2. Hash:字段-值对,适合存储对象。小数据量时使用 ziplist 编码节省内存。
  3. List:有序列表,适合消息队列、时间线。两端操作 O(1),中间操作 O(N)。
  4. Set:无序集合,适合标签、关注关系。支持集合运算,元素自动去重。
  5. Sorted Set:有序集合,适合排行榜、延迟队列。使用跳表实现范围查询。

练习

  1. 使用 String 实现一个文章阅读计数器,支持自增和获取
  2. 使用 Hash 存储用户信息,并实现字段级别的更新和删除
  3. 使用 List 实现一个简单的消息队列,支持生产和消费
  4. 使用 Set 实现用户标签系统,并计算两个用户的共同标签
  5. 使用 Sorted Set 实现一个游戏排行榜,支持更新分数和获取排名

参考资料