艾体宝干货 | Redis Python 开发系列#3 核心数据结构(下)

部署运行你感兴趣的模型镜像

本文将探讨 Redis 的 List, Set, Sorted Set 数据结构,通过 Python 代码示例展示如何构建消息队列、实现社交关系(共同好友)和实时排行榜,解决高并发下的常见业务难题。

前言

在掌握了 String 和 Hash 后,我们将探索 Redis 更为强大的三种数据结构:**List(列表)**、**Set(集合)** 和 **Sorted Set(有序集合)**。它们是构建复杂功能的基石,能优雅地解决消息队列、社交关系、实时排行榜等高级场景问题。

本篇读者收益​:

  • 精通 List 类型,掌握其作为消息队列、栈、最新列表的实现方法。
  • 精通 Set 类型,掌握其去重特性和强大的集合运算(交集、并集、差集)。
  • 精通 Sorted Set 类型,掌握其按分数排序的能力,轻松实现排行榜和范围查询。
  • 能根据业务场景,在这三种结构间做出最合适的选择。

先修要求​:假设读者已掌握 Redis 基础连接和 String/Hash 操作(详见系列前两篇)。

关键要点​:

  1. List 是双向链表,头尾操作极快(O(1)),是实现简单消息队列的利器。
  2. Set 保证元素唯一性,其集合运算能高效解决如“共同关注”等业务问题。
  3. Sorted Set 兼具 Set 的唯一性和 List 的有序性,是排行榜功能的绝配。
  4. 选择正确的数据结构,往往比优化代码更能提升系统性能和简化开发。

背景与原理简述

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, LTRIMO(n) 命令会变慢。
  • Set / Sorted Set​: 避免巨大的集合。SMEMBERS, ZRANGE 等命令会阻塞服务。对于大数据集,考虑使用 SSCAN, ZSCAN 进行增量迭代(后续文章详解)。
  • 通用规则​: 单个 Value 的大小不应超过 10KB,集合的元素数量应尽量控制在万级以内。

内存优化

Sorted Set 在元素较少且分数较小时,也会使用一种叫做 ziplist 的紧凑编码。可通过 zset-max-ziplist-entrieszset-max-ziplist-value 参数配置。

安全与可靠性

  1. 阻塞命令​: BLPOP, BRPOP 等阻塞命令会占用一个连接。确保客户端库(如 redis-py)的连接池配置了足够的连接数(max_connections)来处理并发阻塞。
  2. 原子性​: ZINCRBY, SPOP 等命令都是原子操作,可以安全地在并发环境下使用。
  3. 生产环境禁用 ​KEYS**: 再次强调,绝对不要使用 KEYS * 来查找模式匹配的 Key,尤其是在包含大量 Key 的生產環境中。使用 SCAN 命令代替。

常见问题与排错

  • SMEMBERSZRANGE 0 -1 导致服务变慢**: 这是遇到了大 Key 问题。立即使用 SSCANZSCAN 进行替代,并考虑拆分大集合。
  • BLPOP 一直阻塞**: 检查超时参数设置,并确保有生产者往列表里推送消息。
  • Sorted Set 分数相同时的排序​: 当多个成员分数相同时,它们将按**字典序**排列。
  • ZADDXXNX 选项**: 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 提供了排序和范围查询。

您可能感兴趣的与本文相关的镜像

Python3.8

Python3.8

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值