本文将探讨 Redis 的 List, Set, Sorted Set 数据结构,通过 Python 代码示例展示如何构建消息队列、实现社交关系(共同好友)和实时排行榜,解决高并发下的常见业务难题。
前言
在掌握了 String 和 Hash 后,我们将探索 Redis 更为强大的三种数据结构:**List(列表)**、**Set(集合)** 和 **Sorted Set(有序集合)**。它们是构建复杂功能的基石,能优雅地解决消息队列、社交关系、实时排行榜等高级场景问题。
本篇读者收益:
- 精通 List 类型,掌握其作为消息队列、栈、最新列表的实现方法。
- 精通 Set 类型,掌握其去重特性和强大的集合运算(交集、并集、差集)。
- 精通 Sorted Set 类型,掌握其按分数排序的能力,轻松实现排行榜和范围查询。
- 能根据业务场景,在这三种结构间做出最合适的选择。
先修要求:假设读者已掌握 Redis 基础连接和 String/Hash 操作(详见系列前两篇)。
关键要点:
- List 是双向链表,头尾操作极快(
O(1)),是实现简单消息队列的利器。 - Set 保证元素唯一性,其集合运算能高效解决如“共同关注”等业务问题。
- Sorted Set 兼具 Set 的唯一性和 List 的有序性,是排行榜功能的绝配。
- 选择正确的数据结构,往往比优化代码更能提升系统性能和简化开发。
背景与原理简述
Redis 的魅力在于其为不同场景量身定制的数据结构。本篇主角是三种常用于实现复杂业务逻辑的类型:
- **List(列表)**: 一个简单的字符串列表,按插入顺序排序。它是双向链表实现的,这意味着在头部和尾部添加或删除元素的速度非常快(
O(1)),但通过索引访问中间元素较慢(O(n))。 - Set(集合): Redis Set 是字符串的无序集合,通过哈希表实现。其最大特点是 元素唯一性 和 高效的集合运算(求交集、并集、差集)。
- **Sorted Set(有序集合,简称 ZSet)**: 这是 Redis 最富表现力的数据结构之一。它类似 Set,但每个元素都关联一个
score(分数),元素按score进行排序。它通过**跳跃表(Skip List)** 和哈希表实现,保证了排序和高效访问。
理解它们的底层实现,有助于我们预测其性能并做出业务选择。
环境准备与快速上手
本篇所有示例基于以下连接客户端展开。
# filename: setup.py
import os
import redis
from redis import Redis
# 使用连接池创建客户端
pool = redis.ConnectionPool(
host=os.getenv('REDIS_HOST', 'localhost'),
port=int(os.getenv('REDIS_PORT', 6379)),
password=os.getenv('REDIS_PASSWORD'),
decode_responses=True,
max_connections=10
)
r = Redis(connection_pool=pool)
# 简单的连接测试
assert r.ping() is True
print("连接成功,开始操作 List, Set 和 Sorted Set!")
核心用法与代码示例
List (列表) 操作
基本操作与应用场景:消息队列、最新列表
# filename: list_operations.py
def list_basic_operations():
"""List 基本操作:队列、栈、最新列表"""
key = 'task_queue'
# 1. 生产者:从左侧/右侧推送任务 (LPUSH/RPUSH)
# LPUSH + RPOP 或 RPUSH + LPOP 可实现FIFO队列
r.lpush(key, 'task_video_1') # 左推
r.lpush(key, 'task_video_2')
r.rpush(key, 'task_email_1') # 右推
print(f"All tasks after push: {r.lrange(key, 0, -1)}") # ['task_video_2', 'task_video_1', 'task_email_1']
# 2. 消费者:从右侧/左侧弹出任务 (RPOP/LPOP)
# 经典队列模式:生产者LPUSH,消费者RPOP (或反之)
task = r.rpop(key) # 弹出最老的任务 'task_email_1'
print(f"Processing task: {task}")
# 3. 阻塞式弹出 (BRPOP/BLPOP) - 真正的队列消费者
# 应用场景:任务队列工作者。如果没有元素,会阻塞连接直到超时或有新元素。
# result = r.brpop(key, timeout=30) # 阻塞30秒等待任务
# if result:
# queue_name, task = result
# print(f"Got task from blocking pop: {task}")
# 4. 获取列表范围 (LRANGE) 和 长度 (LLEN)
tasks = r.lrange(key, 0, -1) # 获取所有元素,慎用!可能很大。
length = r.llen(key)
print(f"Remaining tasks ({length}): {tasks}")
# 5. 修剪列表 (LTRIM) - 维护固定长度的最新列表
# 应用场景:最新100条消息、最近登录用户列表
list_key = 'recent_logins'
for user_id in range(1, 105):
r.lpush(list_key, f'user_{user_id}')
# 只保留最新的100条,修剪掉索引100之后的所有元素
r.ltrim(list_key, 0, 99)
print(f"Recent logins count: {r.llen(list_key)}") # 100
# 运行示例
list_basic_operations()
Set (集合) 操作
基本操作与应用场景:标签系统、唯一性保证
# filename: set_operations.py
def set_basic_operations():
"""Set 基本操作:标签、抽奖、唯一值存储"""
article_id = 42
tags_key = f'article:{article_id}:tags'
# 1. 添加和获取成员 (SADD/SMEMBERS)
# 应用场景:文章标签、用户兴趣
r.sadd(tags_key, 'python', 'redis', 'database', 'tutorial')
all_tags = r.smembers(tags_key)
print(f"Article tags: {all_tags}")
# 2. 随机弹出/获取成员 (SPOP/SRANDMEMBER)
# 应用场景:抽奖、随机推荐
winner = r.spop(tags_key) # 随机移除并返回一个元素
random_tag = r.srandmember(tags_key) # 随机获取但不移除一个元素
print(f"Popped winner: {winner}, Random tag: {random_tag}")
# 3. 检查成员是否存在 (SISMEMBER) 和 移除成员 (SREM)
is_member = r.sismember(tags_key, 'python')
print(f"Is 'python' a tag? {is_member}")
r.srem(tags_key, 'database') # 移除指定成员
# 4. 集合运算的威力 - 社交系统的核心
# 应用场景:共同好友(交集)、可能认识的人(差集)、全部兴趣(并集)
user_a_friends = {'user_b', 'user_c', 'user_d'}
user_b_friends = {'user_a', 'user_c', 'user_e'}
r.sadd('user:a:friends', *user_a_friends) # 解包集合
r.sadd('user:b:friends', *user_b_friends)
# 交集 (SINTER): 共同好友
common_friends = r.sinter('user:a:friends', 'user:b:friends')
print(f"Common friends of A and B: {common_friends}") # {'user_c'}
# 并集 (SUNION): 所有好友
all_friends = r.sunion('user:a:friends', 'user:b:friends')
print(f"All friends: {all_friends}") # {'user_a', 'user_b', 'user_c', 'user_d', 'user_e'}
# 差集 (SDIFF): A有但B没有的好友
a_unique_friends = r.sdiff('user:a:friends', 'user:b:friends')
print(f"Friends only in A: {a_unique_friends}") # {'user_d'}
# 将运算结果存储到新key (SINTERSTORE/SUNIONSTORE/SDIFFSTORE)
r.sinterstore('user:a_b:common_friends', 'user:a:friends', 'user:b:friends')
# 运行示例
set_basic_operations()
Sorted Set (有序集合) 操作
基本操作与应用场景:排行榜、优先队列
# filename: zset_operations.py
def zset_basic_operations():
"""Sorted Set 基本操作:排行榜、按分范围查询"""
leaderboard_key = 'game:leaderboard'
# 1. 添加成员和分数 (ZADD)
# 应用场景:游戏积分榜、热门文章排行
r.zadd(leaderboard_key, {'player_a': 1000, 'player_b': 1500, 'player_c': 800, 'player_d': 1200})
# 2. 按分数升序/降序获取排名 (ZRANGE/ZREVRANGE)
# 获取前3名 (降序)
top_3 = r.zrevrange(leaderboard_key, 0, 2, withscores=True) # withscores=True 返回分数
print(f"Top 3 players: {top_3}") # [('player_b', 1500.0), ('player_d', 1200.0), ('player_a', 1000.0)]
# 3. 获取成员的排名和分数 (ZRANK/ZREVRANK/ZSCORE)
player_b_rank = r.zrevrank(leaderboard_key, 'player_b') # 降序排名,0是第一名
player_b_score = r.zscore(leaderboard_key, 'player_b')
print(f"Player B rank: {player_b_rank}, score: {player_b_score}")
# 4. 按分数范围查询 (ZRANGEBYSCORE/ZREVRANGEBYSCORE)
# 获取分数在 1000 到 1300 之间的玩家
mid_tier_players = r.zrangebyscore(leaderboard_key, 1000, 1300, withscores=True)
print(f"Players with score between 1000-1300: {mid_tier_players}")
# 5. 递增成员分数 (ZINCRBY) - 原子操作,排行榜核心
# 玩家A赢得一局游戏,加50分
new_score = r.zincrby(leaderboard_key, 50, 'player_a')
print(f"Player A's new score: {new_score}")
# 6. 获取集合大小 (ZCARD) 和 分数区间内成员数 (ZCOUNT)
total_players = r.zcard(leaderboard_key)
players_above_1000 = r.zcount(leaderboard_key, 1000, '+inf')
print(f"Total players: {total_players}, Players above 1000: {players_above_1000}")
# 运行示例
zset_basic_operations()
性能优化与容量规划
数据结构选择参考
| 需求场景 | 推荐结构 | 原因 |
|---|---|---|
| 消息队列 | List(BLPOP/BRPOP) | 原生支持阻塞弹出,顺序保证,实现简单。 |
| 最新 N 条记录 | List(LTRIM) | LTRIM 可轻松维护固定长度的列表,LPUSH 插入快。 |
| 标签、分类、唯一值 | Set | 自动去重,快速判断存在性。 |
| 共同好友、共同兴趣 | Set(SINTER) | 集合运算原生支持,效率极高。 |
| 排行榜、计分板 | Sorted Set | 按分数自动排序,支持范围查询和排名。 |
| 延迟队列 | Sorted Set(按时间戳为 score) | 可以按 score 范围查询到期的任务。 |
| 时间轴 | Sorted Set(按时间戳为 score) | 可以按时间范围高效检索。 |
大 Key 预警
- List: 避免使用过长的 List(如数十万元素)。
LRANGE,LTRIM等O(n)命令会变慢。 - Set / Sorted Set: 避免巨大的集合。
SMEMBERS,ZRANGE等命令会阻塞服务。对于大数据集,考虑使用SSCAN,ZSCAN进行增量迭代(后续文章详解)。 - 通用规则: 单个 Value 的大小不应超过 10KB,集合的元素数量应尽量控制在万级以内。
内存优化
Sorted Set 在元素较少且分数较小时,也会使用一种叫做 ziplist 的紧凑编码。可通过 zset-max-ziplist-entries 和 zset-max-ziplist-value 参数配置。
安全与可靠性
- 阻塞命令:
BLPOP,BRPOP等阻塞命令会占用一个连接。确保客户端库(如redis-py)的连接池配置了足够的连接数(max_connections)来处理并发阻塞。 - 原子性:
ZINCRBY,SPOP等命令都是原子操作,可以安全地在并发环境下使用。 - 生产环境禁用
KEYS**: 再次强调,绝对不要使用KEYS *来查找模式匹配的 Key,尤其是在包含大量 Key 的生產環境中。使用SCAN命令代替。
常见问题与排错
SMEMBERS或ZRANGE 0 -1导致服务变慢**: 这是遇到了大 Key 问题。立即使用SSCAN或ZSCAN进行替代,并考虑拆分大集合。BLPOP一直阻塞**: 检查超时参数设置,并确保有生产者往列表里推送消息。- Sorted Set 分数相同时的排序: 当多个成员分数相同时,它们将按**字典序**排列。
ZADD的XX和NX选项**:NX表示仅添加新成员,XX表示仅更新已有成员。善用它们可以避免意外覆盖或插入。
实战案例/最佳实践
案例一:可靠的简单消息队列
# filename: simple_queue.py
import threading
import time
class SimpleTaskQueue:
def __init__(self, redis_client, queue_name):
self.r = redis_client
self.queue_name = queue_name
def produce_task(self, task_data):
"""生产者:推送任务"""
# 使用 LPUSH 将任务推入队列左侧
self.r.lpush(self.queue_name, task_data)
print(f"Produced task: {task_data}")
def consume_task(self, worker_id):
"""消费者:阻塞地获取并处理任务"""
print(f"Worker {worker_id} started...")
while True:
# 使用 BRPOP 从队列右侧阻塞地获取任务,超时时间5秒
# BRPOP 返回一个元组 (list_name, element)
result = self.r.brpop(self.queue_name, timeout=5)
if result is None:
# 超时,没有任务,可以做一些其他工作或继续循环
print(f"Worker {worker_id}: No task, waiting...")
continue
queue_name, task_data = result
print(f"Worker {worker_id} processing task: {task_data}")
# 模拟任务处理时间
time.sleep(1)
print(f"Worker {worker_id} finished task: {task_data}")
# 使用示例
def demo_queue():
queue = SimpleTaskQueue(r, 'my_task_queue')
# 启动一个消费者线程
consumer_thread = threading.Thread(target=queue.consume_task, args=('worker_1',), daemon=True)
consumer_thread.start()
# 主线程生产任务
for i in range(5):
queue.produce_task(f'task_data_{i}')
time.sleep(0.5)
time.sleep(6) # 让消费者有时间处理
# demo_queue()
案例二:实时游戏排行榜
# filename: game_leaderboard.py
class GameLeaderboard:
def __init__(self, redis_client, leaderboard_key):
self.r = redis_client
self.leaderboard_key = leaderboard_key
def add_player(self, player_id, initial_score=0):
"""添加玩家到排行榜"""
self.r.zadd(self.leaderboard_key, {player_id: initial_score})
def update_score(self, player_id, delta):
"""更新玩家分数(增加或减少)"""
new_score = self.r.zincrby(self.leaderboard_key, delta, player_id)
return new_score
def get_top_n(self, n=10):
"""获取前N名玩家"""
return self.r.zrevrange(self.leaderboard_key, 0, n-1, withscores=True)
def get_player_rank_and_score(self, player_id):
"""获取玩家的排名和分数"""
rank = self.r.zrevrank(self.leaderboard_key, player_id)
if rank is None:
return None, None
score = self.r.zscore(self.leaderboard_key, player_id)
return rank + 1, score # 排名从0开始,转为从1开始更直观
def get_players_around_me(self, player_id, range_size=2):
"""获取玩家附近的竞争对手(前后各range_size名)"""
rank = self.r.zrevrank(self.leaderboard_key, player_id)
if rank is None:
return []
start = max(0, rank - range_size)
end = rank + range_size
return self.r.zrevrange(self.leaderboard_key, start, end, withscores=True)
# 使用示例
def demo_leaderboard():
lb = GameLeaderboard(r, 'my_game_leaderboard')
players = ['player_1', 'player_2', 'player_3', 'player_4']
# 初始化
for p in players:
lb.add_player(p)
# 模拟游戏更新
lb.update_score('player_1', 100)
lb.update_score('player_2', 150)
lb.update_score('player_3', 75)
lb.update_score('player_4', 200)
lb.update_score('player_1', 50) # player_1 又得了50分
# 查询
top_2 = lb.get_top_n(2)
print(f"Top 2: {top_2}")
rank, score = lb.get_player_rank_and_score('player_1')
print(f"Player_1 rank: {rank}, score: {score}")
around = lb.get_players_around_me('player_1', 1)
print(f"Around player_1: {around}")
demo_leaderboard()
小结
List、Set 和 Sorted Set 将 Redis 从简单的键值存储提升为了一个强大的数据结构和计算平台。List 提供了顺序和阻塞操作,Set 提供了唯一性和集合运算,Sorted Set 提供了排序和范围查询。
482

被折叠的 条评论
为什么被折叠?



