Redis 完全手册:从入门到精通

内容纲要

前言

Redis(Remote Dictionary Server)是一个开源的、基于内存的高性能键值存储系统,被广泛用作数据库、缓存、消息代理和流处理引擎 1。本手册旨在为所有 Redis 用户——无论是初学者、经验丰富的开发者还是系统管理员——提供一个全面、易懂的指南,帮助大家更好地理解和使用 Redis。

目录

  1. (#chapter-1-redis-简介) 1.1(#11-redis-的历史与演进) 1.2 核心概念与特性 1.3(#13-redis-的优势) 1.4 典型应用场景
  2. 安装与配置 2.1 在 Linux 上安装 2.2(#22-在-macos-上安装) 2.3(#23-在-windows-上安装-通过-wsl2) 2.4(#24-redis-配置文件-redisconf-详解) 2.5 基本配置指令 2.6 启动、停止与连接 (redis-cli)
  3. (#chapter-3-redis-数据结构深度解析) 3.1 键 (Keys) 和值 (Values) 基础 3.2(#32-字符串-strings) 3.3 列表 (Lists) 3.4 哈希 (Hashes) 3.5(#35-集合-sets) 3.6(#36-有序集合-sorted-sets--zsets) 3.7(#37-流-streams) 3.8(#38-位图-bitmaps) 3.9 HyperLogLogs 3.10 地理空间索引 (Geospatial Indexes) 3.11 数据结构命令总结与应用场景
  4. 数据持久化 4.1 持久化概述 4.2(#42-rdb-快照) 4.3 AOF (仅追加文件) 4.4(#44-rdb-与-aof-的选择与混合使用) 4.5 备份与恢复策略
  5. (#chapter-5-事务-transactions) 5.1(#51-multi-exec-discard) 5.2(#52-watch-与乐观锁) 5.3 事务中的错误处理
  6. Lua 脚本 6.1 为什么使用 Lua 脚本 6.2(#62-eval-和-evalsha-命令) 6.3(#63-keys-和-argv) 6.4 脚本的原子性 6.5 脚本缓存与管理 6.6 Lua 脚本示例
  7. (#chapter-7-redis-集群-cluster) 7.1 集群概述与优势 7.2 数据分片与哈希槽 7.3 主从复制与故障转移 7.4(#74-搭建-redis-集群) 7.5 集群管理与伸缩 7.6 客户端如何与集群交互
  8. (#chapter-8-redis-sentinel-哨兵) 8.1(#81-sentinel-概述与高可用) 8.2(#82-sentinel-架构) 8.3 自动故障转移过程 8.4(#84-sentinel-配置与部署) 8.5(#85-客户端连接-sentinel)
  9. 性能调优 9.1 关键性能指标监控 9.2 内存优化策略 9.3 延迟监控与慢查询分析 9.4 网络与连接优化 9.5 常见性能瓶颈与解决方案
  10. 安全性 10.1 网络安全配置 10.2 认证机制 10.3 命令管理与重命名 10.4(#104-数据加密-tlsssl) 10.5 访问控制列表 (ACLs) 10.6 安全最佳实践清单
  11. 高级主题概览 11.1(#111-redis-模块-modules) 11.2(#112-发布订阅-pubsub)
  12. 客户端库 12.1 选择合适的客户端 12.2 常用编程语言客户端介绍
  13. 常见问题与解答 (FAQ)
  14. 总结与展望

Chapter 1: Redis 简介

Redis 是一个以其卓越性能和多功能性而闻名的开源内存数据结构存储系统 1。它可以用作数据库、缓存、消息代理和流处理引擎,能够每秒处理数百万次操作,并被全球组织信任用于需要实时数据处理和超低延迟响应的应用 1。

1.1 Redis 的历史与演进

Redis 的诞生源于其创造者 Salvatore Sanfilippo(更为人所知的网名是 antirez)在解决实际性能问题时的需求。2009年,为了提升其初创公司 LLOOGG(一个实时网络日志分析器)的性能,Sanfilippo 开始开发 Redis 1。最初,这只是一个为处理高频数据操作而设计的简单解决方案,但它迅速发展成为世界上最受欢迎的内存数据存储之一 1。

以下是 Redis 发展历程中的一些关键里程碑:

  • 2009年: Salvatore Sanfilippo 开始初步开发 Redis 1。同年4月,Redis 第一个版本发布,因其速度、简单性和多功能性迅速受到开发者欢迎 2。
  • 2010年: Redis 1.0 发布,包含了基本的数据结构和持久化功能 1。同年,Redis 2.0 发布,引入了诸如有序集合和哈希等特性,进一步扩展了其能力 2。
  • 2011年: Sanfilippo 离开工作岗位,全职投入 Redis 的开发 2。
  • 2012年: Redis 2.6 发布,引入了 Lua 脚本支持和改进的过期处理机制 1。
  • 2013年: Redis Sentinel 问世,用于高可用性监控和故障转移 1。
  • 2015年: Redis 3.0 发布,引入了模块(Modules)的概念,允许开发者通过自定义命令扩展 Redis 的功能 2。
  • 2018年: Redis 5.0 发布,带来了 Streams 数据类型和改进的内存管理等新特性 2。
  • 2020年: Redis Labs(Redis 背后的公司)融资1亿美元,巩固了其在企业技术行业的领导地位 2。
  • 2024年: Redis Stack 集成了搜索、JSON、时间序列和图形功能 1。

如今,Redis 由 Redis Ltd. 开发,并得到了全球社区的贡献,在保持其简单性和高性能核心原则的同时,不断发展以满足现代应用的需求 1。

1.2 核心概念与特性

Redis 之所以广受欢迎,得益于其独特的核心概念和强大的特性。

  • 内存存储 (In-memory storage): Redis 的所有数据都存储在 RAM(随机存取存储器)中,这消除了传统数据库中常见的磁盘 I/O 瓶颈。因此,大多数操作的响应时间都能达到亚毫秒级别,使其成为需要即时数据访问的实时应用的理想选择 1。这种设计优先考虑速度,同时提供可选的持久化机制,与传统数据库优先考虑数据安全性的做法形成对比 1。
  • 丰富的数据结构 (Rich Data Structures): Redis 不仅仅是一个简单的键值存储。它提供了一套丰富的数据结构,包括字符串(Strings)、列表(Lists)、集合(Sets)、有序集合(Sorted Sets)、哈希(Hashes)、位图(Bitmaps)、HyperLogLogs、地理空间索引(Geospatial Indexes)和流(Streams)1。这些数据结构针对不同的访问模式进行了优化,使得开发者能够更高效地处理各种复杂的数据模型 1。
  • 单线程事件循环 (Single-threaded event loop): Redis 主进程采用单线程处理所有客户端请求,利用高效的事件驱动架构。这种设计避免了多线程带来的复杂性和开销,同时确保了操作的原子性和数据状态的一致性 1。
  • 内存管理策略 (Memory Management Strategies): Redis 采用复杂的内存管理策略以最大化性能并提供可预测的行为 1。
    • 内存分配: 根据平台使用 jemalloc 或 libc malloc,并采用智能内存管理以最小化碎片 1。
    • 数据编码优化: Redis 会根据内容和大小自动选择最优的数据表示方式,例如对小列表使用压缩编码,高效存储整数,以及动态调整哈希表大小以平衡内存使用和访问速度 1。
    • 内存淘汰策略 (Eviction Policies): 当达到内存限制时,Redis 可以根据可配置的策略自动删除数据,如 LRU(最近最少使用)、LFU(最不经常使用)、基于 TTL 的淘汰或随机淘汰,从而实现精细的缓存行为控制 1。
    • 内存碎片整理: 活动内存碎片整理功能在后台运行,以减少内存碎片,这对于具有动态数据模式的长期运行实例尤其重要 1。
  • 持久化 (Persistence): 虽然 Redis 主要在内存中操作以追求极致速度,但它也提供了两种持久化机制,以确保数据在服务器重启或故障后不会丢失:
    • RDB (Redis Database): 在指定的时间间隔内生成数据集的时间点快照 4。
    • AOF (Append Only File): 记录服务器接收到的每个写操作。这些操作可以在服务器启动时重新执行以重建原始数据集 4。 用户可以选择单独使用 RDB 或 AOF,也可以同时使用两者以获得更高的数据安全性 4。
  • 高可用性与可扩展性 (High Availability and Scalability):
    • Redis Sentinel: 提供高可用性,包括监控、通知和自动故障转移 1。
    • Redis Cluster: 实现数据的自动分片,允许水平扩展 Redis部署,将数据分布到多个节点上 2。
  • Lua 脚本 (Lua Scripting): 允许用户在 Redis 服务器端执行复杂操作,减少网络延迟并确保操作的原子性 1。
  • 发布/订阅 (Pub/Sub): 内置的发布/订阅消息传递功能,可用于构建实时消息系统 2。

这些核心特性共同构成了 Redis 强大功能的基础,使其能够灵活适应各种应用需求,从简单的缓存到复杂的数据处理和实时分析。

1.3 Redis 的优势

Redis 相较于其他数据库和缓存系统,尤其是在与 Memcached 这类内存缓存系统对比时,展现出诸多优势。

  • 更丰富的数据结构: 这是 Redis 最显著的优势之一。Memcached 主要支持简单的键值字符串存储,而 Redis 支持字符串、列表、集合、有序集合、哈希等多种复杂数据结构 3。这些数据结构为解决复杂问题提供了更直接和高效的方案,例如使用有序集合实现排行榜,使用列表实现队列等。
  • 持久化能力: Redis 提供了 RDB 和 AOF 两种持久化机制,可以将内存中的数据保存到磁盘,确保在服务器重启或故障时数据的持久性 3。Memcached 本身不提供数据持久化功能,数据在服务停止后会丢失 6。这一差异使得 Redis 不仅可以作为缓存,还可以作为轻量级的主数据库使用。
  • 更高的内存使用效率: 尽管两者都是内存存储,但在某些情况下 Redis 的内存使用效率更高。例如,当存储大量小对象时,Redis 的哈希结构由于内部编码优化(如 ziplist),可能比 Memcached 存储序列化后的相同数据更节省内存。此外,Redis 能够回收不再使用的内存,而 Memcached 一旦分配了内存块,即使数据被删除或过期,通常也不会立即将其返还给操作系统,直到重启服务 6。
  • 原子操作: Redis 的许多操作都是原子性的,包括对复杂数据结构的操作(如 INCRLPUSH 等)。这意味着这些操作要么完全执行,要么完全不执行,保证了并发环境下数据的一致性。
  • 内置复制、集群和高可用方案: Redis 原生支持主从复制,可以通过 Redis Sentinel 实现高可用性监控和自动故障转移,通过 Redis Cluster 实现数据的自动分片和水平扩展 2。Memcached 本身不提供这些高级功能,需要依赖客户端库或第三方工具来实现分布式和高可用。
  • Lua 脚本支持: Redis 允许在服务器端执行 Lua 脚本,可以将多个命令打包成一个原子操作,减少网络往返次数,并实现更复杂的服务器端逻辑 3。Memcached 不具备此功能。
  • 事务支持: Redis 支持事务,可以将一组命令打包执行,保证这些命令的原子性(要么都执行,要么都不执行)3。
  • 更广泛的生态系统和社区支持: Redis 拥有一个庞大且活跃的社区,提供了丰富的客户端库、工具和文档资源 6。其流行度和应用范围也通常被认为超过 Memcached 6。

虽然 Memcached 在某些纯粹的、高吞吐量的字符串缓存场景下可能因其多线程架构(相对于 Redis 的单主线程模型)而表现出一定的性能优势,但 Redis 凭借其更丰富的功能集、数据持久化能力和灵活性,在更广泛的应用场景中成为更受欢迎的选择 3。

1.4 典型应用场景

Redis 因其高性能和多功能性,在众多领域都有广泛的应用。以下是一些典型的应用场景:

  • 高速缓存 (Caching): 这是 Redis 最常见的用途之一。通过将频繁访问的数据(如数据库查询结果、网页片段、对象等)存储在 Redis 中,可以显著减少对后端数据库的访问压力,加快应用响应速度 7。例如,在电子商务平台中,商品信息、用户会话等可以被缓存以提升用户体验 7。
  • 会话管理 (Session Management): Redis 可以高效地存储和管理用户会话信息。在分布式 Web 应用中,使用 Redis 作为会话存储可以确保用户在不同服务器之间的会话一致性和无缝体验 7。
  • 排行榜 (Leaderboards): Redis 的有序集合 (Sorted Set) 数据结构非常适合实现实时排行榜功能。例如,在游戏中,玩家的分数可以存储在有序集合中,Redis 会自动根据分数进行排序,方便快速查询和更新排名 7。
  • 实时分析与数据摄取 (Real-Time Analytics & Data Ingestion): Redis 能够以极高的速度处理数据摄取,支持每秒数百万次操作,并提供亚毫秒级的延迟,使其成为需要实时分析的应用的理想选择 7。例如,事件流平台可以使用 Redis 快速摄取、转换和传递大量数据进行实时分析。
  • 消息队列/任务队列 (Message Queues/Task Queues): Redis 的列表 (List) 数据结构可以作为简单的消息队列使用,支持先进先出 (FIFO) 的消息传递。通过 LPUSHBRPOP (阻塞式弹出) 等命令,可以实现生产者-消费者模式的任务分发和处理 7。Redis Streams 提供了更高级的消息队列功能,支持消费组等特性。
  • 发布/订阅 (Publish/Subscribe): Redis 内置了发布/订阅机制,可以用于构建实时的消息通知系统、聊天应用等 3。客户端可以订阅一个或多个频道,当有消息发布到这些频道时,所有订阅者都会收到通知。
  • 计数器与限流 (Counters & Rate Limiting): Redis 的原子增减操作 (如 INCR, DECR) 非常适合实现各种计数器,例如网站访问量、点赞数等。结合过期时间,也可以轻松实现API接口的速率限制功能。
  • 机器学习模型推理 (Machine Learning Inference): 机器学习模型进行实时推理时需要快速访问数据。Redis 提供的高吞吐量和低延迟特性,使其适合缓存预处理数据或模型权重,帮助 AI 应用快速做出决策 7。
  • 地理空间数据处理 (Geospatial Data Processing): Redis 支持地理空间索引,可以存储地理位置信息(经纬度),并执行基于位置的查询,如查找附近的点、计算两点间距离等。

这些只是 Redis 应用场景的一部分,其灵活性和性能使其能够适应更多创新性的用途。


Chapter 2: 安装与配置

本章节将指导您如何在不同的操作系统上安装 Redis,并介绍其核心配置文件 redis.conf 以及如何启动、停止和连接到 Redis 服务。

2.1 在 Linux 上安装

在 Linux 系统上安装 Redis 有多种方式,包括从源代码编译安装和使用包管理器安装。官方推荐在生产环境中使用通过初始化脚本进行的规范安装 9。

2.1.1 通过包管理器安装 (APT - Ubuntu/Debian)

对于 Ubuntu/Debian 系统,可以通过 apt 包管理器进行安装。为了获取最新的 Redis 版本,建议添加 Redis 官方的 packages.redis.io 仓库。

  1. 添加 Redis 仓库并安装:

    首先,安装必要的依赖,然后添加 GPG 密钥和 Redis APT 仓库:

    Bash

    sudo apt update
    sudo apt install -y lsb-release curl gpg
    curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
    sudo chmod 644 /usr/share/keyrings/redis-archive-keyring.gpg
    echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list

    更新包列表并安装 Redis:

    Bash

    sudo apt update
    sudo apt install -y redis-server

    10

  2. 启动和验证 Redis 服务:

    安装完成后,Redis 服务通常会自动启动。您可以使用以下命令检查服务状态:

    Bash

    sudo systemctl status redis-server

    如果服务未运行,可以手动启动:

    Bash

    sudo systemctl start redis-server

    为了确保 Redis 在系统启动时自动运行,可以启用该服务:

    Bash

    sudo systemctl enable redis-server

    10

  3. 连接测试:

    使用 redis-cli 连接到 Redis 服务器并发送 PING 命令:

    Bash

    redis-cli
    127.0.0.1:6379> PING
    PONG

    如果收到 PONG 响应,说明 Redis 服务器已成功安装并正在运行 10。

2.1.2 从源代码编译安装

如果您希望使用特定版本或进行更细致的控制,可以选择从源代码编译安装。

  1. 下载并编译源代码:

    从 Redis 官方网站下载最新的稳定版源码包,解压并编译:

    Bash

    wget https://download.redis.io/redis-stable.tar.gz
    tar -xzf redis-stable.tar.gz
    cd redis-stable
    make

    编译完成后,建议运行测试:

    Bash

    make test

    如果所有测试通过,可以将编译好的二进制文件安装到系统中,默认会安装到 /usr/local/bin

    Bash

    sudo make install

    9

  2. 配置为系统服务 (使用 init 脚本):

    为了使 Redis 能够在后台运行并在系统重启后自动启动,官方推荐使用提供的 init 脚本进行规范安装。

    • 创建所需目录

      :

      Bash

      sudo mkdir /etc/redis
      sudo mkdir /var/redis

      9

    • 复制 init 脚本

      : 将 Redis 源码包中

      utils

      目录下的

      redis_init_script

      复制到

      /etc/init.d/

      ,并建议以端口号命名(例如

      redis_6379

      ):

      Bash

      sudo cp utils/redis_init_script /etc/init.d/redis_6379
      sudo chmod 0755 /etc/init.d/redis_6379

      9

    • 编辑 init 脚本: 打开 /etc/init.d/redis_6379 并根据需要设置 REDISPORT 变量。脚本中的 PID 文件路径和配置文件名将依赖此端口号 9。

    • 复制配置文件

      : 将源码包根目录下的

      redis.conf

      模板文件复制到

      /etc/redis/

      ,并以端口号命名(例如

      6379.conf

      ):

      Bash

      sudo cp redis.conf /etc/redis/6379.conf

      9

    • 创建数据和工作目录

      :

      Bash

      sudo mkdir /var/redis/6379

      9

    • 编辑配置文件 (/etc/redis/6379.conf)

      : 进行以下关键更改

      9

      • daemonize yes (默认为 no)
      • pidfile /var/run/redis_6379.pid
      • port 6379 (如果使用默认端口则无需更改)
      • 设置 loglevel (例如 notice)
      • logfile /var/log/redis_6379.log
      • dir /var/redis/6379 (非常重要,指定数据存储目录)
    • 添加服务到启动项

      :

      Bash

      sudo update-rc.d redis_6379 defaults

      9

    • 启动服务并验证

      :

      Bash

      sudo /etc/init.d/redis_6379 start
      redis-cli PING
      # 应返回 PONG
      redis-cli SAVE
      # 检查 /var/redis/6379/dump.rdb 是否生成
      # 检查 /var/log/redis_6379.log 日志文件

      9

2.1.3 安全配置初步

安装完成后,应进行基本的安全配置:

  • 绑定 IP 地址

    : 编辑

    redis.conf

    文件,找到

    bind

    指令。默认情况下,它可能只绑定到

    127.0.0.1

    (localhost)。如果需要从其他服务器访问 Redis,可以将其设置为特定 IP 地址或

    0.0.0.0

    (监听所有网络接口)。但请注意,绑定到

    0.0.0.0

    会使 Redis 暴露在所有网络接口上,务必结合防火墙和密码认证使用

    10

    # /etc/redis/redis.conf
    bind 127.0.0.1 ::1 # 仅本地访问
    # 或者
    # bind your_server_ip
  • 设置密码认证

    : 在

    redis.conf

    中找到

    requirepass

    指令,取消注释并设置一个强密码

    10

    # /etc/redis/redis.conf
    requirepass YourStrongPassword

    修改配置后,需要重启 Redis 服务使更改生效:

    Bash

    sudo systemctl restart redis-server # (对于 APT/systemd 安装)
    # 或 sudo /etc/init.d/redis_6379 restart (对于 init 脚本安装)

    连接时需要使用

    AUTH

    命令进行认证:

    Bash

    redis-cli
    127.0.0.1:6379> AUTH YourStrongPassword
    OK

    10

  • 重命名或禁用危险命令

    : 为了安全起见,可以重命名或禁用一些危险的命令,如

    FLUSHALL

    ,

    FLUSHDB

    ,

    CONFIG

    ,

    KEYS

    等。在

    redis.conf

    中添加:

    # /etc/redis/redis.conf
    rename-command FLUSHALL ""  # 禁用 FLUSHALL
    rename-command CONFIG ""    # 禁用 CONFIG
    rename-command KEYS "SOME_RANDOM_STRING_FOR_KEYS" # 重命名 KEYS

    10

2.2 在 macOS 上安装

在 macOS 上安装 Redis 最常用的方法是使用 Homebrew 包管理器。

  1. 安装 Xcode 命令行工具 (如果尚未安装):

    打开终端,运行:

    Bash

    xcode-select --install

    15

  2. 安装 Homebrew (如果尚未安装):

    在终端中运行以下命令从 Homebrew 官方 Git 仓库下载并安装:

    Bash

    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

    按照终端提示完成安装 15。

  3. 安装 Redis:

    使用 Homebrew 安装 Redis:

    Bash

    brew install redis

    15

    另一种方式是使用 Redis 官方的 Homebrew Cask 16:

    Bash

    brew tap redis/redis
    brew install --cask redis

    这种方式安装的 Redis 可能不会与 brew services 命令集成 16。

  4. 启动 Redis 服务:

    如果通过 brew install redis 安装,可以使用 brew services 启动 Redis 并在登录时自动启动:

    Bash

    brew services start redis

    15

    如果通过 brew install --cask redis 安装,需要手动启动,并确保 Redis 的安装路径(Apple Silicon Mac 通常是 /opt/homebrew/bin,Intel Mac 通常是 /usr/local/bin)在 PATH 环境变量中 16。

    Bash

    # 检查 PATH
    echo $PATH
    # 如果需要,添加到 ~/.zshrc 或 ~/.bashrc
    export PATH=$(brew --prefix)/bin:$PATH
    # 手动启动 (路径可能因 Homebrew 版本和 Mac 架构而异)
    redis-server /opt/homebrew/etc/redis.conf # Apple Silicon
    # 或
    # redis-server /usr/local/etc/redis.conf # Intel

    16

  5. 测试 Redis 连接:

    打开新的终端窗口,运行 redis-cli:

    Bash

    redis-cli

    连接成功后,提示符会变为类似 127.0.0.1:6379>。发送 PING 命令:

    127.0.0.1:6379> PING
    PONG

    收到 PONG 表示 Redis 正在运行 15。

  6. 停止 Redis 服务:

    如果使用 brew services 启动:

    Bash

    brew services stop redis

    如果手动启动,可以使用 redis-cli 关闭:

    Bash

    redis-cli SHUTDOWN

    16

  7. 配置 Redis (可选):

    Redis 的配置文件通常位于 /opt/homebrew/etc/redis.conf (Apple Silicon) 或 /usr/local/etc/redis.conf (Intel)。您可以编辑此文件进行自定义配置,例如设置密码、更改端口等。修改后需要重启 Redis 服务。

    Bash

    # 例如,限制最大内存为 2GB
    # maxmemory 2000000000
    brew services restart redis # 如果使用 brew services

    15

2.3 在 Windows 上安装 (通过 WSL2)

Redis 官方不直接支持 Windows,但可以通过适用于 Linux 的 Windows 子系统 (WSL2) 在 Windows 上运行 Redis 15。这提供了一个原生的 Linux 环境来运行 Redis。

  1. 启用 WSL2:

    • 确保您的 Windows 10 (版本 2004, Build 19041 或更高) 或 Windows 11 已更新 18。

    • 以管理员身份打开 PowerShell,运行以下命令启用 WSL 和虚拟机平台功能:

      PowerShell

      dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
      dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
    • 重启计算机。

    • 下载并安装 Linux 内核更新包(通常 WSL2 会提示或自动处理)。

    • 在 PowerShell 中设置 WSL2 为默认版本:

      PowerShell

      wsl --set-default-version 2

    17

  2. 从 Microsoft Store 安装 Linux 发行版 (如 Ubuntu):

    • 打开 Microsoft Store。
    • 搜索 "Ubuntu" (推荐最新 LTS 版本,如 Ubuntu 22.04)。
    • 下载并安装 17。
    • 首次启动 Ubuntu 时,会提示您创建 Linux 用户名和密码 15。
  3. 在 Ubuntu (WSL2) 中安装 Redis:

    • 打开已安装的 Ubuntu 发行版终端。

    • 更新包列表并安装 Redis 服务器:

      Bash

      sudo apt update
      sudo apt upgrade -y
      sudo apt install -y redis-server

      17

    • 验证安装和版本:

      Bash

      redis-server --version

      18

  4. 启动 Redis 服务:

    在 Ubuntu 终端中启动 Redis 服务:

    Bash

    sudo service redis-server start

    15

    或者,如果系统使用 systemd (WSL2 中可能需要额外配置以完全支持 systemd,但 service 命令通常可用):

    Bash

    sudo systemctl start redis-server
  5. 测试 Redis 连接:

    使用 redis-cli 进行测试:

    Bash

    redis-cli
    127.0.0.1:6379> PING
    PONG

    15

    您也可以尝试设置和获取一个键值对:

    Bash

    127.0.0.1:6379> SET mykey "Hello from WSL2"
    OK
    127.0.0.1:6379> GET mykey
    "Hello from WSL2"

    17

  6. 停止 Redis 服务:

    Bash

    sudo service redis-server stop

    17

  7. 配置 Redis (可选):

    Redis 的配置文件位于 WSL2 中 Ubuntu 文件系统的 /etc/redis/redis.conf。您可以使用 sudo nano /etc/redis/redis.conf 或其他编辑器进行修改。修改后,使用 sudo service redis-server restart 重启服务 10。

重要提示:

  • 在 WSL2 中运行的 Redis 服务,其端口 (默认为 6379) 通常可以直接从 Windows 主机通过 localhost:6379 访问。
  • 如果遇到 systemctl 命令不工作的情况,可能是因为 WSL 发行版默认未使用 systemd。service 命令通常可以作为替代。在某些较新版本的 WSL2 中,可以配置启用 systemd 支持 18。

2.4 Redis 配置文件 (redis.conf) 详解

Redis 的行为主要通过其配置文件 redis.conf 进行控制。虽然 Redis 可以在没有配置文件的情况下使用内置的默认配置启动,但这仅推荐用于测试和开发目的 19。对于生产环境或任何需要自定义行为的场景,都应该通过提供一个 redis.conf 文件来正确配置 Redis 19。

  • 文件位置:

    • 从源代码编译安装并遵循 init 脚本设置时,配置文件通常位于 /etc/redis/<port>.conf (例如 /etc/redis/6379.conf) 9。
    • 通过包管理器 (如 APT) 安装时,通常位于 /etc/redis/redis.conf 10。
    • 在 macOS 上通过 Homebrew 安装时,通常位于 /opt/homebrew/etc/redis.conf/usr/local/etc/redis.conf
  • 格式:

    redis.conf 文件包含一系列指令,格式非常简单:keyword argument1 argument2... argumentN 19。

    • 注释以 # 开头。
    • 字符串参数如果包含空格,可以用单引号或双引号括起来 19。
  • 默认配置与自文档化:

    Redis 发行版中通常包含一个示例 redis.conf 文件 (对于 Redis 8 及更高版本,可能是 redis-full.conf),这个文件本身就是一份详尽的文档,其中包含了对每个指令的解释、含义和预期用途 19。强烈建议通读此文件以了解所有可用的配置选项。

  • 加载配置:

    Redis 在启动时加载配置文件。可以通过命令行参数指定配置文件的路径:

    Bash

    redis-server /path/to/your/redis.conf

    13

  • 通过命令行传递参数:

    除了使用配置文件,还可以在启动 Redis 服务器时直接通过命令行传递配置参数。这对于测试特定设置非常有用。命令行参数的格式与配置文件中的指令类似,但关键字前需要加上 -- 19。

    例如,要以端口 6380 启动一个新的 Redis 实例,并将其作为 127.0.0.1:6379 的副本:

./redis-server --port 6380 --replicaof 127.0.0.1 6379

```

19

这种方式会在内存中生成一个临时的配置文件 19。

  • 运行时更改配置

    : 可以使用

    CONFIG SET

    CONFIG GET

    命令在 Redis 服务器运行时动态地修改或查询配置参数,而无需停止和重启服务

    19

    Code snippet

    CONFIG GET loglevel
    CONFIG SET loglevel verbose

    需要注意的是,并非所有配置指令都支持运行时修改。通过

    CONFIG SET

    所做的更改仅对当前运行的实例有效,不会自动保存到

    redis.conf

    文件中。如果希望将这些运行时更改持久化,以便在下次重启后依然生效,需要手动修改

    redis.conf

    文件,或者使用

    CONFIG REWRITE

    命令

    19

    CONFIG REWRITE

    会自动扫描

    redis.conf

    文件,并更新与当前配置值不匹配的字段,同时保留文件中的注释

    19

redis.conf 文件是 Redis 定制化的核心。其详尽的注释是一个刻意的设计选择,旨在降低用户理解和有效管理 Redis 配置的门槛,即使对于复杂的设置也是如此 19。这种内嵌于配置文件的教学方法是 Redis 的一个重要可用性特性。

同时,运行时配置的灵活性(通过 CONFIG SET)虽然为生产环境带来了便利,但也引入了潜在的风险:如果未及时使用 CONFIG REWRITE 将运行时更改持久化到 redis.conf,服务器重启后这些更改将会丢失,可能导致非预期的行为 19。因此,建立一套规范的配置管理流程至关重要,确保运行中配置与持久化配置的一致性。

(概念图: redis.conf 结构概览)

Code snippet

graph TD
    A[redis.conf] --> B(网络设置 Network);
    A --> C(通用设置 General);
    A --> D(快照 Snapshotting);
    A --> E(复制 Replication);
    A --> F(安全 Security);
    A --> G(客户端 Clients);
    A --> H(内存管理 Memory Management);
    A --> I(仅追加模式 Append Only Mode);
    A --> J(Lua脚本 Lua Scripting);
    A --> K(集群 Cluster);
    A --> L(慢查询日志 Slow Log);
    A --> M(延迟监控 Latency Monitor);
    A --> N(事件通知 Event Notification);
    A --> O(高级配置 Advanced Config);

    B --> B1(bind);
    B --> B2(protected-mode);
    B --> B3(port);
    B --> B4(tcp-backlog);
    B --> B5(timeout);
    B --> B6(tcp-keepalive);

    C --> C1(daemonize);
    C --> C2(pidfile);
    C --> C3(loglevel);
    C --> C4(logfile);
    C --> C5(databases);

    F --> F1(requirepass);
    F --> F2(rename-command);
    F --> F3(ACLs);

    H --> H1(maxmemory);
    H --> H2(maxmemory-policy);
    H --> H3(maxmemory-samples);

2.5 基本配置指令

以下是一些 redis.conf 中最常用和最重要的配置指令,理解它们对于正确配置和运行 Redis 至关重要。

  • 网络相关 (Networking):

    • bind 127.0.0.1 ::1
    • 作用: 指定 Redis 监听的网络接口。默认情况下,Redis 只接受来自本地回环地址 (localhost) 的连接。

    • 常见值

      :

      • 127.0.0.1 ::1: 仅接受 IPv4 和 IPv6 的本地连接 (推荐用于单机部署或通过 SSH 隧道访问)。
      • 一个或多个特定 IP 地址 (如 bind 192.168.1.100 10.0.0.5): 仅接受来自这些指定 IP 地址的连接。
      • 0.0.0.0: 监听所有可用的网络接口 (IPv4)。
      • ::: 监听所有可用的网络接口 (IPv6,如果系统支持)。
    • 注意: 如果绑定到 0.0.0.0 或其他非本地地址,务必启用密码认证 (requirepass) 并配置防火墙规则,以防未经授权的访问 9。

    • protected-mode yes
    • 作用: 一种安全增强功能。当 Redis 没有设置密码 (requirepass) 且 bind 指令未明确指定监听的 IP 地址 (即隐式监听所有接口,或显式绑定到 0.0.0.0 等) 时,保护模式会阻止来自外部网络的连接,只允许本地连接。

    • 常见值: yes (默认), no

    • 注意: 如果您确实需要从外部网络访问 Redis,并且已经设置了强密码 (requirepass),或者使用了 ACLs,可以将此项设为 no。否则,保持 yes 以增加安全性 22。

    • port 6379
    • 作用: Redis 服务器监听的 TCP 端口号。

    • 常见值: 6379 (默认)。可以更改为其他未被占用的端口,例如出于安全考虑或在同一台机器上运行多个 Redis 实例时。

    • 注意: 如果更改了端口,客户端连接时也需要指定新的端口号 9。

  • 通用设置 (General):

    • daemonize no
    • 作用: 当设置为 yes 时,Redis 会作为后台守护进程运行。设置为 no (默认从源码安装时) 时,Redis 在前台运行,日志直接输出到标准输出。

    • 常见值: no, yes

    • 注意: 包管理器安装的 Redis 通常默认设置为 yes。如果设置为 yes,通常需要配置 pidfilelogfile 9。

    • pidfile /var/run/redis_6379.pid
    • 作用: 当 daemonize 设置为 yes 时,Redis 会将进程 ID (PID) 写入此文件。

    • 注意: 确保 Redis 用户对此路径有写入权限。文件名通常包含端口号以区分多个实例 9。

    • logfile ""
    • 作用: 指定日志文件的路径。如果设置为空字符串 (默认),且 daemonizeno,日志将输出到标准输出。如果 daemonizeyes,且 logfile 为空,日志将被发送到 /dev/null

    • 常见值: 例如 /var/log/redis/redis-server.log/var/log/redis_6379.log

    • 注意: 确保 Redis 用户对日志文件及其所在目录有写入权限 9。

    • loglevel notice
    • 作用: 设置日志记录的详细程度。

    • 常见值

      :

      • debug: 记录大量信息,适用于开发或调试。
      • verbose: 包含许多不常用的信息,但比 debug 少。
      • notice: 适度的详细程度,生产环境常用 (默认)。
      • warning: 只记录非常重要或关键的警告/错误信息。
    • 9

    • databases 16
    • 作用: 设置 Redis 实例可用的数据库数量。默认有 16 个数据库,编号从 0 到 15。客户端可以通过 SELECT <dbid> 命令切换数据库。

    • 注意: Redis Cluster 不支持多个数据库 (只使用数据库 0)。如果计划使用集群,应避免在应用中使用多数据库特性。

  • 持久化相关 (Persistence):

    • save  
    • 作用: 配置 RDB 快照持久化策略。当在指定秒数内发生指定次数的写操作时,Redis 会自动保存数据集到磁盘。可以设置多条 save 规则。

    • 示例

      :

      save 900 1   # 900秒 (15分钟) 内至少有1个key改变则保存
      save 300 10  # 300秒 (5分钟)  内至少有10个key改变则保存
      save 60 10000 # 60秒 (1分钟)  内至少有10000个key改变则保存
    • 注意: 设置为空字符串 save "" 可以禁用自动 RDB 快照 13。

    • rdbcompression yes
    • 作用: 是否对 RDB 快照文件进行 LZF 压缩。

    • 常见值: yes (默认), no

    • 注意: 压缩可以减小 RDB 文件大小,但会消耗一些 CPU 资源。

    • dbfilename dump.rdb
    • 作用: RDB 快照文件的名称。

    • dir./
    • 作用: RDB 快照文件和 AOF 持久化文件的工作目录。

    • 常见值: 例如 /var/lib/redis/6379/var/redis/6379

    • 注意: 确保 Redis 用户对此目录有读写权限。默认的 ./ 表示 Redis 启动时所在的目录,这在生产环境中通常不合适 9。

    • appendonly no
    • 作用: 是否启用 AOF (Append Only File) 持久化。

    • 常见值: no (默认), yes

    • 注意: AOF 提供了比 RDB 更高的数据持久性保证 20。

    • appendfilename "appendonly.aof"
    • 作用: AOF 文件的名称 (当 appendonlyyes 时)。

    • appendfsync everysec
    • 作用: AOF 文件同步到磁盘的策略。

    • 常见值

      :

      • no: 从不主动 fsync,由操作系统决定何时同步。速度最快,但数据最不安全。
      • everysec: 每秒 fsync 一次 (默认)。性能和持久性的良好折中,最多可能丢失1秒的数据。
      • always: 每个写命令都 fsync。最安全,但性能最低。
    • [41 (概念提及)]

  • 安全相关 (Security):

    • requirepass YourStrongPassword
    • 作用: 设置客户端连接 Redis 时需要提供的密码。

    • 注意: 强烈建议在生产环境中设置一个强密码。客户端连接后需要使用 AUTH <password> 命令进行认证 10。

    • rename-command CONFIG ""
    • 作用: 重命名或禁用指定的 Redis 命令。将命令重命名为空字符串 "" 相当于禁用该命令。

    • 示例: rename-command FLUSHALL "" (禁用 FLUSHALL 命令以防误删数据)。

    • 注意: 这是增强安全性的一个重要手段,可以限制对高危命令的访问 10。

  • 限制相关 (Limits):

    • maxclients 10000
    • 作用: 设置 Redis 允许的最大并发客户端连接数。

    • 注意: 操作系统也对可打开的文件描述符数量有限制,可能需要调整系统配置 (ulimit -n) 以支持更高的 maxclients 值 20。

    • maxmemory 
    • 作用: 设置 Redis 可使用的最大内存量 (以字节为单位)。当达到此限制时,Redis 会根据 maxmemory-policy 执行内存淘汰。

    • 示例: maxmemory 2gb (2吉字节)。

    • 注意: 如果不设置此项,Redis 可能会耗尽服务器所有可用内存。这是控制 Redis 内存占用的关键指令 13。

    • maxmemory-policy noeviction
    • 作用: 当 maxmemory 限制达到时,Redis 采取的内存淘汰策略。

    • 常见值

      :

      • noeviction: (默认) 不淘汰任何数据,对于写操作直接返回错误。
      • allkeys-lru: 淘汰最近最少使用的键。
      • volatile-lru: 仅从设置了过期时间的键中淘汰最近最少使用的。
      • allkeys-random: 随机淘汰键。
      • volatile-random: 仅从设置了过期时间的键中随机淘汰。
      • volatile-ttl: 仅从设置了过期时间的键中淘汰剩余生存时间最短的。
      • allkeys-lfu: 淘汰最不经常使用的键 (Redis 4.0+)。
      • volatile-lfu: 仅从设置了过期时间的键中淘汰最不经常使用的 (Redis 4.0+)。
    • 注意: 选择合适的淘汰策略对于缓存应用至关重要 1。

许多默认的 Redis 设置,如 bind 127.0.0.1 和在未设置密码时的 protected-mode yes,是为开发和本地测试环境优化的安全默认值 21。然而,生产部署通常需要仔细审查和修改这些默认值,特别是涉及到网络暴露、认证和数据持久性的配置。例如,当应用服务器需要从不同主机访问 Redis 时,就需要更改 bind 指令,并可能在设置了 requirepass 的情况下禁用 protected-mode 10。同样,默认的持久化策略可能不足以满足关键数据的安全需求。这表明,一个“默认”的 Redis 配置很少能直接用于生产环境,而需要根据具体需求进行显式的调整。

此外,某些配置指令之间存在紧密的相互作用。例如,单独设置 maxmemory 而不配置合适的 maxmemory-policy (如 allkeys-lru),当内存达到上限时,默认的 noeviction 策略会导致 Redis 停止接受写操作,而不是按预期淘汰旧数据 1。类似地,如果将 daemonize 设置为 yes 使 Redis 在后台运行,那么正确配置 pidfilelogfile 就变得至关重要,否则将难以管理进程且丢失重要的操作日志 9。理解这些指令间的依赖关系是正确配置 Redis 的关键。

安全相关指令的演进,例如 protected-mode 的引入和对 rename-command 的强调,反映了随着 Redis 在多样化环境中广泛部署,对其安全性的日益重视 10。这些特性为管理员提供了更多工具来加固 Redis 实例,防止意外误操作或潜在的恶意攻击。

下表总结了一些对初学者而言最重要的 redis.conf 指令:

指令 默认值 (近似) 示例值 目的 关键考虑因素
bind 127.0.0.1 -::1 192.168.1.1000.0.0.0 监听的网络接口 设为 0.0.0.0 需配合强密码和防火墙 13
protected-mode yes no 在无密码且监听所有接口时,保护 Redis 免受外部访问 设为 no 前务必配置密码或 ACL 22
port 6379 6380 TCP 监听端口 更改后客户端需同步更新 13
daemonize no (源码安装) yes 是否作为守护进程运行 设为 yes 时需配置 pidfilelogfile 9
logfile "" (标准输出) /var/log/redis.log 日志文件路径 确保 Redis 用户有写入权限 13
dir ./ /var/lib/redis RDB/AOF 文件存储目录 确保 Redis 用户有读写权限,选择合适的持久化存储位置 9
save (多个默认规则) save 300 10 RDB 快照触发条件 (秒数 写次数) 影响数据丢失风险和性能,可设置多条 13
appendonly no yes 是否启用 AOF 持久化 AOF 通常提供更高的数据安全性 20
requirepass (无密码) YourStrongP@ssw0rd 设置客户端认证密码 生产环境强烈建议设置 10
maxmemory (无限制) 2gb Redis 可使用的最大内存 防止耗尽系统内存,需配合 maxmemory-policy 13
maxmemory-policy noeviction allkeys-lru 内存达到 maxmemory 时的淘汰策略 对缓存行为至关重要 1

2.6 启动、停止与连接 (redis-cli)

管理 Redis 服务进程和通过其命令行界面 redis-cli 进行交互是日常运维和开发的基础操作。

  • 启动 Redis:

    • 通过初始化脚本 (System V init)

    : 如果您是从源代码编译安装并配置了 init 脚本 (如

    redis_init_script

    ),通常使用以下命令启动:

    Bash

    sudo /etc/init.d/redis_6379 start

    这里的

    redis_6379

    是 init 脚本的名称,可能因您的配置而异

    9

    • 通过 systemd

    : 在使用 systemd 作为初始化系统的现代 Linux 发行版上 (例如通过 APT 或 YUM 安装的 Redis),使用

    systemctl

    命令:

    Bash

    sudo systemctl start redis-server  # 或者可能是 redis

    10

    • 通过 Homebrew (macOS)

    : 如果使用 Homebrew 安装并希望其作为服务运行:

    Bash

    brew services start redis

    15

    或者,如果通过

    brew install --cask redis

    安装或希望手动控制,可以直接运行

    redis-server

    并指定配置文件:

    Bash

    redis-server /opt/homebrew/etc/redis.conf # (路径可能不同)

    16

    • 直接运行 redis-server

    : 您可以直接执行

    redis-server

    命令。如果不指定配置文件,它会尝试加载默认的

    redis.conf

    (如果存在于启动目录或预定义位置) 或使用内置的默认配置。

    Bash

    redis-server /path/to/your/redis.conf
    # 或者,如果 redis-server 在 PATH 中且配置文件在默认位置
    # redis-server
  • 停止 Redis:

    • 通过初始化脚本

    :

    Bash

    sudo /etc/init.d/redis_6379 stop
    • 通过 systemd

    :

    Bash

    sudo systemctl stop redis-server # 或 redis

    12

    • 通过 Homebrew

    :

    Bash

    brew services stop redis
    • 通过 redis-cli

    : 可以使用

    redis-cli

    发送

    SHUTDOWN

    命令来优雅地关闭 Redis 服务器。

    Bash

    redis-cli SHUTDOWN

    16

    SHUTDOWN

    命令会先执行一次保存操作 (如果开启了 RDB 或 AOF 持久化且有未保存的更改),然后再关闭。可以使用

    SHUTDOWN SAVE

    强制保存,或

    SHUTDOWN NOSAVE

    强制不保存直接关闭 (可能导致数据丢失)。

  • 连接 Redis (redis-cli):

    redis-cli 是 Redis 的命令行接口工具,用于与 Redis 服务器进行交互。

    • 基本连接

    : 不带参数运行

    redis-cli

    会尝试连接到

    127.0.0.1

    6379

    端口。

    Bash

    redis-cli

    10

    • 指定主机和端口

    : 使用

    -h

    (或

    --host

    ) 参数指定主机名或 IP 地址,使用

    -p

    (或

    --port

    ) 参数指定端口号。

    Bash

    redis-cli -h your_redis_host -p your_redis_port
    • 密码认证

    : 如果 Redis 服务器设置了密码 (通过

    requirepass

    配置),连接时需要进行认证。 可以在连接时通过

    -a

    (或

    --pass

    ) 参数提供密码:

    Bash

    redis-cli -a YourStrongPassword

    或者,在连接成功后,使用

    AUTH

    命令进行认证:

    Code snippet

    127.0.0.1:6379> AUTH YourStrongPassword
    OK

    10

    • 执行基本命令

    : 连接成功后,可以直接输入 Redis 命令。例如:

    Code snippet

    127.0.0.1:6379> PING
    PONG
    127.0.0.1:6379> SET mykey "Hello Redis"
    OK
    127.0.0.1:6379> GET mykey
    "Hello Redis"
    127.0.0.1:6379> DEL mykey
    (integer) 1

    9

    • 退出 redis-cli: 输入 QUITexit,或者按 Ctrl+D

启动和停止 Redis 的具体方法与所选的安装方式紧密相关。这强调了用户在选择安装路径时,需要理解其对后续管理的深远影响。例如,通过源码安装并手动配置的 init 脚本 9 将使用该脚本进行启停,而通过系统包管理器(如 apt)在采用 systemd 的系统上安装的 Redis 10 则依赖 systemctl。macOS 上通过 Homebrew 安装的 Redis 15 通常使用 brew services。这种多样性意味着不存在单一的通用启停命令,因此全面的文档必须覆盖这些常见场景。

redis-cli 不仅仅是一个客户端工具,它更是一个基础的管理和调试工具。它能够发送任何 Redis 命令,包括像 SHUTDOWNCONFIG 这样的管理命令,这使其不可或缺 10。虽然应用程序通常使用特定语言的客户端库与 Redis 交互,但管理员和开发者会频繁使用 redis-cli 进行直接操作,如测试连接 (PING)、手动操作数据 (SET, GET)、检查服务器状态 (INFO) 甚至关闭服务器 (SHUTDOWN) 16。这使得 redis-cli 成为一个功能强大的工具,如果 Redis 实例暴露在不安全网络中,对 redis-cli 的访问也应受到保护。


Chapter 3: Redis 数据结构深度解析

Redis 之所以强大且灵活,很大程度上归功于其支持的多种复杂数据结构。这些数据结构不仅仅是简单的键值对,它们各自拥有独特的特性和操作命令,能够高效地解决各种编程问题。

3.1 键 (Keys) 和值 (Values) 基础

Redis 的核心是一个键值存储系统,其中每个 键 (Key) 都与一个 值 (Value) 相关联 20。

  • 键 (Keys):

    • 键是二进制安全的字符串。这意味着键可以是任何字节序列,例如普通的 ASCII 字符串、JPEG 图片内容,或者序列化的对象。

    • 命名规范

    : 虽然 Redis 对键名没有严格的限制,但采用一致且有意义的命名规范非常重要,这有助于组织数据和提高可读性。常见的模式是使用冒号 (

    :

    ) 作为分隔符来创建命名空间,例如:

    • object-type:id (例如: user:1001, product:_sku123_)

    • object-type:id:field (例如: user:1001:username, order:o987:status)

    • 大小限制: Redis 键的最大长度是 512 MB,但通常不建议使用过长的键名,因为它们会消耗更多内存并且在网络传输和比较时效率较低。简短且描述性强的键名是最佳实践。

  • 值 (Values):

    • 与键不同,Redis 的值可以是多种复杂的数据类型 1。这些数据类型包括字符串 (Strings)、列表 (Lists)、哈希 (Hashes)、集合 (Sets)、有序集合 (Sorted Sets)、流 (Streams)、位图 (Bitmaps)、HyperLogLogs 和地理空间索引 (Geospatial Indexes)。
    • 大小限制: 对于字符串类型的值,最大可以存储 512 MB 的数据 3。其他数据结构(如列表、哈希、集合等)中单个元素的大小也受此限制,而整个数据结构可以包含的元素数量则有其自身的限制 (通常非常大,如 232−1 个元素)。

虽然 Redis 常被简单地称为“键值存储”,但其真正的威力在于“值”可以是一个复杂的数据结构,并且 Redis 提供了针对这些结构内元素的丰富操作命令。这与那些只能将键映射到单一、不透明值(通常是字符串或字节数组)的简单键值存储有着本质区别。Redis 允许直接在服务器端对值的组成部分执行操作(例如,向列表中添加一个元素,从哈希中获取一个字段),这远比获取整个值、在客户端修改、然后再写回服务器的方式更为高效和强大 1。

512MB 的值大小限制 3 对于大多数结构化数据和缓存场景来说是相当充裕的。然而,这也暗示了 Redis 并非设计用来为每个键存储巨大的、单一的数据块。有效的数据建模通常涉及将大型对象分解为更易于管理的 Redis 结构,或者考虑 Redis 是否是存储超大单个项目的合适工具。如果数据项经常接近此限制,可能表明需要重新思考数据模型(例如,使用哈希表来表示对象,或使用列表/流来存储一系列较小的项),或者将大型数据块存储在其他地方(如对象存储服务 S3),仅在 Redis 中缓存元数据或指针。

3.2 字符串 (Strings)

字符串是 Redis 中最基本的数据类型。Redis 的字符串是二进制安全的,这意味着它们可以包含任何类型的数据,例如文本、序列化的 JSON 对象,甚至是 JPEG 图片,最大长度可达 512 MB 24。

  • 描述: 一个字节序列。
  • 用途:
    • 缓存 HTML 片段、API 响应等。
    • 存储简单的计数器 (例如,页面浏览量、用户在线数)。
    • 存储序列化后的对象 (尽管对于结构化对象,哈希通常是更好的选择)。
    • 作为锁机制的基础 (例如,使用 SETNX)。
  • 核心命令:
命令 描述 时间复杂度
SET key value [EX seconds \ | PX milliseconds][NX \ | XX]
GET key 获取键的值。如果键不存在,返回 nil O(1)
APPEND key value 如果键已存在并且是字符串,则将 value 追加到键原有值的末尾。如果键不存在,则创建它并设其值为 value。返回追加后字符串的长度。 O(1)
INCR key 将键的整数值加 1。如果键不存在,则先初始化为 0 再执行加 1。如果值不是整数或超出范围,则返回错误。 O(1)
DECR key 将键的整数值减 1。 O(1)
INCRBY key increment 将键的整数值增加指定的 increment O(1)
DECRBY key decrement 将键的整数值减少指定的 decrement O(1)
MSET key value [key value...] 同时设置一个或多个键值对。 O(N)
MGET key [key...] 获取一个或多个给定键的值。 O(N)
SETNX key value 仅当键不存在时,设置键的值。设置成功返回 1,否则返回 0。 O(1)
GETSET key value 将键的值设为 value,并返回键的原有值。如果键不存在,返回 nil O(1)
STRLEN key 返回键所储存的字符串值的长度。 O(1)
GETRANGE key start end 返回键所储存的字符串值的指定范围内的子串 (包括 startend,均为 0-based 索引)。 O(N)
*数据来源: [24, 25, 26]*
  • CLI 示例:

    Code snippet

    > SET myname "Redis User"
    OK
    > GET myname
    "Redis User"
    > APPEND myname "!"
    (integer) 11
    > GET myname
    "Redis User!"
    > INCR counter
    (integer) 1
    > INCRBY counter 10
    (integer) 11
    > MSET key1 "Hello" key2 "World"
    OK
    > MGET key1 key2
    1) "Hello"
    2) "World"
  • Python (redis-py) 示例:

    Python

    import redis
    
    # 连接到 Redis (假设 Redis 运行在 localhost:6379)
    # decode_responses=True 会将 Redis 返回的字节串自动解码为 Python 字符串
    r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
    
    # SET 和 GET
    r.set("message", "Hello from Python")
    print(f"Message: {r.get('message')}") # 输出: Message: Hello from Python
    
    # 设置带过期时间的键 (EX: 秒)
    r.set("temporary_data", "This will expire", ex=60)
    
    # APPEND
    r.append("message", " and Redis!")
    print(f"Appended message: {r.get('message')}") # 输出: Appended message: Hello from Python and Redis!
    
    # INCR 和 INCRBY
    r.set("page_views", "0") # INCR 需要一个可以被解释为整数的字符串
    r.incr("page_views")
    r.incrby("page_views", 5)
    print(f"Page views: {r.get('page_views')}") # 输出: Page views: 6
    
    # MSET 和 MGET
    r.mset({"user:1:name": "Alice", "user:2:name": "Bob"})
    user_names = r.mget(["user:1:name", "user:2:name", "user:3:name"]) # user:3:name 不存在
    print(f"User names: {user_names}") # 输出: User names:
    
    # SETNX (Set if Not Exists) - 常用于实现锁
    if r.setnx("mylock", "locked_by_worker1"):
      print("Lock acquired")
      # 执行一些需要锁保护的操作
      r.delete("mylock") # 释放锁
    else:
      print("Could not acquire lock")
    
    # GETSET
    old_value = r.getset("message", "New message content")
    print(f"Old message value: {old_value}") # 输出: Old message value: Hello from Python and Redis!
    print(f"New message value: {r.get('message')}") # 输出: New message value: New message content

    26

字符串命令如 INCRDECR 的原子性 24 使 Redis 成为实现分布式计数器(例如页面浏览量、API 调用频率限制)的绝佳选择,无需担心并发环境下的竞争条件。如果多个应用实例尝试读取一个值,对其进行递增,然后写回,竞争条件可能导致计数不准确。Redis 的 INCR 命令在服务器端原子地执行这个读-改-写操作,这种简单性和可靠性是 Redis 广泛用于计数器的原因。

SET 命令的可选参数(如 EXPXNXXX)24 在单个命令中提供了强大的条件逻辑和过期管理功能,减少了网络往返次数并简化了客户端代码。若无这些选项,设置带过期时间的键需要两个命令(SET 后跟 EXPIRE)。仅当键不存在时设置键(用于锁或确保幂等性)则需要先检查键是否存在(EXISTSGET),然后再 SETNXXX 选项允许这些常见模式以原子且高效的方式一次性执行。这体现了 Redis 在为常见分布式计算模式提供高效原语方面的设计重点。

位操作命令如 GETBITSETBITBITCOUNTBITOP 虽然在技术上是对字符串进行操作,但由于其特殊用途,将在后续的(#38-位图-bitmaps) 章节中详细介绍。

3.3 列表 (Lists)

Redis 列表是简单的字符串列表,按照插入顺序排序。您可以从列表的头部(左侧)或尾部(右侧)添加元素。列表在 Redis 内部是作为链表实现的,这意味着即使列表中包含数百万个元素,在头部或尾部添加新元素的操作也是在常数时间内完成的 (O(1)) 8。

  • 描述: 有序的字符串集合,基于链表实现,支持从两端快速插入和删除。
  • 用途:
    • 消息队列/任务队列: 利用 LPUSH 生产任务,BRPOP (阻塞式右弹出) 消费任务,可以实现先进先出 (FIFO) 的队列 8。
    • 栈 (Stack): 利用 LPUSH 推入元素,LPOP 弹出元素,可以实现后进先出 (LIFO) 的栈结构 8。
    • 活动提要/时间线: 例如,社交网络中用户的最新动态列表,可以使用 LPUSH 添加最新活动,并使用 LRANGE 获取最近的 N 条活动 8。
    • 日志记录: 存储最近的日志条目。
  • 核心命令:
命令 描述 时间复杂度
LPUSH key element [element...] 将一个或多个元素插入到列表头部 (左侧)。 O(1) (每个元素)
RPUSH key element [element...] 将一个或多个元素插入到列表尾部 (右侧)。 O(1) (每个元素)
LPOP key [count] 移除并返回列表头部的元素 (count > 1 时为多个元素)。 O(N) (N 为 count)
RPOP key [count] 移除并返回列表尾部的元素 (count > 1 时为多个元素)。 O(N) (N 为 count)
LLEN key 返回列表的长度。 O(1)
LRANGE key start stop 返回列表中指定范围内的元素 (0-based 索引, stop 可以是负数表示从尾部计数, -1 表示最后一个元素)。 O(S+N) (S 为到开始的偏移, N 为范围长度)
LINDEX key index 返回列表中指定索引处的元素。 O(N) (N 为索引值)
LSET key index element 将列表中指定索引的元素设置为新值。 O(N) (N 为索引值)
LREM key count element 根据 count 的值,移除列表中与 element 相等的元素。count > 0: 从头向尾移除;count < 0: 从尾向头移除;count = 0: 移除所有。 O(N) (N 为列表长度)
LTRIM key start stop 对一个列表进行修剪,只保留指定范围内的元素。 O(N) (N 为被移除的元素数量)
RPOPLPUSH source destination 原子地从 source 列表尾部弹出一个元素,并将其推入 destination 列表头部,返回被弹出的元素。常用于可靠消息队列。 O(1)
BRPOPLPUSH source destination timeout RPOPLPUSH 的阻塞版本。如果 source 为空,连接将阻塞直到超时或有元素可用。timeout 为 0 表示无限期阻塞。 O(1)
BLPOP key [key...] timeout LPOP 的阻塞版本。从第一个非空列表中弹出头部元素,或在所有列表都为空时阻塞。timeout 为 0 表示无限期阻塞。 O(1)
BRPOP key [key...] timeout RPOP 的阻塞版本。从第一个非空列表中弹出尾部元素,或在所有列表都为空时阻塞。timeout 为 0 表示无限期阻塞。 O(1)
*数据来源: [8, 27]*
  • CLI 示例:

    Code snippet

    > LPUSH mylist "world"
    (integer) 1
    > LPUSH mylist "hello"
    (integer) 2
    > RPUSH mylist "!"
    (integer) 3
    > LRANGE mylist 0 -1  # 获取所有元素
    1) "hello"
    2) "world"
    3) "!"
    > LPOP mylist
    "hello"
    > RPOP mylist
    "!"
    > LLEN mylist
    (integer) 1
    > LTRIM myactivity 0 99 # 保留最新的100条活动记录
    OK
  • Python (redis-py) 示例 (简单任务队列):

    Python

    import redis
    import time
    
    r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
    TASK_QUEUE_KEY = "task_queue"
    
    # 生产者 (Producer)
    def produce_task(task_data):
      r.lpush(TASK_QUEUE_KEY, task_data)
      print(f"Produced: {task_data}")
    
    # 工人 (Worker)
    def consume_task_blocking():
      print("Worker waiting for tasks...")
      # BRPOP 会阻塞直到有任务可用或超时 (timeout=0 表示无限期阻塞)
      # 返回一个元组 (list_name, task_data) 或 None (如果超时)
      task_info = r.brpop(TASK_QUEUE_KEY, timeout=0)
      if task_info:
          list_name, task_data = task_info
          print(f"Consumed from {list_name}: {task_data}")
          # 模拟任务处理
          time.sleep(1)
          print(f"Finished processing: {task_data}")
      else:
          print("Worker timed out or queue is empty.") # 仅当 timeout > 0 时可能发生
    
    if __name__ == "__main__":
      # 启动一个生产者线程/进程 和一个或多个工人线程/进程 来观察效果
      # 这里为了简单演示,直接调用
    
      # 生产者生产任务
      produce_task("Task data 1: Process payment")
      produce_task("Task data 2: Send email notification")
    
      # 工人消费任务 (在实际应用中,工人通常在独立的进程或线程中循环运行)
      # consume_task_blocking()
      # consume_task_blocking()
      # consume_task_blocking() # 如果没有更多任务,这一行会一直阻塞
    
      # 示例:让工人消费两个任务
      print("\n--- Worker 1 ---")
      consume_task_blocking() # 消费 "Task data 2"
      print("\n--- Worker 2 ---")
      consume_task_blocking() # 消费 "Task data 1"

    27

列表的阻塞操作(如 BLPOPBRPOPBRPOPLPUSH)8 是构建高效、服务器推送型任务队列的基础。它们允许工作进程在没有任务时等待,而不是进行忙轮询,从而减少了 CPU 使用和网络流量。一个简单的队列实现可能让工作者重复轮询列表(使用 LPOP),这种方式效率低下。BLPOP 8 允许工作者发出一个命令,该命令在服务器端阻塞连接,直到列表有可用项或超时。这意味着工作者在等待时消耗的资源极少。BRPOPLPUSH 对于可靠队列更为强大,因为它原子地将一个项从处理队列移动到备份/进行中队列,从而防止在工作者崩溃时丢失任务。

LTRIM 8 是管理列表大小的关键命令,特别适用于像活动提要或日志这样的用例,这些用例中您通常只想保留最近的 N 个项目。这可以防止列表无限增长并消耗过多内存。例如,如果在每次 LPUSH 之后使用 LTRIM mylist 0 99,可以确保列表永远不会包含超过 100 个项目。这提供了一个固定大小的滚动窗口,这是一个常见的需求,并有助于有效地管理内存。

3.4 哈希 (Hashes)

Redis 哈希是一个字符串字段 (field) 和字符串值 (value) 之间的映射集合,非常适合用来表示对象 25。您可以将一个哈希看作是一个微型的 Redis 实例,它存储在一个单独的键下。

  • 描述: 键值对的集合,其中键本身是一个 Redis 键,而值包含多个字段和与字段关联的值。字段和值都是字符串。
  • 用途:
    • 存储对象属性,例如用户配置(用户名、邮箱、积分等)、商品信息(名称、价格、库存等)25。
    • 将相关数据分组到一个键下,便于管理。
  • 内存效率: 当哈希中包含的字段数量较少且字段值较短时,Redis 会使用一种称为 ziplist 的特殊编码方式来存储哈希,这种方式非常节省内存。当字段数量或字段值大小超过一定阈值时,会自动转换为更通用的哈希表实现。
  • 核心命令:
命令 描述 时间复杂度
HSET key field value [field value...] 设置哈希 key 中一个或多个 fieldvalue。如果 field 已存在,则覆盖其值。如果 key 不存在,则创建。 O(1) (每个字段对)
HGET key field 获取哈希 key 中指定 field 的值。 O(1)
HMGET key field [field...] 获取哈希 key 中一个或多个指定 field 的值。 O(N) (N 为字段数量)
HGETALL key 获取哈希 key 中所有的字段和值。返回一个包含字段和值的列表。 O(N) (N 为字段数量)
HDEL key field [field...] 删除哈希 key 中的一个或多个指定 field O(1) (每个字段)
HLEN key 返回哈希 key 中字段的数量。 O(1)
HEXISTS key field 检查哈希 key 中是否存在指定的 field O(1)
HKEYS key 返回哈希 key 中所有的字段名。 O(N) (N 为字段数量)
HVALS key 返回哈希 key 中所有的值。 O(N) (N 为字段数量)
HINCRBY key field increment 将哈希 key 中指定 field 的整数值增加 increment。如果 field 不存在,则先创建并设为 0。 O(1)
HINCRBYFLOAT key field increment 将哈希 key 中指定 field 的浮点数值增加 increment O(1)
HSETNX key field value 仅当哈希 key 中的 field 不存在时,设置其值为 value O(1)
HSCAN key cursor 迭代哈希键中的键值对。 O(1) (每次调用), O(N) (完整迭代)
*注: `HMSET` 命令在 Redis 4.0.0 之后被视为已弃用,推荐使用 `HSET` 同时设置多个字段值。数据来源: [25, 30]*
  • CLI 示例:

    Code snippet

    > HSET user:1000 username "johndoe" email "john.doe@example.com" visits 10
    (integer) 3  # 添加了3个字段
    > HGET user:1000 username
    "johndoe"
    > HMGET user:1000 email visits
    1) "john.doe@example.com"
    2) "10"
    > HGETALL user:1000
    1) "username"
    2) "johndoe"
    3) "email"
    4) "john.doe@example.com"
    5) "visits"
    6) "10"
    > HINCRBY user:1000 visits 1
    (integer) 11
    > HEXISTS user:1000 country
    (integer) 0
  • Python (redis-py) 示例 (存储和检索用户数据):

    Python

    import redis
    
    r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
    
    user_id = "user:1001"
    
    # 使用 HSET (或 HMSET 的现代等效方式) 存储用户数据
    # mapping 参数可以直接传入一个字典
    user_data = {
      "username": "alice_wonder",
      "email": "alice@example.com",
      "city": "Wonderland",
      "logins": 0
    }
    r.hset(user_id, mapping=user_data)
    print(f"User {user_id} data stored.")
    
    # 获取单个字段
    username = r.hget(user_id, "username")
    print(f"Username: {username}") # 输出: Username: alice_wonder
    
    # 获取多个字段
    email_city = r.hmget(user_id, ["email", "city", "non_existent_field"])
    print(f"Email and City: {email_city}") # 输出: Email and City: ['alice@example.com', 'Wonderland', None]
    
    # 获取所有字段和值
    all_data = r.hgetall(user_id)
    print(f"All data for {user_id}: {all_data}")
    # 输出: All data for user:1001: {'username': 'alice_wonder', 'email': 'alice@example.com', 'city': 'Wonderland', 'logins': '0'}
    
    # 原子增加字段值
    new_login_count = r.hincrby(user_id, "logins", 1)
    print(f"New login count for {user_id}: {new_login_count}") # 输出: New login count for user:1001: 1
    updated_logins = r.hget(user_id, "logins")
    print(f"Updated logins from HGET: {updated_logins}") # 输出: Updated logins from HGET: 1
    
    # 检查字段是否存在
    print(f"Does 'city' exist? {'Yes' if r.hexists(user_id, 'city') else 'No'}") # 输出: Does 'city' exist? Yes
    print(f"Does 'country' exist? {'Yes' if r.hexists(user_id, 'country') else 'No'}") # 输出: Does 'country' exist? No
    
    # 删除字段
    r.hdel(user_id, "city")
    print(f"City field deleted. All data now: {r.hgetall(user_id)}")
    # 输出: City field deleted. All data now: {'username': 'alice_wonder', 'email': 'alice@example.com', 'logins': '1'}

    26

与将对象序列化为 JSON 字符串并存储在 Redis 字符串键中相比,哈希通常在存储类对象数据方面更节省内存,特别是对于字段不多或只需要访问少数几个字段的对象。这是因为 Redis 对哈希有内部优化(例如,对小型哈希使用 ziplist 编码)。如果一个对象存储为 JSON 字符串,那么对单个字段的任何更新都需要获取整个字符串,反序列化,修改,再序列化,然后用 SET 命令写回整个字符串。而使用哈希,像 HSETHINCRBY 30 这样的命令允许直接在服务器端原子地更新单个字段。这减少了网络流量和客户端处理,并且由于 Redis 能够以高度优化的方式存储小型哈希,因此可能更节省内存。

在哈希中原子地增加数字字段的能力(HINCRBYHINCRBYFLOAT)30 使得哈希非常适合存储和更新与单个实体相关的多个计数器(例如用户统计数据、游戏得分),而不会产生竞争条件。想象一下跟踪用户的各种统计数据(如个人资料查看次数、发帖数、收到的点赞数)。将这些作为单独的字符串键存储(例如 user:123:viewsuser:123:posts)是可行的,但可能导致键过多。使用哈希(例如键 user:123,字段 viewsposts)可以将这些相关的计数器组织在一起。HINCRBY 30 允许哈希中的每个计数器像字符串的 INCR 一样原子地更新。这为分组计数器提供了更好的数据组织和原子性保证。

3.5 集合 (Sets)

Redis 集合是字符串的无序集合,其中每个成员都是唯一的 25。集合非常适合用于跟踪唯一项或执行常见的集合运算,如交集、并集和差集。

  • 描述: 无序的、不重复的字符串元素集合。
  • 用途:
    • 跟踪唯一项,例如网站的独立访客 IP 地址、文章的标签、用户拥有的权限等 31。
    • 表示关系,例如“喜欢某篇文章的所有用户”、“拥有某个角色的所有用户” 31。
    • 执行集合运算,如查找共同好友 (SINTER)、推荐可能认识的人 (SDIFF) 等。
  • 效率: 添加、删除和检查成员是否存在的时间复杂度通常是 O(1) (与集合中的元素数量无关) 32。
  • 核心命令:
命令 描述 时间复杂度
SADD key member [member...] 向集合 key 中添加一个或多个 member。如果 member 已存在,则忽略。返回成功添加的新成员数量。 O(1) (每个成员)
SREM key member [member...] 从集合 key 中移除一个或多个 member。返回成功移除的成员数量。 O(1) (每个成员)
SISMEMBER key member 判断 member 是否是集合 key 的成员。是则返回 1,否则返回 0。 O(1)
SMEMBERS key 返回集合 key 中的所有成员。注意:对于非常大的集合,此命令可能会阻塞服务器,应谨慎使用或考虑 SSCAN O(N) (N 为集合大小)
SCARD key 返回集合 key 的基数 (即成员数量)。 O(1)
SPOP key [count] 从集合 key 中随机移除并返回一个或多个 (count 指定数量) 成员。 O(1) (单个元素), O(N) (N 为 count)
SRANDMEMBER key [count] 从集合 key 中随机返回一个或多个 (count 指定数量) 成员,但不移除它们。 O(1) (单个元素), O(N) (N 为 count)
SINTER key [key...] 返回给定所有集合的交集。 O(N∗M) (最坏情况, N是最小集合大小, M是集合数)
SUNION key [key...] 返回给定所有集合的并集。 O(N) (N 是所有集合总大小)
SDIFF key [key...] 返回第一个集合与后续所有集合的差集。 O(N) (N 是所有集合总大小)
SINTERSTORE destination key [key...] 计算给定所有集合的交集,并将结果存储在 destination 集合中。 O(N∗M)
SUNIONSTORE destination key [key...] 计算给定所有集合的并集,并将结果存储在 destination 集合中。 O(N)
SDIFFSTORE destination key [key...] 计算第一个集合与后续所有集合的差集,并将结果存储在 destination 集合中。 O(N)
SSCAN key cursor 迭代集合中的元素。 O(1) (每次调用), O(N) (完整迭代)
*数据来源: [25, 31, 32]*
  • CLI 示例:

    Code snippet

    > SADD myset "apple" "banana" "cherry"
    (integer) 3
    > SADD myset "apple" # "apple" 已存在,不会重复添加
    (integer) 0
    > SISMEMBER myset "banana"
    (integer) 1
    > SISMEMBER myset "orange"
    (integer) 0
    > SMEMBERS myset  # 顺序不保证
    1) "cherry"
    2) "apple"
    3) "banana"
    > SCARD myset
    (integer) 3
    
    > SADD groupA "user1" "user2" "user3"
    (integer) 3
    > SADD groupB "user2" "user3" "user4"
    (integer) 3
    > SINTER groupA groupB  # 交集
    1) "user3"
    2) "user2"
    > SUNION groupA groupB  # 并集
    1) "user1"
    2) "user2"
    3) "user3"
    4) "user4"
    > SDIFF groupA groupB   # 差集 (groupA 中有,groupB 中没有的)
    1) "user1"
  • Python (redis-py) 示例 (跟踪网页的独立访客):

    Python

    import redis
    
    r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
    
    page_id = "article:how-to-use-redis-sets"
    visitors_set_key = f"page_visitors:{page_id}"
    
    # 模拟访客访问
    visitor_ips = ["192.168.1.10", "203.0.113.45", "192.168.1.10", "10.0.0.5", "203.0.113.45"]
    
    for ip in visitor_ips:
      # SADD 返回1表示新添加,0表示已存在
      if r.sadd(visitors_set_key, ip) == 1:
          print(f"New unique visitor: {ip} for page {page_id}")
      else:
          print(f"Repeat visitor: {ip} for page {page_id}")
    
    # 获取独立访客数量
    unique_visitor_count = r.scard(visitors_set_key)
    print(f"Total unique visitors for page {page_id}: {unique_visitor_count}") # 输出应为 3
    
    # 获取所有独立访客IP (仅用于演示,大集合时慎用 SMEMBERS)
    all_unique_visitors = r.smembers(visitors_set_key)
    print(f"All unique visitor IPs: {all_unique_visitors}")
    
    # 清理示例数据 (可选)
    # r.delete(visitors_set_key)

    31

SADDSREMSISMEMBER 等核心集合操作的 O(1) 时间复杂度(与集合大小无关)32 使得集合在管理大量唯一项和检查成员资格方面极为高效。传统数据库可能需要复杂的索引或全表扫描来确保唯一性或在大数据集中检查成员资格。Redis 集合由于其底层的哈希表实现,为这些核心操作提供了常数时间性能 32。这种效率对于实时分析(例如,计算唯一活跃用户)或实现诸如“同时喜欢 X 和 Y 的用户”之类的功能至关重要。

服务器端集合代数命令(SINTERSUNIONSDIFF)31 对于数据分析和关系建模非常强大,它们将复杂的逻辑从客户端卸载,并减少了数据传输。想象一下需要查找两个用户之间的共同好友。如果好友列表存储在集合中,SINTER user:A:friends user:B:friends 会直接返回共同好友。如果没有这种服务器端能力,客户端将不得不获取两个完整的集合,通过网络传输它们,然后在客户端执行交集操作。这不仅效率低下,而且更为复杂。这些操作直接在 Redis 中可用,使得以最小的代价进行复杂的数据分析成为可能。

3.6 有序集合 (Sorted Sets / ZSETs)

Redis 有序集合 (Sorted Sets,常简写为 ZSETs) 是一种非常强大的数据结构,它类似于集合,成员是唯一的字符串,但每个成员都关联着一个浮点数类型的分数 (score)。有序集合的成员根据分数进行排序(分数相同则按字典序排序)33。

  • 描述: 唯一字符串成员的集合,每个成员都有一个相关的浮点数分数,集合根据分数自动排序。
  • 实现: 内部通过跳跃列表 (skip list) 和哈希表 (hash table) 结合实现。哈希表用于存储成员到分数的映射,允许 O(1) 复杂度查找成员分数;跳跃列表则用于根据分数对成员进行排序,并支持高效的范围查询 33。
  • 用途:
    • 排行榜: 例如游戏得分榜、文章热度榜等。成员是玩家/文章ID,分数是得分/热度值 7。
    • 优先级队列: 成员是任务,分数是任务的优先级或计划执行时间。
    • 范围查询/二级索引: 当分数可以代表某种度量(如时间戳、价格、权重)时,可以快速查询某个范围内的成员。
    • 速率限制器: 可以用分数记录时间戳,成员记录请求标识,通过查询特定时间窗口内的成员数量来实现。
  • 特性:
    • 成员唯一,分数可重复。
    • 自动按分数排序。
    • 添加、删除、更新成员分数的操作通常具有对数时间复杂度 (O(logN)) 34。
  • 核心命令:
命令 描述 时间复杂度
ZADD key [NX\ | XX][CH] score member [score member...] 向有序集合 key 添加一个或多个成员,或者更新已存在成员的分数。NX:仅添加新成员; XX:仅更新已存在成员; CH:返回被修改的元素数量; INCR:对成员分数进行增加; GT/LT:条件更新(仅当新分数大于/小于旧分数时)。
ZREM key member [member...] 从有序集合 key 中移除一个或多个成员。 O(MlogN) (M为移除数量)
ZSCORE key member 返回有序集合 key 中成员 member 的分数。 O(1)
ZINCRBY key increment member 将有序集合 key 中成员 member 的分数增加 increment O(logN)
ZCARD key 返回有序集合 key 的基数 (成员数量)。 O(1)
ZCOUNT key min max 返回有序集合 key 中,分数在 minmax 之间 (包括 minmax) 的成员数量。 O(logN+M) (M为范围内元素数)
ZRANGE key start stop 按分数从小到大返回有序集合 key 中指定排名范围内的成员。startstop 是 0-based 索引。WITHSCORES 可选,用于同时返回分数。 O(logN+M) (M为返回元素数)
ZREVRANGE key start stop 按分数从大到小返回有序集合 key 中指定排名范围内的成员。 O(logN+M)
ZRANGEBYSCORE key min max 返回有序集合 key 中,所有分数介于 minmax 之间 (包括 minmax) 的成员。按分数从小到大排序。 O(logN+M)
ZREVRANGEBYSCORE key max min 同上,但按分数从大到小排序。 O(logN+M)
ZRANK key member 返回有序集合 key 中成员 member 的排名 (按分数从小到大,0-based)。 O(logN)
ZREVRANK key member 返回有序集合 key 中成员 member 的排名 (按分数从大到小,0-based)。 O(logN)
ZREMRANGEBYRANK key start stop 移除有序集合 key 中指定排名范围内的所有成员。 O(logN+M)
ZREMRANGEBYSCORE key min max 移除有序集合 key 中指定分数范围内的所有成员。 O(logN+M)
ZINTERSTORE destination numkeys key [key...] 计算 numkeys 个有序集合的交集,并将结果存储在 destination 中。可以为每个输入集合指定权重,并指定聚合函数。 O(NlogN+M) (N为最小输入集大小, M为结果集大小)
ZUNIONSTORE destination numkeys key [key...] 计算 numkeys 个有序集合的并集,并将结果存储在 destination 中。 O(N+MlogM) (N为输入总大小, M为结果集大小)
ZSCAN key cursor 迭代有序集合中的元素(包括元素成员和元素分数)。 O(1) (每次调用), O(N) (完整迭代)
*数据来源: [33, 34]*
  • CLI 示例 (排行榜):

    Code snippet

    > ZADD leaderboard 1500 "Alice" 2300 "Bob" 980 "Charlie" 1800 "David"
    (integer) 4
    > ZINCRBY leaderboard 100 "Charlie"  # Charlie 得了 100 分
    "1080"
    > ZREVRANGE leaderboard 0 2 WITHSCORES  # 获取排名前3的玩家及其分数 (分数从高到低)
    1) "Bob"
    2) "2300"
    3) "David"
    4) "1800"
    5) "Alice"
    6) "1500"
    > ZRANK leaderboard "Alice"  # Alice 的排名 (分数从低到高,0-based)
    (integer) 1  # (Charlie 1080, Alice 1500, David 1800, Bob 2300)
    > ZREVRANK leaderboard "Alice" # Alice 的排名 (分数从高到低,0-based)
    (integer) 2
    > ZSCORE leaderboard "David"
    "1800"
  • Python (redis-py) 示例 (简单排行榜):

    Python

    import redis
    
    r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
    
    leaderboard_key = "game_scores:2024_season"
    
    # 添加玩家分数 (使用字典形式更方便)
    # ZADD key mapping={member1: score1, member2: score2}
    r.zadd(leaderboard_key, {
      "player_A": 5500,
      "player_B": 7200,
      "player_C": 6100,
      "player_D": 5500  # player_D 与 player_A 分数相同
    })
    print(f"Initial leaderboard: {r.zrevrange(leaderboard_key, 0, -1, withscores=True)}")
    
    # 增加玩家分数
    r.zincrby(leaderboard_key, 300, "player_A") # player_A 得分增加 300
    print(f"Score of player_A after zincrby: {r.zscore(leaderboard_key, 'player_A')}")
    
    # 获取排名前3的玩家 (分数从高到低)
    top_3_players = r.zrevrange(leaderboard_key, 0, 2, withscores=True)
    print("Top 3 players:")
    for player, score in top_3_players:
      print(f"- {player}: {score}")
    
    # 获取特定玩家的排名 (分数从高到低, 0-based)
    rank_player_C = r.zrevrank(leaderboard_key, "player_C")
    if rank_player_C is not None: # 如果玩家不存在,zrevrank 返回 None
      print(f"Rank of player_C (0-based): {rank_player_C}")
    
    # 获取分数在 6000 到 7000 之间的玩家
    players_in_score_range = r.zrangebyscore(leaderboard_key, 6000, 7000, withscores=True)
    print(f"Players with scores between 6000 and 7000: {players_in_score_range}")
    
    # 获取总玩家数
    total_players = r.zcard(leaderboard_key)
    print(f"Total players on leaderboard: {total_players}")
    
    # 清理示例数据 (可选)
    # r.delete(leaderboard_key)

    33

有序集合是 Redis 的“杀手级特性”之一,因为它们能够高效地解决传统数据库中通常需要复杂索引或操作缓慢的排名和排序问题。唯一性(如集合)和排序(如列表,但按分数)的结合非常强大。在 SQL 数据库中实现实时排行榜通常涉及对索引分数频繁更新和查询,这在高负载下可能成为瓶颈。Redis 有序集合 33 正是为此设计的:ZADD 更新分数效率高(由于跳跃列表,具有对数复杂度)34,而 ZRANGE/ZREVRANGE 获取前 N 名玩家也非常快。这种效率使其成为动态实时排名列表的理想选择。

有序集合中的 score 是一个双精度浮点数 33,这允许非常精细的排序,并且可以创造性地使用,例如,用时间戳作为分数来存储时间序列数据,或用优先级作为分数来实现优先级队列。虽然整数分数在排行榜中很常见,但分数是浮点数的事实意味着您可以表示小数值,甚至可以用分数编码多个信息片段(例如,如果字典序的平局决胜不够用,可以将主排序键作为整数部分,次排序键作为小数部分)。将时间戳用作分数允许 ZRANGEBYSCORE 检索特定时间窗口内的项目,从而有效地将有序集合转变为时间排序的日志或索引。

3.7 流 (Streams)

Redis 流 (Streams) 是一种强大的数据结构,它模拟了一个仅追加的日志 (append-only log),但同时实现了多种操作来克服典型仅追加日志的一些限制,例如支持 O(1) 时间复杂度的随机访问和复杂的消费策略(如消费组)35。流可以用来实时记录和同时分发事件。

  • 描述: 仅追加的日志数据结构,每个条目都有一个唯一的、基于时间的 ID,并包含一组字段-值对。
  • 与列表的区别:
    • ID: 流中的每个条目都有一个由 Redis 自动生成的唯一 ID (通常是 timestamp_in_ms-sequence_number)。列表元素没有固有 ID。
    • 内容: 流的条目是字段-值对的集合 (类似哈希),而列表的元素是简单的字符串。
    • 消费模型: 流支持消费组 (Consumer Groups),允许多个消费者以协调的方式处理同一个流中的消息,确保消息只被一个消费者处理,并支持消息确认 (ACK) 和待处理消息跟踪。列表的消费模型更简单,通常是单个消费者或多个独立消费者竞争。
    • 访问: 流支持按 ID 范围高效检索条目。 35
  • 用途:
    • 事件溯源 (Event Sourcing): 记录应用状态的所有变更作为一系列不可变事件 35。
    • 传感器数据/日志收集: 接收和存储来自大量设备或应用的实时数据流 35。
    • 实时通知系统: 例如,为每个用户在一个单独的流中存储通知记录 35。
    • 消息队列 (比列表更高级): 提供了更强大的消息队列功能,包括持久化、消费组和消息确认 36。
  • 核心命令:
命令 描述
`XADD stream_key [MAXLEN [~] count \ MINID [~] id][* \
XLEN stream_key 返回流 stream_key 的长度 (即条目数量)。
XRANGE stream_key start_id end_id 返回流 stream_key 中 ID 范围在 start_idend_id 之间的条目 (按 ID 升序)。特殊 ID - 表示最小可能 ID,+ 表示最大可能 ID。
XREVRANGE stream_key end_id start_id XRANGE,但按 ID 降序返回条目。
XREAD STREAMS stream_key [stream_key...] id [id...] 从一个或多个流中读取条目,从指定的 id 之后开始。BLOCK 使命令阻塞直到有新条目或超时。特殊 ID $ 表示只读取新到达的条目。
XGROUP CREATE stream_key group_name id 为流 stream_key 创建一个名为 group_name 的消费组,从指定的 id 开始消费。MKSTREAM 会在流不存在时创建它。ENTRIESREAD (Redis 7.0+) 设置组的 entries_read 计数器。
XGROUP DESTROY stream_key group_name 销毁流 stream_key 上的消费组 group_name
XGROUP SETID stream_key group_name id \ | $
XGROUP DELCONSUMER stream_key group_name consumer_name 从消费组 group_name 中删除消费者 consumer_name
XREADGROUP GROUP group_name consumer_name[NOACK] STREAMS stream_key [stream_key...] id [id...] 作为消费组 group_name 中的消费者 consumer_name 从一个或多个流中读取消息。特殊 ID > 表示读取尚未传递给该组内任何其他消费者的消息。NOACK 表示消息不需要确认。
XACK stream_key group_name id [id...] 向消费组 group_name 确认一个或多个消息 id 已成功处理。已确认的消息将从该消费者的待处理条目列表 (PEL) 中移除。
XPENDING stream_key group_name start_id end_id count [consumer_name]] 显示消费组 group_name 中待处理消息 (已传递但未确认) 的信息。可以按空闲时间、ID 范围、数量和消费者进行过滤。
XCLAIM stream_key group_name consumer_name min_idle_time id [id...] 将一个或多个待处理消息 id 的所有权从原消费者转移给当前消费者 consumer_name,通常用于处理长时间未确认消息的故障恢复。min_idle_time 是消息被认领的最小空闲时间。
XAUTOCLAIM stream_key group_name consumer_name min_idle_time start_id (Redis 6.2+) 自动认领消费组中符合条件的过期待处理消息。
XTRIM stream_key MAXLEN [~] count \ | MINID [~] id
XDEL stream_key id [id...] 从流中删除指定的条目。注意:这并不会立即释放内存,只是标记条目为已删除。
*数据来源: [35, 36]*
  • (概念图: 流数据流与消费组)

    Code snippet

    graph TD
      P1[生产者1] --> XADD(XADD)
      P2[生产者2] --> XADD
      XADD --> S;
    
      subgraph 消费组 A
          direction LR
          CG_A_R --> C1[消费者 A1]
          C1 --> XACK_A(XACK group_A)
          CG_A_R2 --> C2[消费者 A2]
          C2 --> XACK_A2(XACK group_A)
      end
    
      subgraph 消费组 B
          direction LR
          CG_B_R --> C3
          C3 --> XACK_B(XACK group_B)
      end
    
      S --> CG_A_R
      S --> CG_A_R2
      S --> CG_B_R
    
      XACK_A --> S
      XACK_A2 --> S
      XACK_B --> S
  • CLI 示例 (事件溯源):

    假设我们正在为一个电子商务应用的订单处理流程进行事件溯源。

    Code snippet

    # 生产者: 创建一个订单事件
    > XADD order_events * user_id "user123" item_id "prod789" quantity 2 status "created"
    "1678886400000-0"  # 返回事件 ID
    
    # 生产者: 更新订单状态
    > XADD order_events * order_id "1678886400000-0" status "payment_pending"
    "1678886405123-0"
    
    # 创建一个消费组 "shippers_group" 来处理已付款的订单,从流的开始读取
    > XGROUP CREATE order_events shippers_group 0 MKSTREAM
    OK
    
    # 消费者 "shipper_worker_1" 从 "shippers_group" 读取最多10条新消息
    # 假设之前有付款事件: XADD order_events * order_id "1678886400000-0" status "paid"
    > XREADGROUP GROUP shippers_group shipper_worker_1 COUNT 10 STREAMS order_events >
    # 假设返回了 ID 为 "1678886410000-0" 的已付款订单事件
    # 1) 1) "order_events"
    #    2) 1) 1) "1678886410000-0"
    #          2) 1) "order_id"
    #             2) "1678886400000-0"
    #             3) "status"
    #             4) "paid"
    
    # shipper_worker_1 处理完发货后,确认消息
    > XACK order_events shippers_group "1678886410000-0"
    (integer) 1
    
    # 查看 "shippers_group" 中是否有待处理的消息
    > XPENDING order_events shippers_group - + 10
    # (如果 shipper_worker_1 未确认,这里会列出消息)

    35

Redis 流及其消费组为构建弹性、可扩展的消息处理系统提供了强大的内置机制,其功能特性与 Kafka 或 RabbitMQ 等专用消息代理相似,但集成在 Redis 内部。简单的基于列表的队列 8 适用于基本场景,但缺乏消息确认、向多个独立消费者/组传递消息以及消费者状态持久化等功能。流解决了这些问题。XADD 35 追加消息。XREADGROUP 35 允许组内的多个消费者拉取消息,Redis 确保每个消息只传递给该组中的一个消费者。XACK 35 确认处理。XPENDINGXCLAIM 36 处理消费者失败时的消息恢复。这一系列功能为实现持久的、至少一次消息传递语义提供了构建块,这是专用消息队列系统的标志。

流中自动生成的、基于时间的有序 ID 35 是其效用的基础,它允许轻松进行范围查询 (XRANGE)、从特定时间点读取 (XREAD),并确保事件的顺序一致。流 ID 通常是 timestamp_ms-sequence_num 的形式。这种结构固有地按时间顺序排列条目。像 XRANGE 35 这样的命令随后可以获取特定时间窗口内的消息。XREAD 加上一个 ID 允许消费者从上次离开的地方继续读取。这种基于时间的有序特性对于事件溯源、日志聚合以及任何事件顺序和时间重要的场景都是必不可少的。

3.8 位图 (Bitmaps)

Redis 位图并非一种独立的数据类型,而是一组在字符串类型上定义的面向位的操作,将字符串视为一个位向量 (bit vector) 37。由于字符串是二进制安全的,并且最大长度可达 512 MB,因此它们适合设置多达 232 (约40亿) 个不同的位。

  • 描述: 对字符串类型进行的一系列位操作,将字符串视为一个可以按位设置和读取的位数组。
  • 用途:
    • 大规模布尔状态跟踪: 例如,跟踪每日活跃用户 (每个用户ID对应一个位)、用户签到、功能开关状态、对象权限 (每个位代表一个特定权限) 等 37。
    • 实时分析: 通过位运算 (AND, OR, XOR, NOT) 对多个位图进行分析,例如计算用户重合度、留存率等 37。
    • 紧凑存储: 对于只需要存储是/否信息的大量实体,位图非常节省空间。
  • 特性:
    • 位图会自动扩展以容纳所需的最高位偏移量。
    • 位操作非常快速。
  • 核心命令:
命令 描述 时间复杂度
SETBIT key offset value 设置或清除键 keyoffset 处的位 (0-based)。value 只能是 0 或 1。如果 offset 超出现有字符串长度,字符串会自动扩展,并用 0 填充。返回该位在操作前的旧值。 O(1)
GETBIT key offset 获取键 keyoffset 处的位的值。如果 offset 超出字符串长度,或者键不存在,则返回 0。 O(1)
BITCOUNT key [start end] 计算键 key 所储存的字符串中,被设置为 1 的位的数量。可选的 startend 参数 (以字节为单位,非位) 可以指定计数的范围。 O(N) (N 为字符串长度)
BITPOS key bit [start end] 返回字符串里面第一个被设置为 bit (0 或 1) 的位的位置。可选的 startend 参数 (以字节为单位) 指定搜索范围。Redis 7.0+ 支持 BYTE | BIT 指定返回字节还是位偏移。
BITOP operation destkey key [key...] 对一个或多个保存二进制位的字符串 key 进行位运算,并将结果保存到 destkey。支持的 operationAND, OR, XOR, NOTNOT 操作只接受一个源键。 O(N) (N 为最长字符串长度)
BITFIELD key 对字符串进行任意位宽的整数的读取和修改。功能强大但复杂,可以看作是更通用的位操作命令。 O(N) (N 为访问的位数或命令数量)
*数据来源: [37, 38]*
  • CLI 示例 (跟踪用户每日登录)

    : 假设我们用位图跟踪用户在某天的登录情况,用户 ID 作为位的偏移量。

    Code snippet

    # 用户 100 在 2023-10-26 登录
    > SETBIT logins:2023-10-26 100 1
    (integer) 0  # 该位之前是 0
    # 用户 250 在 2023-10-26 登录
    > SETBIT logins:2023-10-26 250 1
    (integer) 0
    # 用户 100 再次登录 (重复操作,位值不变)
    > SETBIT logins:2023-10-26 100 1
    (integer) 1  # 该位之前是 1
    
    # 检查用户 100 是否登录
    > GETBIT logins:2023-10-26 100
    (integer) 1
    # 检查用户 500 是否登录 (假设未登录)
    > GETBIT logins:2023-10-26 500
    (integer) 0
    
    # 计算 2023-10-26 的独立登录用户数
    > BITCOUNT logins:2023-10-26
    (integer) 2
    
    # 假设有另一天的登录数据
    > SETBIT logins:2023-10-27 100 1
    (integer) 0
    > SETBIT logins:2023-10-27 300 1
    (integer) 0
    
    # 计算在 2023-10-26 和 2023-10-27 都登录的用户 (留存分析)
    > BITOP AND retention:26_27 logins:2023-10-26 logins:2023-10-27
    (integer) 32 # (结果字符串的字节长度,取决于最大偏移量)
    > BITCOUNT retention:26_27
    (integer) 1  # 只有用户 100 两天都登录了

位图为表示密集的布尔数据集(其中实体可以映射到整数偏移量)提供了极高的内存效率,这是它们的主要优势。例如,要跟踪一百万用户是否每日活跃,使用集合将存储一百万个用户 ID。而使用位图,如果用户 ID 是从 0 到 999,999,则仅需要 1,000,000 位 / 8 位/字节 ≈ 125KB 的内存 38。这是一个巨大的节省。其代价是用户 ID 必须是(或可映射到)整数偏移量。

BITOP 命令 37 能够在服务器端对这些布尔数据集执行强大的分析,例如通过对多个位图(例如每日活动位图)执行位运算来计算留存率、群组分析或功能采用率。如果 logins:YYYY-MM-DD 存储每日活跃用户,那么 BITOP AND monthly_active logins:day1 logins:day2... logins:day30(概念上,尽管 BITOP 通常直接接受较少数量的键)可以找到每天都活跃的用户。更实际地,BITOP AND retained_users logins:week1 logins:week2 可以找到在第1周和第2周都活跃的用户。对结果执行 BITCOUNT 即可得到计数。这使得在服务器上可以非常高效地执行复杂的行为分析。

3.9 HyperLogLogs

Redis HyperLogLog (HLL) 是一种概率数据结构,用于估算一个集合的基数 (即集合中唯一元素的数量),特别适用于非常大的数据集 [39, 40]。它的主要优点是以一个很小且恒定的内存占用 (大约 12KB) 来提供基数估计,其标准误差通常在 0.81% 左右 [40]。

  • 描述: 一种概率性数据结构,用于对大数据集的唯一元素数量进行近似计数。

  • 与集合 (Sets) 的对比:

    • 内存: HLL 使用固定的、非常小的内存 (约 12KB),无论集合多大。而集合的内存消耗与唯一元素的数量和大小成正比 [39, 40]。
    • 精度: HLL 提供的是估计值,存在一定的误差 (Redis 实现中约为 0.81%)。集合提供精确的计数值。
    • 存储元素: HLL 不存储实际的元素值,只存储用于估计基数的状态。集合存储所有实际的唯一元素。
  • 用途:

    • 统计网站或应用的独立访客数 (UV) [39, 40]。
    • 统计用户在搜索引擎中执行的唯一查询数量。
    • 统计大规模数据集中的不同元素个数,而无需消耗大量内存。
  • 核心命令:

命令 描述 时间复杂度
PFADD key element [element...] 将一个或多个元素添加到 HyperLogLog 结构中。如果 HLL 的内部寄存器被修改,返回 1,否则返回 0。 $O(1)$ (每个元素)
PFCOUNT key [key...] 返回单个 HyperLogLog 的近似基数。如果给出多个 key,则返回它们的并集的近似基数,而不会修改任何一个 HLL。 $O(1)$ (单个 key)
$O(N)$ (多个 key, N为HLL数量)
PFMERGE destkey sourcekey [sourcekey...] 将一个或多个源 HyperLogLog 合并成一个目标 HyperLogLog。合并后的 HLL 将包含所有源 HLL 的并集的基数估计。 $O(N)$ (N为HLL数量)
*数据来源: [39, 40]*
  • CLI 示例:

    # 跟踪 2023-10-26 的独立访客
    > PFADD daily_uvs:2023-10-26 "user1" "user2" "user3"
    (integer) 1
    > PFADD daily_uvs:2023-10-26 "user2" "user4" # 添加已存在的 "user2" 和新的 "user4"
    (integer) 1
    > PFCOUNT daily_uvs:2023-10-26
    (integer) 4 # (估计值为 4)
    
    # 跟踪 2023-10-27 的独立访客
    > PFADD daily_uvs:2023-10-27 "user3" "user5" "user6"
    (integer) 1
    > PFCOUNT daily_uvs:2023-10-27
    (integer) 3
    
    # 计算这两天的总独立访客数 (并集)
    > PFCOUNT daily_uvs:2023-10-26 daily_uvs:2023-10-27
    (integer) 6 # (user1, user2, user3, user4, user5, user6)
    
    # 将两天的 HLL 合并成一个周 HLL,以备后用
    > PFMERGE weekly_uvs:2023-w43 daily_uvs:2023-10-26 daily_uvs:2023-10-27
    OK
    > PFCOUNT weekly_uvs:2023-w43
    (integer) 6

    [39]

  • Python (redis-py) 示例:

    import redis
    
    r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
    
    # 模拟三天的网站访客
    hll_day1 = "uv_stats:2024-01-01"
    hll_day2 = "uv_stats:2024-01-02"
    hll_day3 = "uv_stats:2024-01-03"
    
    # 添加访客
    r.pfadd(hll_day1, "alice", "bob", "carol")
    r.pfadd(hll_day2, "bob", "david", "eve")
    r.pfadd(hll_day3, "alice", "frank", "grace")
    
    # 计算每天的独立访客数 (UV)
    uv_day1 = r.pfcount(hll_day1)
    uv_day2 = r.pfcount(hll_day2)
    print(f"Day 1 UV estimate: {uv_day1}") # ~3
    print(f"Day 2 UV estimate: {uv_day2}") # ~3
    
    # 计算第一天和第二天的总 UV
    uv_day1_day2 = r.pfcount(hll_day1, hll_day2)
    print(f"Total UV for Day 1 & 2: {uv_day1_day2}") # ~5 (alice, bob, carol, david, eve)
    
    # 合并三天的 UV 数据以计算周 UV
    weekly_hll = "uv_stats:2024-week1"
    r.pfmerge(weekly_hll, hll_day1, hll_day2, hll_day3)
    weekly_uv = r.pfcount(weekly_hll)
    print(f"Total weekly UV for first 3 days: {weekly_uv}") # ~7 (all unique users)

HyperLogLog 在需要进行基数统计但无法承受精确计数所需内存的场景中,提供了一个出色的权衡方案。当需要统计数百万甚至数十亿个唯一项时,使用集合(Sets)会消耗千兆字节(GB)的内存。而 HLL 仅需约 12KB 就能完成这项工作,且其性能与所计数的项数无关 [39, 40]。这种效率使其成为大规模数据分析和监控系统的关键组成部分。

PFMERGE 命令 [39] 尤其强大,它允许将多个 HLL 结构合并以计算其并集的基数。这意味着可以轻松地聚合时间窗口数据。例如,可以为每天创建一个 HLL 来跟踪日活跃用户(DAU),然后使用 PFMERGE 将 7 天的 HLL 合并,从而轻松地、高效地计算出周活跃用户(WAU),而无需重新处理原始数据。


3.10 地理空间索引 (Geospatial Indexes)

Redis 通过一系列以 GEO 开头的命令,提供了对地理空间数据的原生支持。这些功能允许您存储地理位置(经度和纬度)并对其执行各种查询,例如查找特定半径内的点或计算两点之间的距离。

  • 描述: 一种用于存储和查询地理位置坐标的数据结构。

  • 实现: 地理空间索引在底层是使用有序集合 (ZSETs) 实现的。它通过一种称为 Geohash 的技术,将二维的经纬度坐标编码成一维的分数,然后将位置成员和其 Geohash 分数存储在有序集合中。这巧妙地利用了有序集合的排序和范围查询能力来实现地理空间查询。

  • 用途:

    • 查找“附近的人”或“附近的地点”。
    • 基于位置的服务 (LBS),例如打车应用、外卖服务。
    • 地理围栏 (Geofencing)。
  • 核心命令:

命令 描述 时间复杂度
GEOADD key [NX|XX] [CH] longitude latitude member [longitude latitude member...] 将一个或多个指定的地理空间项(经度、纬度、名称)添加到指定的 key 中。 $O(\log N)$ (每个项)
GEOPOS key member [member...] key 里面返回所有指定 member 的经纬度。 $O(\log N)$ (每个项)
GEODIST key member1 member2 [unit] 返回两个给定位置之间的距离。单位 unit 可以是 m (米, 默认), km (千米), mi (英里), ft (英尺)。 $O(\log N)$
GEORADIUS key longitude latitude radius unit [WITHDIST] [WITHCOORD] [WITHHASH] [COUNT count] [ASC|DESC] (已弃用, 建议使用 GEOSEARCH) 以给定的经纬度为中心,返回键中包含的所有位置项中,与中心距离不超过给定最大距离 radius 的所有位置项。 $O(N + \log M)$ (N为返回结果数, M为索引内项数)
GEORADIUSBYMEMBER key member radius unit [...] (已弃用, 建议使用 GEOSEARCH) 和 GEORADIUS 类似,但中心点由给定的 member 的位置决定,而不是直接提供经纬度。 $O(N + \log M)$
GEOSEARCH key FROMMEMBER member | FROMLONLAT longitude latitude BYRADIUS radius unit | BYBOX width height unit [ASC|DESC] [COUNT count] [WITH... ] (Redis 6.2+) 在地理空间索引中搜索。可以按成员位置或经纬度为中心,通过圆形 (BYRADIUS) 或矩形 (BYBOX) 范围进行搜索。这是 GEORADIUSGEORADIUSBYMEMBER 的现代替代品。 $O(N + \log M)$
GEOSEARCHSTORE destination source [FROMMEMBER... | FROMLONLAT...] [BYRADIUS... | BYBOX...] [...] (Redis 6.2+) 与 GEOSEARCH 类似,但将结果存储在 destination 键中。 $O(N + \log M)$
GEOHASH key member [member...] 返回一个或多个位置项的 Geohash 字符串。 $O(\log N)$ (每个项)
  • CLI 示例:

    # 添加几个城市的位置信息
    > GEOADD cities 13.361389 52.520008 "Berlin" 2.352222 48.856613 "Paris" -0.1278 51.5074 "London"
    (integer) 3
    
    # 获取巴黎的坐标
    > GEOPOS cities "Paris"
    1) 1) "2.35222194623947144"
       2) "48.85661394989608826"
    
    # 计算伦敦到巴黎的距离 (单位:千米)
    > GEODIST cities "London" "Paris" km
    "343.5539"
    
    # 使用 GEOSEARCH 查找以伦敦为中心,半径 400 公里内的城市
    # (在 Redis 6.2+ 上推荐使用 GEOSEARCH)
    > GEOSEARCH cities FROMMEMBER "London" BYRADIUS 400 km WITHDIST
    1) 1) "London"
       2) "0.0000"
    2) 1) "Paris"
       2) "343.5539"
    
    # 使用旧版 GEORADIUS 命令达到类似效果
    > GEORADIUS cities 13.36 52.52 1000 km WITHDIST COUNT 2 ASC
    1) 1) "Berlin"
       2) "0.0001"
    2) 1) "Paris"
       2) "878.0772"
  • Python (redis-py) 示例 (查找附近的咖啡店):

    import redis
    
    r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
    
    coffee_shops_key = "coffee_shops:palo_alto"
    
    # 添加咖啡店位置 (经度, 纬度, 名称)
    # 注意: redis-py 的 geoadd 方法接受 (key, (lon, lat, name), ...) 的格式
    shops = [
        (-122.159, 37.444, "Philz Coffee"),
        (-122.161, 37.446, "Blue Bottle Coffee"),
        (-122.143, 37.441, "Starbucks"),
        (-122.121, 37.425, "Peet's Coffee") # 稍远一点
    ]
    for lon, lat, name in shops:
        r.geoadd(coffee_shops_key, (lon, lat, name))
    
    print("Coffee shops added.")
    
    # 我的当前位置
    my_longitude = -122.16
    my_latitude = 37.445
    
    # 查找以我为中心,半径 1 公里内的所有咖啡店,并显示距离
    # GEOSEARCH in redis-py requires keyword arguments
    nearby_shops = r.geosearch(
        coffee_shops_key,
        longitude=my_longitude,
        latitude=my_latitude,
        radius=1,
        unit="km",
        withdist=True,
        withcoord=True
    )
    
    print(f"\nShops within 1km of ({my_longitude}, {my_latitude}):")
    for shop in nearby_shops:
        # shop is a list: [name, distance, (longitude, latitude)]
        name, distance, (lon, lat) = shop
        print(f"- {name}: {distance:.2f} km away at ({lon:.3f}, {lat:.3f})")
    
    # 计算 Philz Coffee 和 Blue Bottle Coffee 之间的距离 (单位:米)
    distance_m = r.geodist(coffee_shops_key, "Philz Coffee", "Blue Bottle Coffee", unit="m")
    print(f"\nDistance between Philz and Blue Bottle: {distance_m:.0f} meters")

地理空间索引功能展示了 Redis 如何在现有数据结构(有序集合)之上构建复杂功能。通过将二维地理坐标映射到一维 Geohash 字符串,Redis 可以利用有序集合的对数时间复杂度的插入和范围查询能力来高效地执行地理空间查询。这种实现上的优雅意味着 Redis 无需为地理空间功能引入全新的底层数据结构,从而保持了其代码库的精简,并让地理空间操作受益于有序集合的成熟和性能优化。

GEOSEARCH (以及其前身 GEORADIUS) 是该功能的核心。它们使得实现“附近”功能变得非常简单,这是许多现代移动和 Web 应用的关键特性。如果没有原生支持,开发者将需要使用复杂的数学计算(如 Haversine 公式)并可能在传统数据库上执行昂贵的查询。Redis 将这种复杂性抽象出来,提供了一个简单、高性能的 API 来回答“什么在我附近?”这个问题。


3.11 数据结构命令总结与应用场景

为了帮助您在实际开发中快速选择最合适的数据结构,下表总结了 Redis 主要数据结构的特性和典型应用场景。

数据结构 核心特性 典型应用场景 关键命令
String 简单的键值对,二进制安全,支持原子性增减。 缓存 (页面、对象、API 响应),计数器 (点赞数、访问量),分布式锁。 SET, GET, INCR, DECR, MSET, MGET
List 有序的字符串链表,支持从两端快速操作 ($O(1)$),支持阻塞式弹出。 消息队列/任务队列,活动时间线/最新动态列表,栈 (LIFO)。 LPUSH, RPUSH, LPOP, RPOP, LRANGE, BLPOP
Hash 字段-值的映射集合,适合表示对象,可独立更新字段,内存优化。 存储对象 (用户信息、商品详情),将相关数据分组到一个键下。 HSET, HGET, HGETALL, HINCRBY, HMGET
Set 无序、唯一的字符串集合,支持高效的成员检查和服务器端集合运算。 标签系统 (文章标签、用户兴趣),跟踪独立项 (独立访客 IP),共同好友/兴趣推荐。 SADD, SISMEMBER, SMEMBERS, SINTER, SUNION
Sorted Set (ZSet) 唯一的字符串成员,每个成员关联一个浮点数分数,并按分数排序。 排行榜 (游戏得分、热榜),优先级队列,带权重的任务,范围查询 (如按时间戳)。 ZADD, ZRANGE, ZREVRANGE, ZSCORE, ZRANK
Stream 仅追加的日志,每个条目有唯一 ID 和字段-值对,支持消费组。 事件溯源,高级消息队列 (支持 ACK 和持久化),传感器数据收集,日志聚合。 XADD, XREAD, XREADGROUP, XACK, XPENDING
Bitmap 在字符串上进行位操作,极度节省空间,用于大规模布尔状态跟踪。 用户签到,日/月活跃用户统计 (DAU/MAU),功能开关,权限控制。 SETBIT, GETBIT, BITCOUNT, BITOP
HyperLogLog 概率性基数统计,使用恒定且极小的内存 (约 12KB) 估计唯一元素数量。 海量数据的独立访客 (UV) 统计,大规模数据集的唯一项计数。 PFADD, PFCOUNT, PFMERGE
Geospatial 基于 ZSET 实现,存储和查询经纬度坐标。 查找附近的人/地点,LBS 应用,地理围栏。 GEOADD, GEODIST, GEOSEARCH, GEOPOS

选择正确的数据结构是高效使用 Redis 的关键。在设计数据模型时,应首先考虑问题的核心需求:

  • 如果需要存储一个简单的值或进行原子计数,字符串是首选。
  • 如果需要一个有序的元素序列,并作为队列或栈使用,列表是理想选择。
  • 如果需要表示一个结构化对象并能独立更新其字段,哈希比序列化的 JSON 字符串更优。
  • 如果需要存储一组唯一的元素并进行集合运算(如交集、并集),应使用集合
  • 如果不仅需要唯一性,还需要根据一个权重或分数进行排序和排名,有序集合是无与伦比的。
  • 如果需要一个持久的、支持多消费者的消息系统,提供了比列表更强大的功能。
  • 如果需要以极高的空间效率跟踪大量的布尔状态,并且实体可以映射到整数 ID,位图是最佳方案。
  • 如果需要对海量数据进行唯一项计数且可以接受微小误差,HyperLogLog 可以节省大量内存。
  • 如果应用涉及地理位置查询,地理空间索引提供了简单易用的接口。

Chapter 4: 数据持久化

虽然 Redis 主要是一个内存数据库,以提供极致的性能,但它也深知数据安全的重要性。因此,Redis 提供了两种主要的持久化机制,以确保内存中的数据在服务器重启或意外宕机后不会丢失。

4.1 持久化概述

持久化 (Persistence) 是指将内存中的数据写入到永久性存储(如硬盘)的过程。对于 Redis 而言,这意味着将键值对、数据结构等从易失性的 RAM 转移到非易失性的磁盘上。

  • 为什么需要持久化?
    • 故障恢复: 如果 Redis 服务器因为断电、操作系统崩溃或人为错误而关闭,内存中的所有数据都将丢失。持久化机制可以将数据保存到磁盘文件中,在 Redis 重启时,可以通过加载这些文件来恢复数据,将损失降到最低。
    • 数据备份: 持久化产生的文件可以被复制到其他存储介质或位置,用于数据备份和灾难恢复。
    • 数据迁移: 可以将持久化文件从一台服务器复制到另一台,以实现数据的迁移。

Redis 提供了两种主要的持久化策略:RDB (Redis Database)AOF (Append Only File)

特性 RDB (快照) AOF (仅追加文件)
工作方式 在特定时间点生成数据集的完整快照。 记录服务器执行的每一个写命令。
数据安全性 较低。两次快照之间的数据可能会丢失。 较高。根据 appendfsync 策略,最多丢失1秒的数据。
文件大小 较小。是经过压缩的二进制文件。 较大。通常比 RDB 文件大,包含所有写命令。
恢复速度 快。直接加载二进制文件即可。 慢。需要逐条重新执行所有写命令。
性能影响 fork() 子进程时可能导致短暂的服务停顿。 对写操作性能有一定影响,取决于 fsync 策略。
可读性 二进制文件,不可读。 文本文件 (协议格式),可读性较好。

用户可以选择单独使用 RDB 或 AOF,也可以同时启用两者。从 Redis 4.0 开始,当同时启用 RDB 和 AOF 时,AOF 重写会直接利用 RDB 快照的内容,使得混合持久化模式更加高效和可靠。

4.2 RDB (快照)

RDB 持久化是在指定的时间间隔内,生成当前内存中数据集的一个时间点快照 (snapshot),并将其写入磁盘。

4.2.1 工作原理

当触发 RDB 持久化时,Redis 主进程会执行 fork() 系统调用,创建一个子进程。

  1. fork(): 子进程拥有与父进程完全相同的内存数据副本 (利用了写时复制 COW, Copy-on-Write 机制)。
  2. 子进程写盘: 子进程负责将内存中的数据写入到一个临时的 RDB 文件中。
  3. 父进程继续服务: 父进程继续处理客户端的请求,不受写盘操作的影响。
  4. 替换旧文件: 当子进程完成写盘后,它会用新的临时文件原子性地替换掉旧的 RDB 文件。

这种机制使得 Redis 在进行持久化时,对主服务进程的影响降到最低。

4.2.2 优点与缺点

  • 优点:

    • 紧凑的文件: RDB 文件是一个经过压缩的二进制文件,非常适合用于备份和数据迁移。
    • 快速的恢复: 在恢复大数据集时,加载 RDB 文件比重新执行 AOF 文件中的命令要快得多。
    • 对性能影响小: Redis 主进程不直接进行磁盘 I/O,而是由子进程完成。
  • 缺点:

    • 数据丢失风险: RDB 是按时间间隔进行快照的。如果在两次快照之间 Redis 发生故障,那么这期间的所有数据变更都将丢失。
    • fork() 的开销: 当数据集非常大时,fork() 操作可能会消耗较多时间和内存资源,导致 Redis 服务在 fork() 期间出现短暂的停顿 (latency spike)。

4.2.3 配置

redis.conf 文件中,与 RDB 相关的配置主要有:

  • save <seconds> <changes>: 设置触发自动快照的条件。可以配置多条规则。

    # 示例:
    save 900 1   # 900秒内至少有1次写入
    save 300 10  # 300秒内至少有10次写入
    save 60 10000# 60秒内至少有10000次写入

    如果想禁用自动 RDB 持久化,可以注释掉所有的 save 行,或者设置 save ""

  • stop-writes-on-bgsave-error yes: 当后台 RDB 保存操作失败时,是否停止接受写命令。yes (默认) 可以防止用户在持久化出问题时不知道,继续写入数据。

  • rdbcompression yes: 是否对 RDB 文件进行 LZF 压缩。yes (默认) 可以大大减小文件体积,但会消耗一些 CPU。

  • rdbchecksum yes: 是否在存储和加载 RDB 文件时进行 CRC64 校验。yes (默认) 可以提供更高的数据完整性保证,但会有一点性能开销 (约 10%)。

  • dbfilename dump.rdb: RDB 快照文件的名称。

  • dir ./: RDB 文件和 AOF 文件存放的目录。强烈建议修改为专用的持久化数据目录,如 /var/lib/redis

4.3 AOF (仅追加文件)

AOF 持久化记录了服务器接收到的每一个写操作命令 (以 Redis 协议的格式),并将这些命令追加到文件的末尾。当 Redis 重启时,它会通过重新执行 AOF 文件中的所有命令来重建数据集。

4.3.1 工作原理

  1. 命令追加 (Append): 当一个写命令到达时,Redis 会将其追加到 aof_buf (AOF 缓冲区) 中。
  2. 文件同步 (Sync): 根据 appendfsync 配置的策略,Redis 将 AOF 缓冲区的内容写入到内核缓冲区,并最终同步 (fsync) 到磁盘上的 AOF 文件。
  3. 日志重写 (Rewrite): 随着写操作的增多,AOF 文件会变得越来越大。Redis 提供了 AOF 重写机制来解决这个问题。它会在后台创建一个新的 AOF 文件,这个新文件包含了重建当前数据集所需的最少命令集合,而不是简单地复制旧文件。重写过程也是通过 fork() 子进程来完成的,对主进程影响很小。

4.3.2 优点与缺点

  • 优点:

    • 更高的数据安全性: 根据 appendfsync 策略,AOF 可以做到只丢失最多1秒的数据 (everysec 策略) 或者完全不丢失数据 (always 策略)。
    • 文件可读性好: AOF 文件是协议文本格式,易于理解和修复。如果文件末尾因宕机而损坏,可以使用 redis-check-aof 工具轻松修复。
    • 避免了 RDB 的 fork() 停顿问题: AOF 重写虽然也 fork(),但其触发频率和时机通常比 RDB 更可控。
  • 缺点:

    • 文件体积大: 对于相同的数 据集,AOF 文件通常比 RDB 文件大。
    • 恢复速度慢: 数据恢复时需要重新执行所有写命令,速度比加载 RDB 慢。
    • 对写性能有影响: fsync 操作会带来一定的性能开销,尤其是在 always 策略下。

4.3.3 配置

redis.conf 文件中,与 AOF 相关的配置主要有:

  • appendonly no: 是否启用 AOF。设置为 yes 来开启。

  • appendfilename "appendonly.aof": AOF 文件的名称。

  • appendfsync everysec: AOF 同步到磁盘的策略。

    • always: 每个写命令都立即同步,最安全但最慢。
    • everysec (默认): 每秒同步一次,性能和安全性的良好折中。
    • no: 完全由操作系统决定何时同步,最快但不安全。
  • no-appendfsync-on-rewrite no: 在 AOF 重写或 RDB 保存期间,是否阻止 fsync 操作。设置为 yes 可以避免主进程阻塞,但可能导致更多数据丢失。

  • auto-aof-rewrite-percentage 100: 触发 AOF 自动重写的条件之一。当 AOF 文件大小相比上次重写后的大小增长了 100% (即翻倍) 时,触发重写。

  • auto-aof-rewrite-min-size 64mb: 触发 AOF 自动重写的条件之二。只有当 AOF 文件大小达到这个阈值时,才会结合 auto-aof-rewrite-percentage 进行判断。

  • aof-use-rdb-preamble yes (Redis 4.0+): 在 AOF 重写时,是否使用 RDB 快照作为 AOF 文件的前缀。这可以大大加快恢复速度,因为它允许 Redis 在恢复时先加载 RDB 部分,再执行增量 AOF 命令。

4.4 RDB 与 AOF 的选择与混合使用

  • 如何选择?

    • 如果能接受分钟级别的数据丢失,并且关心快速恢复和备份,单独使用 RDB 是一个不错的选择。
    • 如果对数据安全性要求非常高,不能接受超过1秒的数据丢失,那么单独使用 AOF 是必须的。
    • 在大多数情况下,同时启用 RDB 和 AOF 是最佳实践。
  • 混合持久化 (Redis 4.0+)
    当同时启用 RDB 和 AOF 时,Redis 在启动时会优先加载 AOF 文件来恢复数据,因为 AOF 通常能保证更完整的数据。

    更重要的是,从 Redis 4.0 开始,当触发 AOF 重写时,如果 aof-use-rdb-preamble 设置为 yes (默认值),重写过程会发生变化:

    1. Redis fork() 一个子进程。
    2. 子进程将当前内存中的数据集以 RDB 的格式写入到新的 AOF 文件的开头。
    3. 然后,子进程将父进程在重写期间执行的增量写命令追加到这个新 AOF 文件的末尾。
    4. 最后,用这个包含 RDB 前缀和增量 AOF 命令的新文件替换旧的 AOF 文件。

    这种混合格式的 AOF 文件,在恢复时可以先快速加载 RDB 部分,然后应用增量命令,结合了 RDB 恢复快和 AOF 数据安全性高的优点。

4.5 备份与恢复策略

  • 备份:

    1. 定时任务: 设置一个定时任务 (如 cron job),定期将 RDB 文件 (dump.rdb) 或 AOF 文件 (appendonly.aof) 复制到安全的、不同的存储位置 (例如另一台服务器、对象存储服务等)。
    2. 文件一致性: 在复制文件之前,最好先禁用写操作或使用工具确保文件的一致性。对于 RDB,可以直接复制,因为它是时间点快照。对于 AOF,如果 Redis 正在运行,直接复制可能会抓取到一个不完整的状态。一个安全的做法是先执行 BGSAVE 创建一个最新的 RDB 快照,然后备份这个快照。
    3. 保留多个版本: 备份时应保留多个时间点的版本,以防最新的备份文件也已损坏。
  • 恢复:

    1. 停止 Redis: 在恢复数据前,先停止当前的 Redis 服务。
    2. 替换文件: 将备份的 RDB 或 AOF 文件复制到 Redis 的工作目录 (dir 配置项指定的路径)下,并确保文件名与配置 (dbfilenameappendfilename) 一致。
    3. 启动 Redis: 重新启动 Redis 服务。Redis 会自动检测并加载持久化文件来恢复数据。

    注意: 如果同时存在 dump.rdbappendonly.aof 文件,Redis 默认会优先使用 appendonly.aof 文件进行恢复。

Leave a Comment

您的电子邮箱地址不会被公开。 必填项已用*标注

close
arrow_upward