Redis 基础教程:从常用命令到实际业务场景

文章目录

  • [1. Redis 介绍与简单使用:从内存键值数据库到常见业务场景](#1. Redis 介绍与简单使用:从内存键值数据库到常见业务场景)
    • [1.1 先理解 Redis 在系统中的位置](#1.1 先理解 Redis 在系统中的位置)
      • [1.1.1 Redis 不是"另一种写法的 MySQL"](#1.1.1 Redis 不是“另一种写法的 MySQL”)
      • [1.1.2 为什么 Redis 通常很快](#1.1.2 为什么 Redis 通常很快)
      • [1.1.3 Redis 的常见应用场景](#1.1.3 Redis 的常见应用场景)
  • [2. 启动、连接与基础操作](#2. 启动、连接与基础操作)
    • [2.1 Redis 的运行模型](#2.1 Redis 的运行模型)
      • [2.1.1 客户端与服务端](#2.1.1 客户端与服务端)
      • [2.1.2 安装与启动](#2.1.2 安装与启动)
    • [2.2 配置文件中最需要关注的选项](#2.2 配置文件中最需要关注的选项)
      • [2.2.1 连接与进程配置](#2.2.1 连接与进程配置)
      • [2.2.2 不要把 Redis 直接暴露到公网](#2.2.2 不要把 Redis 直接暴露到公网)
    • [2.3 基本连接与数据库编号](#2.3 基本连接与数据库编号)
    • [2.4 键的基本管理](#2.4 键的基本管理)
      • [2.4.1 键命名建议](#2.4.1 键命名建议)
      • [2.4.2 判断和删除键](#2.4.2 判断和删除键)
      • [2.4.3 遍历键时不要使用 `KEYS *`](#2.4.3 遍历键时不要使用 KEYS *)
  • [3. Redis 的核心数据类型](#3. Redis 的核心数据类型)
    • [3.1 数据类型的总体认识](#3.1 数据类型的总体认识)
      • [3.1.1 键统一,值有多种结构](#3.1.1 键统一,值有多种结构)
    • [3.2 String:最基础也最常用的类型](#3.2 String:最基础也最常用的类型)
      • [3.2.1 基本读写](#3.2.1 基本读写)
      • [3.2.2 计数与条件写入](#3.2.2 计数与条件写入)
      • [3.2.3 String 常见使用场景](#3.2.3 String 常见使用场景)
    • [3.3 Hash:保存对象的字段](#3.3 Hash:保存对象的字段)
      • [3.3.1 常用命令](#3.3.1 常用命令)
      • [3.3.2 Hash 与 String 的选择](#3.3.2 Hash 与 String 的选择)
    • [3.4 List:顺序、重复与队列](#3.4 List:顺序、重复与队列)
      • [3.4.1 常用命令](#3.4.1 常用命令)
      • [3.4.2 List 的边界](#3.4.2 List 的边界)
    • [3.5 Set:去重与集合运算](#3.5 Set:去重与集合运算)
      • [3.5.1 常用命令](#3.5.1 常用命令)
      • [3.5.2 不要把 `SMEMBERS` 当作大数据遍历](#3.5.2 不要把 SMEMBERS 当作大数据遍历)
    • [3.6 Sorted Set:排序、分数和名次](#3.6 Sorted Set:排序、分数和名次)
      • [3.6.1 常用命令](#3.6.1 常用命令)
    • [3.7 其他常用高级类型](#3.7 其他常用高级类型)
      • [3.7.1 Bitmap](#3.7.1 Bitmap)
      • [3.7.2 HyperLogLog](#3.7.2 HyperLogLog)
      • [3.7.3 GEO](#3.7.3 GEO)
      • [3.7.4 Stream](#3.7.4 Stream)
  • [4. 过期时间与内存淘汰](#4. 过期时间与内存淘汰)
    • [4.1 TTL 的基本语义](#4.1 TTL 的基本语义)
      • [4.1.1 设置与查询过期时间](#4.1.1 设置与查询过期时间)
      • [4.1.2 过期不是精确倒计时](#4.1.2 过期不是精确倒计时)
    • [4.2 淘汰策略](#4.2 淘汰策略)
      • [4.2.1 区分过期与淘汰](#4.2.1 区分过期与淘汰)
      • [4.2.2 常见策略](#4.2.2 常见策略)
    • [4.3 大键与内存风险](#4.3 大键与内存风险)
  • [5. 持久化:Redis 重启后如何恢复数据](#5. 持久化:Redis 重启后如何恢复数据)
    • [5.1 RDB 快照](#5.1 RDB 快照)
      • [5.1.1 工作机制](#5.1.1 工作机制)
      • [5.1.2 RDB 的优缺点](#5.1.2 RDB 的优缺点)
    • [5.2 AOF 追加日志](#5.2 AOF 追加日志)
      • [5.2.1 工作机制](#5.2.1 工作机制)
      • [5.2.2 AOF 的优缺点](#5.2.2 AOF 的优缺点)
    • [5.3 RDB 与 AOF 如何配合](#5.3 RDB 与 AOF 如何配合)
  • [6. 事务、原子操作与 Lua](#6. 事务、原子操作与 Lua)
    • [6.1 Redis 事务的命令](#6.1 Redis 事务的命令)
    • [6.2 容易误解的"事务"](#6.2 容易误解的“事务”)
    • [6.3 用 Lua 完成条件原子操作](#6.3 用 Lua 完成条件原子操作)
  • [7. 缓存使用模式与常见问题](#7. 缓存使用模式与常见问题)
    • [7.1 Cache Aside:最常用的旁路缓存](#7.1 Cache Aside:最常用的旁路缓存)
      • [7.1.1 读流程](#7.1.1 读流程)
      • [7.1.2 写流程](#7.1.2 写流程)
    • [7.2 缓存穿透](#7.2 缓存穿透)
      • [7.2.1 现象](#7.2.1 现象)
      • [7.2.2 常用处理](#7.2.2 常用处理)
    • [7.3 缓存击穿](#7.3 缓存击穿)
      • [7.3.1 现象](#7.3.1 现象)
      • [7.3.2 常用处理](#7.3.2 常用处理)
    • [7.4 缓存雪崩](#7.4 缓存雪崩)
      • [7.4.1 现象](#7.4.1 现象)
      • [7.4.2 常用处理](#7.4.2 常用处理)
    • [7.5 缓存一致性](#7.5 缓存一致性)
  • [8. 分布式锁、会话与消息通信](#8. 分布式锁、会话与消息通信)
    • [8.1 用 `SET NX EX` 实现基本锁](#8.1 用 SET NX EX 实现基本锁)
    • [8.2 会话存储](#8.2 会话存储)
    • [8.3 Pub/Sub 与 Stream 的区别](#8.3 Pub/Sub 与 Stream 的区别)
      • [8.3.1 Pub/Sub](#8.3.1 Pub/Sub)
      • [8.3.2 Stream](#8.3.2 Stream)
  • [9. 简单实战与排错](#9. 简单实战与排错)
    • [9.1 一组可直接练习的命令](#9.1 一组可直接练习的命令)
      • [9.1.1 保存用户资料与过期会话](#9.1.1 保存用户资料与过期会话)
      • [9.1.2 计数器](#9.1.2 计数器)
      • [9.1.3 排行榜](#9.1.3 排行榜)
      • [9.1.4 标签集合](#9.1.4 标签集合)
    • [9.2 查看运行状态](#9.2 查看运行状态)
    • [9.3 常见错误清单](#9.3 常见错误清单)
      • [9.3.1 用 `KEYS *` 排查线上数据](#9.3.1 用 KEYS * 排查线上数据)
      • [9.3.2 只设置键,不设置过期时间](#9.3.2 只设置键,不设置过期时间)
      • [9.3.3 把 Redis 当成完全不会丢数据的主数据库](#9.3.3 把 Redis 当成完全不会丢数据的主数据库)
      • [9.3.4 锁释放不校验持有者](#9.3.4 锁释放不校验持有者)
      • [9.3.5 把 Pub/Sub 当作可靠队列](#9.3.5 把 Pub/Sub 当作可靠队列)
      • [9.3.6 所有对象都序列化成一个巨大 JSON](#9.3.6 所有对象都序列化成一个巨大 JSON)
  • [10. 本课总结与复习](#10. 本课总结与复习)
    • [10.1 一张图串起 Redis](#10.1 一张图串起 Redis)
    • [10.2 自测题](#10.2 自测题)
      • [10.2.1 Redis 为什么不能简单理解为"内存中的 MySQL"?](#10.2.1 Redis 为什么不能简单理解为“内存中的 MySQL”?)
      • [10.2.2 什么场景应选择 Hash,而不是 String?](#10.2.2 什么场景应选择 Hash,而不是 String?)
      • [10.2.3 `TTL` 返回 `-1` 和 `-2` 分别表示什么?](#10.2.3 TTL 返回 -1 和 -2 分别表示什么?)
      • [10.2.4 为什么缓存写入通常采用"更新数据库后删除缓存"?](#10.2.4 为什么缓存写入通常采用“更新数据库后删除缓存”?)
      • [10.2.5 Redis 事务会像 MySQL 一样在部分命令失败时完整回滚吗?](#10.2.5 Redis 事务会像 MySQL 一样在部分命令失败时完整回滚吗?)
      • [10.2.6 为什么不能把 Pub/Sub 当作可靠消息队列?](#10.2.6 为什么不能把 Pub/Sub 当作可靠消息队列?)


1. Redis 介绍与简单使用:从内存键值数据库到常见业务场景

Redis 是一个把数据放在内存中、以键值对为基本模型、同时提供多种数据结构的服务端数据库。它依靠内存访问、精简的数据结构和高效的网络模型获得远高于传统磁盘数据库的读写性能,常用于缓存、会话、计数、排行榜、分布式锁和消息通信,但它的持久化、容量和一致性边界同样需要在设计中认真考虑。

1.1 先理解 Redis 在系统中的位置

1.1.1 Redis 不是"另一种写法的 MySQL"

MySQL 以表、行、列、主键、外键和事务等关系模型组织数据,擅长按复杂条件查询,并用磁盘保证长期持久化。Redis 则以键值对为入口,把更多语义放进值的数据结构里,例如字符串、哈希、列表、集合和有序集合。

两者最常见的配合方式是:

text 复制代码
请求
  ↓
先查 Redis
  ├─ 命中:直接返回
  └─ 未命中:查询 MySQL
              ↓
            写回 Redis
              ↓
            返回结果

Redis 可以单独作为数据库使用,也可以作为缓存、消息通信组件或临时状态存储使用。但在强事务、复杂关联查询和长期事实数据等场景中,通常仍由关系型数据库承担核心数据职责。

1.1.2 为什么 Redis 通常很快

Redis 的性能来自多个因素共同作用,不能简单归结为"用了内存":

  1. 数据主要存放在内存中:内存随机访问延迟通常远低于机械磁盘或网络存储。
  2. 键值模型直接定位数据:大多数命令通过键即可找到目标,不需要解析复杂 SQL 再制定多表执行计划。
  3. 数据结构针对操作优化:列表、集合、有序集合等结构可以直接完成对应操作,不必把所有数据取到客户端处理。
  4. 命令执行路径短:Redis 的核心命令执行线程按顺序处理命令,减少了大量线程同步和切换成本。
  5. 网络 I/O 高效 :Redis 使用事件驱动和 I/O 多路复用;较新版本还支持多线程处理网络读写,但命令的真正执行仍保持清晰、原子化的语义。
    这里的"单线程"主要指核心命令执行模型,并不是说 Redis 进程从头到尾只有一个线程。持久化、异步释放、网络 I/O 和部分后台任务都可能使用额外线程。

1.1.3 Redis 的常见应用场景

场景 典型数据结构 说明
热点数据缓存 String、Hash 承担高频读请求,降低数据库压力
登录会话 String、Hash 保存会话状态并设置过期时间
计数器、限流 String、Hash INCR 等命令天然适合计数
排行榜 Sorted Set 按分数排序、获取名次和区间数据
标签、关系判断 Set 去重、交集、并集和差集
队列或异步任务 List、Stream List 适合简单队列,Stream 更适合需要消费组和确认机制的场景
分布式锁 String + 过期时间 借助 SET NX EX 和 Lua 脚本实现基本互斥
实时通知 Pub/Sub、Stream Pub/Sub 实时但离线消息会丢失,可靠消费应使用 Stream

选择 Redis 场景时,不应只问"它能不能做",还要问数据丢失代价、单键规模、访问模式、过期策略以及是否需要跨多个键保持原子性。

2. 启动、连接与基础操作

2.1 Redis 的运行模型

2.1.1 客户端与服务端

Redis 通常由一个 redis-server 进程监听 TCP 端口,默认端口是 6379。客户端通过 redis-cli、编程语言客户端或其他工具建立连接,再发送命令并接收结果。

text 复制代码
redis-cli / 应用客户端
        ↓ RESP 协议
redis-server:6379
        ↓
内存数据结构 + 持久化文件

Redis 使用 RESP 协议交换数据,协议本身简单,便于快速解析。客户端和服务端可以部署在同一台机器,也可以跨机器访问,但跨机器访问会把网络延迟纳入总耗时。

2.1.2 安装与启动

Ubuntu/Debian 环境可以使用包管理器安装:

bash 复制代码
sudo apt update
sudo apt install redis-server
sudo systemctl enable --now redis-server

Rocky/CentOS 一类系统可以使用:

bash 复制代码
sudo dnf install redis
sudo systemctl enable --now redis

也可以从源码编译并手动启动:

bash 复制代码
make
sudo make install
redis-server /path/to/redis.conf

课程实验首先要确认服务已经启动:

bash 复制代码
redis-cli -h 127.0.0.1 -p 6379 ping

正常返回:

text 复制代码
PONG

这里的 PING 只证明网络连接和 Redis 命令处理正常,不代表持久化配置、内存容量或安全策略都正确。

2.2 配置文件中最需要关注的选项

2.2.1 连接与进程配置

常见配置项包括:

ini 复制代码
bind 127.0.0.1
port 6379
daemonize yes
dir /var/lib/redis
dbfilename dump.rdb
  • bind 决定监听哪些网卡地址。
  • port 是服务端口。
  • daemonize 控制是否以守护进程方式运行;由 systemd 管理时通常不要手工再守护化。
  • dir 是持久化文件的工作目录。
  • dbfilename 是 RDB 文件名。

2.2.2 不要把 Redis 直接暴露到公网

Redis 保存的是应用状态,误暴露端口可能造成数据泄露、删除甚至远程执行风险。至少要遵守以下原则:

  1. 默认只监听 127.0.0.1 或内网地址。
  2. 通过防火墙限制来源网络。
  3. 配置密码或 Redis 6 之后的 ACL。
  4. 不把危险管理命令暴露给不可信客户端。
  5. 跨公网访问时使用 TLS、VPN 或网关,不直接开放默认端口。
    实验环境也应避免使用 0.0.0.0 加空密码的组合。

2.3 基本连接与数据库编号

默认配置通常会提供多个逻辑数据库:

bash 复制代码
redis-cli -h 127.0.0.1 -p 6379 -n 0

连接后可以执行:

text 复制代码
PING
SELECT 1
DBSIZE

SELECT 只是切换同一个 Redis 实例内的逻辑数据库,并不是切换一个独立服务器。不同逻辑数据库之间默认不能直接参与同一条多键命令;Redis Cluster 只使用数据库 0。

DBSIZE 返回当前逻辑数据库的键数量,通常比遍历全部键便宜。

FLUSHDB 会清空当前数据库,FLUSHALL 会清空所有逻辑数据库。它们是危险命令,只应在明确的数据清理实验中执行。

2.4 键的基本管理

2.4.1 键命名建议

推荐用有层次、可扫描、稳定的前缀,例如:

text 复制代码
user:1001:profile
user:1001:session
order:20261015:count
rank:game:daily:20261015

一个常用格式是:

text 复制代码
业务:对象:标识:属性

键名应表达业务含义,但不要把不断变化的数据直接拼进键名,否则会产生大量难以管理的键。

2.4.2 判断和删除键

text 复制代码
EXISTS user:1001:profile
TYPE user:1001:profile
DEL user:1001:profile
UNLINK user:1001:profile
  • EXISTS 返回键是否存在。
  • TYPE 返回值的数据类型。
  • DEL 同步删除键。
  • UNLINK 先从键空间解除引用,再把内存释放放到后台完成,适合删除大键时降低主线程阻塞。

2.4.3 遍历键时不要使用 KEYS *

KEYS pattern 会扫描整个键空间,键很多时可能长时间阻塞命令执行。在线环境通常使用增量遍历:

text 复制代码
SCAN 0 MATCH user:* COUNT 100

SCAN 返回一个游标,需要反复调用直到游标回到 0。它是弱一致遍历,能降低阻塞风险,但不保证一次完整快照。

3. Redis 的核心数据类型

3.1 数据类型的总体认识

3.1.1 键统一,值有多种结构

Redis 的最外层始终是"键 → 值",不同数据类型改变的是值如何组织:

类型 值的基本形态 最适合的问题
String 字节串、数字或序列化数据 单值缓存、计数、位操作
Hash 字段到值的映射 对象属性、局部更新
List 有顺序、可重复的列表 栈、队列、最近列表
Set 无序、去重的集合 标签、关系、交并差
Sorted Set 带分数的去重集合 排行榜、按分值范围查询
Bitmap 按位组织的字符串 签到、布尔状态
HyperLogLog 基数估算结构 大基数去重统计
GEO 带经纬度的有序集合 附近地点、距离计算
Stream 追加式消息流 消费组、消息确认

初学者最容易出现的误区是"能用一个大的 String 解决,就不考虑其他类型"。这通常会导致每次读取、修改和序列化整块数据,增加网络传输与命令耗时。

3.2 String:最基础也最常用的类型

3.2.1 基本读写

text 复制代码
SET user:1001:name "Alice"
GET user:1001:name
MSET user:1001:name "Alice" user:1001:city "Harbin"
MGET user:1001:name user:1001:city
STRLEN user:1001:name

GET 读取不存在的键时返回空值;客户端需要把"键不存在"和"键存在但值为空"区分开时,应结合 EXISTS 或缓存空值策略。

3.2.2 计数与条件写入

text 复制代码
SET article:100:views 0
INCR article:100:views
INCRBY article:100:views 10
DECR article:100:views
SETNX lock:job:1 owner-a

INCR 对数字字符串原子加一,因此可用于计数和简单限流。SETNX 只在键不存在时写入,可用于表示"第一次占用"。

设置值时同时指定过期时间,可以避免"设置了键却忘记设置过期时间":

text 复制代码
SET session:abc token-123 EX 1800
SET lock:order:100 owner-a NX EX 10

SET NX EX 是多条语义组合成的一次原子操作,比先 SETNX 再 EXPIRE 更可靠。

3.2.3 String 常见使用场景

  • 缓存序列化后的用户信息。
  • 保存验证码和会话令牌。
  • 保存页面计数、访问次数和限流计数。
  • 存储 JSON、Protobuf 等应用层序列化结果。
  • 通过 SETBIT、GETBIT 和 BITCOUNT 保存签到或布尔状态。
    String 的缺点是局部更新能力弱。一个很大的 JSON 字符串即使只修改一个字段,也可能需要读取全部内容,再整体写回。

3.3 Hash:保存对象的字段

3.3.1 常用命令

text 复制代码
HSET user:1001 name "Alice" age 20 city "Harbin"
HGET user:1001 name
HMGET user:1001 name age
HGETALL user:1001
HEXISTS user:1001 email
HDEL user:1001 city
HINCRBY user:1001 age 1

Hash 把多个字段放在同一个键下,适合字段数量有限、需要频繁读取或修改部分字段的对象。

3.3.2 Hash 与 String 的选择

需求 更合适的类型
整体读写一个对象 String
频繁更新对象中的少量字段 Hash
需要分别设置字段过期时间 通常拆成多个键
对象很大且字段很多 评估 Hash 规模,必要时拆分
需要跨对象聚合查询 通常应由数据库承担

Redis 的 Hash 不能像 JSON 那样任意嵌套。复杂嵌套结构通常仍需要应用层序列化,或者按业务读取方式重新拆分键。

3.4 List:顺序、重复与队列

3.4.1 常用命令

text 复制代码
RPUSH queue:mail "mail-1" "mail-2"
LPUSH queue:mail "mail-3"
LPOP queue:mail
RPOP queue:mail
LRANGE queue:mail 0 -1
LLEN queue:mail

从左侧写入、右侧读取,可以形成先进先出队列;从同一侧写入和读取,则更像栈。

阻塞读取适合作简单消费者:

text 复制代码
BLPOP queue:mail 5

它在列表为空时最多等待 5 秒,有数据则取出。相比客户端不断轮询,阻塞读取更节省资源。

3.4.2 List 的边界

  • 列表允许重复元素。
  • LRANGE 适合读取区间,不代表列表可以像数组一样随机 O(1) 访问。
  • 按值删除没有像 Set 那样直接的成员删除语义。
  • 简单队列可以依靠 List,但如果需要消费组、消息确认、失败重试和消息回溯,应使用 Stream。

3.5 Set:去重与集合运算

3.5.1 常用命令

text 复制代码
SADD article:100:tags mysql redis database
SISMEMBER article:100:tags redis
SCARD article:100:tags
SMEMBERS article:100:tags
SREM article:100:tags database
SINTER set:a set:b
SUNION set:a set:b
SDIFF set:a set:b

Set 的成员无序且自动去重,适合标签、点赞用户集合、共同关注和访问权限集合。

3.5.2 不要把 SMEMBERS 当作大数据遍历

集合很大时,SMEMBERS 会一次返回全部成员,可能造成网络和内存压力。只关心成员是否存在时使用 SISMEMBER;需要遍历大集合时考虑 SSCAN。

3.6 Sorted Set:排序、分数和名次

3.6.1 常用命令

text 复制代码
ZADD rank:game 80 alice 95 bob 70 carol
ZINCRBY rank:game 5 alice
ZREVRANGE rank:game 0 9 WITHSCORES
ZRANGE rank:game 0 -1 WITHSCORES
ZREVRANK rank:game alice
ZSCORE rank:game alice
ZRANGEBYSCORE rank:game 80 100
ZREM rank:game carol

Sorted Set 中的成员不重复,每个成员带一个分数。Redis 按分数排序,因此非常适合:

  • 游戏分数排行榜。
  • 热搜、销量和活跃度排名。
  • 按时间或权重筛选任务。
  • 延迟队列和滑动窗口的一部分实现。
    分数相同的时候,还会按成员的字典序形成稳定次序。业务如果要求完全自定义并列规则,需要把规则编码进分数或另行处理。

3.7 其他常用高级类型

3.7.1 Bitmap

Bitmap 使用字符串中的每一位保存布尔状态:

text 复制代码
SETBIT sign:user:1 15 1
GETBIT sign:user:1 15
BITCOUNT sign:user:1

它可以高效保存签到、开关和活跃状态,但前提是位编号有意义,且数据稀疏程度不会造成大量空洞。

3.7.2 HyperLogLog

text 复制代码
PFADD uv:20261015 user-a user-b user-c
PFCOUNT uv:20261015

HyperLogLog 用固定且较小的内存估算不同元素数量,适合 UV 统计。它返回近似值,不保存成员,因此不能回答"某用户是否已经访问过"。

3.7.3 GEO

Redis 的 GEO 能保存经纬度并计算距离、查询附近对象:

text 复制代码
GEOADD shops 126.63 45.75 "shop-a" 126.64 45.76 "shop-b"
GEODIST shops "shop-a" "shop-b" km
GEOSEARCH shops FROMMEMBER "shop-a" BYRADIUS 5 km ASC

它适合附近门店和位置检索的轻量需求,不替代具备完整空间索引和复杂几何计算能力的 GIS 系统。

3.7.4 Stream

Stream 是追加式消息流,支持消费组和待确认消息:

text 复制代码
XADD stream:orders * orderId 1001 status paid
XREAD COUNT 10 STREAMS stream:orders 0
XGROUP CREATE stream:orders group-a 0 MKSTREAM
XREADGROUP GROUP group-a consumer-1 COUNT 10 STREAMS stream:orders ">"

相比 Pub/Sub,Stream 能保存历史消息,但消息保留、确认、重试和清理仍需要业务策略。

4. 过期时间与内存淘汰

4.1 TTL 的基本语义

4.1.1 设置与查询过期时间

text 复制代码
SET code:13800000000 "123456" EX 300
TTL code:13800000000
PTTL code:13800000000
EXPIRE code:13800000000 60
PERSIST code:13800000000
  • EX 以秒设置过期时间。
  • PX 以毫秒设置过期时间。
  • TTL 返回剩余秒数。
  • PTTL 返回剩余毫秒数。
  • PERSIST 移除过期时间,让键长期存在。
    对于已存在的键,TTL 返回 -1 表示没有过期时间,返回 -2 表示键不存在。

4.1.2 过期不是精确倒计时

Redis 主要通过两种方式清理过期键:

  1. 访问键时发现已经过期,再删除,称为惰性删除。
  2. 后台周期性地抽样检查过期键并删除,称为主动过期。
    因此"到达过期时间"和"物理内存立刻释放"不是同一瞬间。有效语义是:过期后的键不应再被返回,但内存回收可能稍后完成。

4.2 淘汰策略

4.2.1 区分过期与淘汰

  • 过期:某个键达到了自己的 TTL。
  • 淘汰 :内存达到 maxmemory 后,Redis 为了继续写入而删除某些键。
    没有设置 TTL 的键不会被 volatile-* 策略淘汰,因为这些策略只从"有 TTL 的键"中选择。

4.2.2 常见策略

策略 候选范围与行为
noeviction 不淘汰;写命令可能返回内存不足错误
allkeys-lru 从所有键中优先淘汰最近最少使用的键
volatile-lru 只从有 TTL 的键中按 LRU 思路淘汰
allkeys-lfu 从所有键中优先淘汰访问频率低的键
volatile-lfu 只从有 TTL 的键中按 LFU 思路淘汰
allkeys-random 所有键中随机淘汰
volatile-random 有 TTL 的键中随机淘汰
volatile-ttl 有 TTL 的键中优先淘汰更早过期的键

缓存通常使用合适的 allkeys-* 策略;如果 Redis 中同时存放不能丢的关键状态,则必须谨慎配置,必要时把关键数据和缓存拆到不同实例。

4.3 大键与内存风险

一个包含几百万元素的 Hash、List、Set 或 Sorted Set,即使单次命令看起来简单,删除、复制、迁移或一次性读取都可能阻塞 Redis。

设计时要关注:

  • 单个值有多大。
  • 单个集合有多少成员。
  • 命令一次会返回多少数据。
  • 是否会在主线程中执行耗时删除。
  • 是否把无限增长的数据当成固定大小缓存。
    大集合应按业务维度拆分,读取使用分页或 SCAN,删除可考虑 UNLINK 或按块清理。

5. 持久化:Redis 重启后如何恢复数据

5.1 RDB 快照

5.1.1 工作机制

RDB 把某一时刻的数据压缩成快照文件。常见配置:

ini 复制代码
save 900 1
save 300 10
save 60 10000
dbfilename dump.rdb
dir /var/lib/redis

三个数字分别表示:满足一定秒数和一定写次数后触发快照。Redis 通常通过 fork 子进程生成快照,避免主线程直接长时间遍历全部数据。

5.1.2 RDB 的优缺点

优点 缺点
文件紧凑,适合备份和传输 两次快照之间的新写入可能丢失
恢复时加载速度快 数据量大时 fork 和快照有资源开销
适合灾难恢复和迁移 快照周期决定恢复点粒度

RDB 不能保证最后一次写入立即落盘。如果业务不能接受数分钟级别的数据丢失,只配置 RDB 是不够的。

5.2 AOF 追加日志

5.2.1 工作机制

AOF 把写命令记录下来,重启时通过重放命令恢复状态:

ini 复制代码
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec

appendfsync 的常见选择:

  • always:每次写入都同步,持久性更强,性能代价更大。
  • everysec:通常每秒同步一次,是持久性和性能之间的常见平衡,但仍可能丢失约一秒数据。
  • no:由操作系统决定何时刷盘,性能优先,丢数据风险更高。
    AOF 会随着写命令累积而变大,因此需要重写。重写的目标是用更少的命令表达等价的数据状态。

5.2.2 AOF 的优缺点

优点 缺点
记录粒度更细,通常丢数据更少 文件通常更大
便于理解最近发生的写操作 恢复时需要重放命令
everysec 兼顾性能与持久性 仍不是"绝不丢数据"

5.3 RDB 与 AOF 如何配合

生产环境通常可以根据恢复目标组合 RDB 与 AOF:

  • 只要快速备份和较大时间粒度的恢复,可使用 RDB。
  • 需要尽量减少数据丢失,应启用 AOF。
  • 两者同时启用时,Redis 重启会优先利用 AOF 恢复更完整的状态。
  • 较新版本还支持混合持久化和多部分 AOF,降低重写时的资源压力。
    无论采用哪种持久化,都必须实际备份持久化文件,并验证恢复流程。复制到另一台 Redis 实例不等于独立备份,因为误删除等逻辑错误也会被复制。

6. 事务、原子操作与 Lua

6.1 Redis 事务的命令

Redis 事务的基本命令是:

text 复制代码
MULTI
SET account:1:balance 100
SET account:2:balance 200
EXEC

执行过程:

  1. MULTI 开始收集命令。
  2. 后续命令先进入队列,不立即执行。
  3. EXEC 按顺序执行队列中的命令。
  4. DISCARD 取消事务。
  5. WATCH 可以监视键,若键在执行前被其他客户端修改,事务执行会失败或返回空结果,用于乐观并发控制。

6.2 容易误解的"事务"

Redis 事务保证队列中的命令连续执行,不被其他客户端的命令插入,但它不是关系型数据库中那种完整回滚事务。

例如命令本身在运行时才发现类型错误,已经执行成功的其他命令通常不会因为这条错误而自动回滚。因此 Redis 更适合把它理解为"命令批量顺序执行",复杂业务约束仍需应用层、Lua 或数据库配合。

6.3 用 Lua 完成条件原子操作

分布式锁释放必须"确认锁的持有者后删除",不能直接删除:

lua 复制代码
if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

这段脚本读取锁值并与客户端标识比较,只有匹配时才删除,从而避免误删其他客户端后来获得的锁。

更一般的原子逻辑------例如"库存大于零才扣减"------也可以放进 Lua,但脚本应短小、结果确定、避免长时间循环,并在集群环境中注意所有键应落在同一槽位。

7. 缓存使用模式与常见问题

7.1 Cache Aside:最常用的旁路缓存

7.1.1 读流程

text 复制代码
GET key
  ├─ 命中:返回 Redis 数据
  └─ 未命中:
       查询数据库
       命中数据库后写回 Redis,并设置 TTL
       返回数据库结果

示例命令:

text 复制代码
GET user:1001:profile
SET user:1001:profile "serialized-profile" EX 600

7.1.2 写流程

常见做法是"先更新数据库,再删除缓存":

text 复制代码
更新 MySQL
DEL user:1001:profile

下一次读取未命中时会重新加载最新数据。相比直接 SET 新值,删除缓存让后续读取以数据库为准,也能减少并发写入覆盖新值的概率。

即使如此,数据库更新和缓存删除之间仍有短暂窗口,缓存通常采用最终一致模型;强一致要求不能只靠 TTL 和删除缓存自然获得。

7.2 缓存穿透

7.2.1 现象

大量请求查询数据库中根本不存在的键,缓存永远无法命中,数据库不断承受无效查询。攻击者刻意查询不存在的用户 ID 时尤其明显。

7.2.2 常用处理

  • 对"不存在"的结果写入短 TTL 的空值标记。
  • 使用布隆过滤器提前判断对象是否可能存在。
  • 对接口做参数合法性校验和限流。
  • 对明显异常的高频查询进行风控。
    缓存空值可以快速实现,但需要短过期时间,避免真实数据后来创建后仍然被错误判为不存在。

7.3 缓存击穿

7.3.1 现象

一个热点键过期或暂时失效,大量并发请求同时发现未命中,再一起查询数据库。问题核心是"热点数据 + 同时重建"。

7.3.2 常用处理

  • 使用互斥锁,只让一个请求重建缓存,其余请求等待或返回旧值。
  • 热点数据不设固定短 TTL,或使用逻辑过期,由后台异步刷新。
  • 配合请求合并和限流,降低数据库瞬时压力。

7.4 缓存雪崩

7.4.1 现象

大量键在同一时间过期,或者 Redis 整体不可用,导致请求同时涌向数据库。

7.4.2 常用处理

  • 给 TTL 增加随机抖动,避免批量同时过期。
  • 使用高可用部署和合理的内存容量。
  • 对数据库查询做限流、降级和熔断。
  • 对关键数据准备多级缓存,并确保故障恢复后不会瞬间全量回源。

7.5 缓存一致性

缓存一致性的目标不是简单让两个存储永远同时变化,而是根据业务明确可接受的新旧程度和延迟:

  • 一般展示类数据允许短时间最终一致。
  • 金额、库存等关键数据需要明确事务边界和补偿机制。
  • 缓存只负责加速,不应成为唯一事实来源。
  • 通过版本号、消息队列或数据库日志同步时,必须处理消息重复、乱序和失败重试。
    只给缓存设置 TTL 并不能自动解决所有一致性问题,它只是限制了不一致状态最长可能持续多久。

8. 分布式锁、会话与消息通信

8.1 用 SET NX EX 实现基本锁

text 复制代码
SET lock:order:100 owner-a NX EX 10

含义是:

  • NX:键不存在时才设置。
  • EX 10:锁 10 秒后自动过期。
  • 值 owner-a:标识锁的持有者。
    释放时执行前文的比较后删除 Lua 脚本,不能只执行普通 DEL。
    还需要考虑:
  1. 业务执行时间可能超过锁租期。
  2. 服务暂停、主从切换和网络延迟可能导致锁状态变化。
  3. 短租期降低故障后长期死锁风险,但增加业务未完成就失锁的概率。
  4. 对严格互斥要求,应评估 Redlock 的争议、部署拓扑和业务补偿机制,而不是把 SET NX EX 当成万能事务。

8.2 会话存储

登录成功后,可以把会话信息写入 Redis:

text 复制代码
HSET session:abc userId 1001 role user
EXPIRE session:abc 1800

后续请求携带令牌或会话 ID,服务端从 Redis 取出对应用户信息。相比把状态全部放在单机内存,集中式 Redis 更容易支持多台应用服务器。

需要注意:

  • 清空 Redis 可能让所有用户退出登录。
  • 会话键应设置明确的过期时间。
  • 敏感信息应加密或只保存最小必要字段。
  • Redis 故障时,应用需要有降级、重新登录或短期本地校验策略。

8.3 Pub/Sub 与 Stream 的区别

8.3.1 Pub/Sub

发布者向频道发送消息,在线订阅者立即收到:

text 复制代码
SUBSCRIBE notice:system
PUBLISH notice:system "deploy started"

Pub/Sub 的优点是实时、命令简单;缺点是默认不保存历史,订阅者离线期间的发布消息会错过,也没有消费组确认语义。

8.3.2 Stream

Stream 把消息追加保存,可以有多个消费组、多个消费者和待确认列表,适合订单事件、异步任务和需要重试的轻量消息流。

Stream 仍需要设计消息保留长度、消费失败重试、死信处理和幂等。它不是完整的 Kafka 替代品,选型应看吞吐、保留策略、有序性和运维能力。

9. 简单实战与排错

9.1 一组可直接练习的命令

9.1.1 保存用户资料与过期会话

text 复制代码
HSET user:1001 name "Alice" age 20 city "Harbin"
HGETALL user:1001
EXPIRE user:1001 3600
TTL user:1001

9.1.2 计数器

text 复制代码
SET article:100:views 0
INCRBY article:100:views 10
GET article:100:views

9.1.3 排行榜

text 复制代码
ZADD rank:game 80 alice 95 bob 70 carol
ZREVRANGE rank:game 0 2 WITHSCORES
ZINCRBY rank:game 10 alice
ZREVRANK rank:game alice

9.1.4 标签集合

text 复制代码
SADD article:100:tags mysql redis database
SADD article:101:tags redis cache
SINTER article:100:tags article:101:tags

9.2 查看运行状态

text 复制代码
INFO server
INFO memory
INFO persistence
INFO stats
CONFIG GET maxmemory
CONFIG GET maxmemory-policy
SLOWLOG GET 10

常见观察指标包括:

  • used_memory:Redis 当前分配和使用的内存。
  • used_memory_human:便于阅读的内存大小。
  • maxmemory:配置的内存上限。
  • evicted_keys:因淘汰被删除的键数量。
  • keyspace_hits / keyspace_misses:缓存命中与未命中。
  • connected_clients:当前客户端连接数。
  • rdb_last_bgsave_status、aof_last_write_status:持久化状态。
  • slowlog:执行时间超过阈值的命令。
    排错时不要只看"Redis 是否 PING 得通",还要检查内存是否达到上限、是否存在大键、是否发生淘汰、持久化是否失败和网络是否出现超时。

9.3 常见错误清单

9.3.1 用 KEYS * 排查线上数据

键数量很大时会阻塞服务。使用 SCAN,并控制返回数量。

9.3.2 只设置键,不设置过期时间

缓存、验证码、会话和临时锁如果没有 TTL,长期运行后会不断堆积,最终触发内存压力或误删关键键。

9.3.3 把 Redis 当成完全不会丢数据的主数据库

Redis 可以持久化,但 RDB 快照周期、AOF everysec、节点故障和主从切换都可能产生数据窗口。关键数据必须有数据库、日志和恢复策略。

9.3.4 锁释放不校验持有者

业务执行超时后锁可能已被其他客户端获得,直接 DEL 会误删新锁。释放锁必须比较持有者标识,并通过原子脚本删除。

9.3.5 把 Pub/Sub 当作可靠队列

离线期间的 Pub/Sub 消息默认会丢失,也不支持消费确认。可靠消息场景应评估 Stream 或专用消息系统。

9.3.6 所有对象都序列化成一个巨大 JSON

如果业务只修改一个字段,却反复读取和写回整个大对象,会产生不必要的网络和 CPU 开销。应根据访问模式选择 Hash 或拆分键。

10. 本课总结与复习

10.1 一张图串起 Redis

text 复制代码
客户端
  ↓ RESP 命令
Redis 服务端
  ↓
内存中的键值数据结构
  ├─ String / Hash / List / Set / ZSet
  ├─ Bitmap / HyperLogLog / GEO / Stream
  ├─ TTL 与内存淘汰
  └─ RDB / AOF 持久化
  ↓
缓存、会话、计数、排行、锁、消息通信

10.2 自测题

10.2.1 Redis 为什么不能简单理解为"内存中的 MySQL"?

因为 Redis 的核心模型是键值和多种数据结构,常用命令围绕键定位与结构操作展开;MySQL 的核心是关系表、约束、复杂查询和事务。两者可以配合,但不能直接画等号。

10.2.2 什么场景应选择 Hash,而不是 String?

对象字段数量有限、需要频繁读取或修改部分字段时,Hash 可以避免反复读取、序列化和写回整个对象。

10.2.3 TTL 返回 -1 和 -2 分别表示什么?

对已存在的键,-1 表示没有设置过期时间;-2 表示键不存在。

10.2.4 为什么缓存写入通常采用"更新数据库后删除缓存"?

删除缓存后,后续读取会从数据库重新加载,减少并发写时直接把旧值写回缓存的风险。但数据库更新与缓存删除之间仍有短暂不一致窗口。

10.2.5 Redis 事务会像 MySQL 一样在部分命令失败时完整回滚吗?

通常不会。Redis 事务保证命令连续执行,但运行期错误不会自动撤销已经成功执行的命令,因此不能把它当成完整的关系型事务。

10.2.6 为什么不能把 Pub/Sub 当作可靠消息队列?

Pub/Sub 默认不保存离线期间的消息,也没有消费组确认与重试机制。需要可靠消费时应评估 Stream 或专用消息中间件。

#Redis #数据库 #缓存 #键值数据库 #数据结构 #持久化

相关推荐
ss2732 小时前
30 PostgreSQL ACID特性详解
数据库·postgresql
syagain_zsx2 小时前
ODB 与 MySQL:C++ 里的 ORM 实践(类即表、事务铁律与查询封装)
开发语言·数据库·c++·mysql·工具库·1024程序员节
时间售卖铺2 小时前
MySQL 备份
运维·数据库·mysql·云原生
靠谱的工程师朋友3 小时前
电子会计档案管理软件科普 核心能力与选型要点解析
网络·数据库·人工智能·电子会计档案
北冥you鱼3 小时前
Go 语言 Ticker 定时任务全解析:原理、陷阱与最佳实践
开发语言·数据库·golang
用户094248568033 小时前
第8章:Redis Bitmap 与 BitField 实战:签到、活跃用户与状态压缩
redis·架构·源码
花间相见3 小时前
【计算基础|MySQL17】三大日志:undo / redo / binlog
数据库·后端·面试
严同学正在努力3 小时前
PostgreSQL 数据库安装部署教程(Linux 多发行版 + Windows + Docker 全覆盖)
linux·数据库·postgresql
众链网络3 小时前
年卡跨淡旺季的有效期模型:不可用日期、次数与顺延规则
java·前端·数据库