Redis 缓存技术实战教程
面向零基础用户的完整 Redis 学习指南,从入门到实战
目录
- Redis 是什么
- 安装与配置
- 五大基本数据类型
- 3.1 String(字符串)
- 3.2 List(列表)
- 3.3 Hash(哈希)
- 3.4 Set(集合)
- 3.5 Sorted Set(有序集合)
- 持久化机制
- 主从复制
- 哨兵模式
- 集群搭建
- 缓存策略
- 分布式锁
- 消息队列
- 实战:秒杀系统缓存设计
- 性能优化与最佳实践
1. Redis 是什么
Redis(Remote Dictionary Server)是一个开源的、基于内存的键值对数据库。与传统数据库不同,Redis 将数据存储在内存中,因此读写速度极快,单机即可达到每秒数万甚至数十万次操作。
Redis 的核心特点
- 极速:数据存储在内存中,读写延迟通常在微秒级别
- 丰富的数据结构:不仅支持简单的 Key-Value,还支持 String、List、Hash、Set、Sorted Set 等
- 持久化:支持 RDB 和 AOF 两种方式将内存数据保存到磁盘
- 高可用:支持主从复制、哨兵模式和集群模式
- 原子操作:所有操作都是原子性的,支持事务和 Lua 脚本
Redis 的典型应用场景
| 场景 | 说明 |
|---|---|
| 缓存 | 热点数据缓存,减轻数据库压力 |
| 会话管理 | 存储用户 Session |
| 排行榜 | 使用 Sorted Set 实现实时排行 |
| 计数器 | 点赞数、浏览量等原子计数 |
| 分布式锁 | 跨服务的互斥访问控制 |
| 消息队列 | 使用 List 或 Stream 实现简单消息队列 |
| 限流 | 接口访问频率限制 |
| 社交关系 | 关注/粉丝/共同好友 |
2. 安装与配置
2.1 在 Linux 上安装(以 Ubuntu 为例)
# 更新包管理器
sudo apt update
# 安装 Redis
sudo apt install redis-server -y
# 启动 Redis 服务
sudo systemctl start redis-server
# 设置开机自启
sudo systemctl enable redis-server
# 检查运行状态
sudo systemctl status redis-server
2.2 使用 Docker 安装(推荐开发环境)
# 拉取并运行 Redis 容器
docker run -d \
--name redis \
-p 6379:6379 \
-v /data/redis:/data \
redis:7-alpine
# 进入 Redis CLI
docker exec -it redis redis-cli
2.3 从源码编译安装
# 下载源码
wget https://download.redis.io/redis-stable.tar.gz
tar xzf redis-stable.tar.gz
cd redis-stable
# 编译
make
# 安装到系统路径
sudo make install
# 启动
redis-server
2.4 核心配置文件详解
Redis 配置文件通常位于 /etc/redis/redis.conf,以下是关键配置项:
# ========== 基础配置 ==========
# 绑定地址(默认只允许本地访问)
bind 127.0.0.1
# 保护模式(生产环境建议开启)
protected-mode yes
# 监听端口
port 6379
# 后台运行
daemonize yes
# 日志级别:debug / verbose / notice / warning
loglevel notice
logfile /var/log/redis/redis-server.log
# 数据库数量(0-15)
databases 16
# ========== 内存配置 ==========
# 最大内存限制
maxmemory 256mb
# 内存淘汰策略
maxmemory-policy allkeys-lru
# ========== 安全配置 ==========
# 访问密码
requirepass your_strong_password_here
# ========== 网络配置 ==========
# 客户端超时时间(秒),0 表示不超时
timeout 300
# 最大客户端连接数
maxclients 10000
2.5 连接与测试
# 连接本地 Redis
redis-cli
# 连接远程 Redis
redis-cli -h 192.168.1.100 -p 6379 -a your_password
# 测试连通性
127.0.0.1:6379> PING
PONG
# 查看服务器信息
127.0.0.1:6379> INFO server
# 查看当前数据库 key 数量
127.0.0.1:6379> DBSIZE
(integer) 0
3. 五大基本数据类型
Redis 的强大之处在于其丰富的数据类型。下面逐一讲解每种类型的用法和适用场景。
3.1 String(字符串)
String 是 Redis 最基础的数据类型,可以存储字符串、整数或浮点数。一个 String 类型的 value 最大可存储 512MB。
基本操作
# 设置键值
SET name "张三"
# 获取值
GET name
# 输出: "张三"
# 设置带过期时间的键(秒)
SET token "abc123" EX 3600
# 设置带过期时间的键(毫秒)
SET token "abc123" PX 3600000
# 仅当 key 不存在时设置(分布式锁常用)
SETNX lock "holder_1"
# 设置值并返回旧值
SET counter 100
GETSET counter 200
# 输出: "100",此时 counter 的值为 200
# 批量设置
MSET k1 "v1" k2 "v2" k3 "v3"
# 批量获取
MGET k1 k2 k3
# 删除键
DEL name
# 检查键是否存在
EXISTS name
# 设置过期时间(秒)
EXPIRE name 60
# 查看剩余过期时间(秒)
TTL name
# 输出: (integer) 55(剩余秒数)
# 输出: (integer) -1(永不过期)
# 输出: (integer) -2(已过期/不存在)
数值操作
# 初始化计数器
SET views 0
# 自增
INCR views
# 输出: (integer) 1
INCR views
# 输出: (integer) 2
# 指定增量
INCRBY views 10
# 输出: (integer) 12
# 自减
DECR views
# 输出: (integer) 11
# 指定减量
DECRBY views 5
# 输出: (integer) 6
# 浮点数增加
SET price 10.5
INCRBYFLOAT price 0.5
# 输出: "11"
字符串操作
# 追加字符串
SET greeting "Hello"
APPEND greeting " World"
# 输出: (integer) 11
# greeting 的值为 "Hello World"
# 获取字符串长度
STRLEN greeting
# 输出: (integer) 11
# 截取子串(闭区间,从0开始)
GETRANGE greeting 0 4
# 输出: "Hello"
# 替换子串
SETRANGE greeting 6 "Redis"
# greeting 的值为 "Hello Redis"
实战场景
# 场景1:接口限流(每分钟最多100次请求)
SET rate_limit:user:1001 0 EX 60 NX
INCR rate_limit:user:1001
# 场景2:分布式ID生成
INCR global:order_id
# 每次调用都会返回一个递增的唯一数字
# 场景3:缓存用户信息(JSON格式)
SET user:1001 '{"name":"张三","age":25,"city":"北京"}' EX 7200
3.2 List(列表)
List 是一个双向链表,支持从两端插入和弹出元素,非常适合实现队列和栈。
基本操作
# 从左侧(头部)插入
LPUSH queue "task1" "task2" "task3"
# 列表顺序: task3 -> task2 -> task1
# 从右侧(尾部)插入
RPUSH queue "task4"
# 列表顺序: task3 -> task2 -> task1 -> task4
# 查看列表长度
LLEN queue
# 输出: (integer) 4
# 获取指定范围的元素(闭区间)
LRANGE queue 0 -1
# 输出: 1) "task3" 2) "task2" 3) "task1" 4) "task4"
# 获取指定索引的元素(从0开始)
LINDEX queue 0
# 输出: "task3"
# 设置指定索引的值
LSET queue 0 "task_new"
# 从左侧弹出(取头部并移除)
LPOP queue
# 输出: "task3"
# 从右侧弹出(取尾部并移除)
RPOP queue
# 输出: "task4"
# 阻塞弹出(列表为空时等待,最多等30秒)
BLPOP queue 30
# 裁剪列表(只保留指定范围)
LTRIM queue 0 1
高级操作
# 在指定元素前插入
LPUSHX queue "urgent_task"
# 移除指定元素(LREM key count value)
# count > 0: 从头到尾移除 count 个值为 value 的元素
# count < 0: 从尾到头移除 |count| 个值为 value 的元素
# count = 0: 移除所有值为 value 的元素
RPUSH mylist "a" "b" "a" "c" "a"
LREM mylist 2 "a"
# 移除从头开始的 2 个 "a"
# 将元素从一个列表移到另一个(原子操作)
RPOPLPUSH source_list dest_list
# 阻塞版的 RPOPLPUSH(用于可靠队列)
BRPOPLPUSH source_list dest_list 30
实战场景
# 场景1:简单消息队列(生产者-消费者模式)
# 生产者
LPUSH mq:orders '{"orderId":"1001","amount":99.9}'
# 消费者(阻塞等待)
BRPOP mq:orders 0
# 场景2:最近浏览记录(只保留最近10条)
LPUSH recent:views:user1001 "product:5001"
LTRIM recent:views:user1001 0 9
# 场景3:任务队列(可靠处理)
# 将任务放入处理队列
LPUSH tasks:pending '{"task":"send_email","to":"user@example.com"}'
# 处理时先移到进行中队列
RPOPLPUSH tasks:pending tasks:processing
# 处理完成后从进行中队列移除
LREM tasks:processing 1 '{"task":"send_email","to":"user@example.com"}'
3.3 Hash(哈希)
Hash 是一个键值对集合,特别适合存储对象。相比用 String 存储 JSON,Hash 可以只修改某个字段而无需读取整个对象。
基本操作
# 设置单个字段
HSET user:1001 name "张三"
HSET user:1001 age 25
HSET user:1001 city "北京"
# 批量设置多个字段
HMSET user:1002 name "李四" age 30 city "上海"
# 获取单个字段
HGET user:1001 name
# 输出: "张三"
# 获取多个字段
HMGET user:1001 name age city
# 输出: 1) "张三" 2) "25" 3) "北京"
# 获取所有字段和值
HGETALL user:1001
# 输出: 1) "name" 2) "张三" 3) "age" 4) "25" 5) "city" 6) "北京"
# 获取所有字段名
HKEYS user:1001
# 获取所有值
HVALS user:1001
# 获取字段数量
HLEN user:1001
# 输出: (integer) 3
# 检查字段是否存在
HEXISTS user:1001 name
# 输出: (integer) 1
# 删除字段
HDEL user:1001 city
数值操作
# 字段值自增(整数)
HSET stats:article:1001 views 0
HINCRBY stats:article:1001 views 1
# 输出: (integer) 1
HINCRBY stats:article:1001 views 100
# 输出: (integer) 101
# 字段值自增(浮点数)
HSET product:1001 price 99.9
HINCRBYFLOAT product:1001 price -10.0
# 输出: "89.9"
实战场景
# 场景1:存储用户信息(比 String JSON 更灵活)
HMSET user:1001 \
name "张三" \
email "zhangsan@example.com" \
phone "13800138000" \
vip_level 3 \
register_time "2024-01-15"
# 只更新 VIP 等级,不影响其他字段
HSET user:1001 vip_level 4
# 场景2:购物车(用户ID -> 商品ID -> 数量)
HSET cart:user1001 product:5001 2
HSET cart:user1001 product:5002 1
# 增加商品数量
HINCRBY cart:user1001 product:5001 1
# 查看购物车所有商品
HGETALL cart:user1001
# 删除某个商品
HDEL cart:user1001 product:5002
# 场景3:文章计数器
HMSET article:1001:stats \
views 0 \
likes 0 \
comments 0 \
shares 0
3.4 Set(集合)
Set 是无序且不重复的元素集合,支持集合运算(交集、并集、差集)。
基本操作
# 添加元素
SADD tags:article:1001 "Redis" "缓存" "数据库" "性能"
# 查看所有成员
SMEMBERS tags:article:1001
# 输出: 1) "Redis" 2) "缓存" 3) "数据库" 4) "性能"
# 检查元素是否存在
SISMEMBER tags:article:1001 "Redis"
# 输出: (integer) 1
# 获取集合大小
SCARD tags:article:1001
# 输出: (integer) 4
# 随机获取元素
SRANDMEMBER tags:article:1001 2
# 随机返回 2 个元素(不删除)
# 随机弹出元素
SPOP tags:article:1001
# 随机弹出并移除 1 个元素
# 移除元素
SREM tags:article:1001 "性能"
集合运算
# 准备数据
SADD user:1001:followers "1002" "1003" "1004" "1005"
SADD user:1002:followers "1001" "1003" "1005" "1006"
# 交集(共同关注)
SINTER user:1001:followers user:1002:followers
# 输出: 1) "1003" 2) "1005"
# 并集(所有关注的人)
SUNION user:1001:followers user:1002:followers
# 差集(user:1001 关注但 user:1002 没关注的)
SDIFF user:1001:followers user:1002:followers
# 输出: 1) "1002" 2) "1004"
# 将交集结果存储到新集合
SINTERSTORE common:1001:1002 user:1001:followers user:1002:followers
实战场景
# 场景1:点赞功能
SADD likes:article:1001 "user:1001"
SADD likes:article:1001 "user:1002"
# 检查是否已点赞
SISMEMBER likes:article:1001 "user:1001"
# 取消点赞
SREM likes:article:1001 "user:1001"
# 获取点赞数
SCARD likes:article:1001
# 场景2:标签系统(查找共同标签的文章)
SADD tag:Redis "article:1001" "article:1003"
SADD tag:缓存 "article:1001" "article:1002"
SINTER tag:Redis tag:缓存
# 输出: "article:1001"(同时有 Redis 和缓存标签的文章)
# 场景3:抽奖系统
SADD lottery:2024 "user:1001" "user:1002" "user:1003" "user:1004"
# 随机抽取 2 个中奖者(不重复)
SRANDMEMBER lottery:2024 2
# 抽取并移除(确保不重复中奖)
SPOP lottery:2024
3.5 Sorted Set(有序集合)
Sorted Set(ZSet)在 Set 的基础上为每个元素关联一个分数(score),元素按分数排序。这是 Redis 最强大的数据类型之一,广泛用于排行榜、延迟队列等场景。
基本操作
# 添加元素(带分数)
ZADD leaderboard 100 "player:A"
ZADD leaderboard 85 "player:B"
ZADD leaderboard 92 "player:C"
ZADD leaderboard 78 "player:D"
ZADD leaderboard 95 "player:E"
# 批量添加
ZADD leaderboard 88 "player:F" 76 "player:G"
# 按分数从低到高排序
ZRANGE leaderboard 0 -1 WITHSCORES
# 输出:
# 1) "player:D"
# 2) "78"
# 3) "player:G"
# 4) "76"
# ...
# 按分数从高到低排序(排行榜常用)
ZREVRANGE leaderboard 0 2 WITHSCORES
# 输出前3名:
# 1) "player:A" 2) "100"
# 3) "player:E" 4) "95"
# 5) "player:C" 6) "92"
# 获取元素的分数
ZSCORE leaderboard "player:A"
# 输出: "100"
# 获取元素的排名(从0开始,按分数从低到高)
ZRANK leaderboard "player:A"
# 输出: (integer) 4(第5名)
# 获取元素的排名(按分数从高到低)
ZREVRANK leaderboard "player:A"
# 输出: (integer) 0(第1名)
# 获取集合大小
ZCARD leaderboard
# 输出: (integer) 7
# 分数自增
ZINCRBY leaderboard 5 "player:B"
# player:B 的分数从 85 变为 90
# 删除元素
ZREM leaderboard "player:G"
范围查询
# 按分数范围查询(80-100分的玩家)
ZRANGEBYSCORE leaderboard 80 100 WITHSCORES
# 按分数范围查询(从高到低)
ZREVRANGEBYSCORE leaderboard 100 80 WITHSCORES
# 统计分数范围内的元素数量
ZCOUNT leaderboard 80 100
# 输出: (integer) 5
# 按字典序范围查询(分数相同时)
ZRANGEBYLEX leaderboard [player:A [player:C
实战场景
# 场景1:实时排行榜
ZADD rank:game:daily 1500 "user:1001"
ZADD rank:game:daily 2300 "user:1002"
ZADD rank:game:daily 1800 "user:1003"
# 获取 Top 10
ZREVRANGE rank:game:daily 0 9 WITHSCORES
# 获取用户排名
ZREVRANK rank:game:daily "user:1001"
# 场景2:延迟队列(定时任务)
# 添加延迟任务,score 为执行时间戳
ZADD delay_queue 1706246400 '{"task":"send_reminder","userId":1001}'
ZADD delay_queue 1706246500 '{"task":"expire_order","orderId":"ORD001"}'
# 获取到期任务(当前时间戳之前的所有任务)
ZRANGEBYSCORE delay_queue 0 1706246450
# 场景3:滑动窗口限流
# 记录请求时间戳
ZADD rate:user:1001 1706246400.123 "req1"
ZADD rate:user:1001 1706246400.456 "req2"
# 统计最近60秒的请求数
ZREMRANGEBYSCORE rate:user:1001 0 1706246340
ZCARD rate:user:1001
4. 持久化机制
Redis 数据存储在内存中,一旦进程退出,数据就会丢失。持久化机制就是将内存数据保存到磁盘,以便重启后恢复数据。
4.1 RDB 快照
RDB(Redis Database)通过生成内存数据的快照文件(dump.rdb)来持久化数据。
工作原理
Redis fork 出一个子进程,子进程将内存中的数据写入一个临时 RDB 文件,写入完成后替换旧的 RDB 文件。主进程在 fork 期间继续处理请求,不影响服务。
配置方式
# 在 redis.conf 中配置
# 自动触发条件(满足任意一个即触发 BGSAVE)
# 格式: save <seconds> <changes>
save 900 1 # 900秒内至少1次修改
save 300 10 # 300秒内至少10次修改
save 60 10000 # 60秒内至少10000次修改
# 禁用 RDB(如果只用 AOF)
# save ""
# RDB 文件名
dbfilename dump.rdb
# RDB 文件保存目录
dir /var/lib/redis
# 压缩 RDB 文件
rdbcompression yes
# RDB 文件校验
rdbchecksum yes
手动触发
# 同步保存(阻塞主线程,生产环境慎用)
SAVE
# 后台异步保存(推荐)
BGSAVE
# 查看最后一次保存的时间
LASTSAVE
优缺点
| 优点 | 缺点 |
|---|---|
| 文件紧凑,适合备份和灾难恢复 | 可能丢失最后一次快照后的数据 |
| 恢复速度快 | fork 子进程时内存占用翻倍 |
| 对性能影响较小 | 数据量大时 fork 耗时较长 |
4.2 AOF 追加日志
AOF(Append Only File)以日志追加的方式记录每一个写操作命令。
工作原理
每当有写命令执行时,Redis 将该命令追加到 AOF 文件末尾。重启时 Redis 重新执行 AOF 文件中的命令来恢复数据。
配置方式
# 开启 AOF
appendonly yes
# AOF 文件名
appendfilename "appendonly.aof"
# 同步策略
# always: 每次写命令都同步(最安全,性能最差)
# everysec: 每秒同步一次(推荐,平衡安全与性能)
# no: 由操作系统决定何时同步(性能最好,安全性最低)
appendfsync everysec
# AOF 重写时不执行 fsync(避免阻塞)
no-appendfsync-on-rewrite no
# 自动重写触发条件
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
手动重写
# 手动触发 AOF 重写(压缩 AOF 文件)
BGREWRITEAOF
优缺点
| 优点 | 缺点 |
|---|---|
| 数据安全性高,最多丢失1秒数据 | AOF 文件通常比 RDB 文件大 |
| 可读性好,可手动修改 | 恢复速度比 RDB 慢 |
| 自动重写机制控制文件大小 | 重写期间可能影响性能 |
4.3 混合持久化
Redis 4.0+ 支持混合持久化,结合 RDB 和 AOF 的优势。
# 开启混合持久化
aof-use-rdb-preamble yes
AOF 重写时,前半部分写入 RDB 格式的全量数据,后半部分追加 AOF 格式的增量命令。恢复时先加载 RDB 部分(速度快),再重放 AOF 部分(数据完整)。
5. 主从复制
主从复制(Master-Slave Replication)是 Redis 实现高可用的基础。主节点负责写操作,从节点复制主节点的数据并提供读服务。
5.1 架构说明
客户端 ──写──> Master
│
┌─────┼─────┐
│ │ │
▼ ▼ ▼
Slave1 Slave2 Slave3
读 读 读
5.2 配置方式
方式一:配置文件
在从节点的 redis.conf 中添加:
# 指定主节点地址和端口
replicaof 192.168.1.100 6379
# 如果主节点设置了密码
masterauth your_master_password
# 从节点只读(默认开启)
replica-read-only yes
方式二:命令行动态配置
# 在从节点上执行
127.0.0.1:6380> REPLICAOF 192.168.1.100 6379
OK
# 取消复制,提升为主节点
127.0.0.1:6380> REPLICAOF NO ONE
OK
5.3 查看复制状态
# 在主节点查看
127.0.0.1:6379> INFO replication
# 输出关键信息:
# role:master
# connected_slaves:2
# slave0:ip=192.168.1.101,port=6380,state=online,offset=12345,lag=0
# slave1:ip=192.168.1.102,port=6381,state=online,offset=12345,lag=1
# 在从节点查看
127.0.0.1:6380> INFO replication
# 输出关键信息:
# role:slave
# master_host:192.168.1.100
# master_port:6379
# master_link_status:up
5.4 复制原理
- 全量复制:从节点首次连接主节点时,主节点生成 RDB 快照发送给从节点,同时将期间的写命令缓存在缓冲区中,RDB 传输完成后发送缓冲区的命令
- 增量复制:全量复制后,主节点持续将写命令发送给从节点,保持数据同步
- 断线重连:Redis 2.8+ 支持部分重同步(PSYNC),通过复制偏移量和积压缓冲区避免全量复制
6. 哨兵模式
哨兵(Sentinel)模式在主从复制的基础上实现了自动故障转移。当主节点宕机时,哨兵会自动将一个从节点提升为新的主节点。
6.1 架构说明
Sentinel1 Sentinel2 Sentinel3
│ │ │
└────┬─────┘──────────┘
│ 监控
▼
Master (6379)
┌────┼────┐
▼ ▼ ▼
Slave1 Slave2 Slave3
6.2 配置哨兵
创建 sentinel.conf 文件:
# 哨兵监听端口
port 26379
# 后台运行
daemonize yes
# 日志文件
logfile "/var/log/redis/sentinel.log"
# 监控的主节点
# 格式: sentinel monitor <master-name> <ip> <port> <quorum>
# quorum: 需要多少个哨兵同意才能执行故障转移
sentinel monitor mymaster 192.168.1.100 6379 2
# 主节点密码
sentinel auth-pass mymaster your_master_password
# 主节点无响应超时时间(毫秒)
sentinel down-after-milliseconds mymaster 5000
# 故障转移超时时间(毫秒)
sentinel failover-timeout mymaster 60000
# 同时进行复制的从节点数量
sentinel parallel-syncs mymaster 1
6.3 启动与管理
# 启动哨兵
redis-sentinel /path/to/sentinel.conf
# 或者使用 redis-server
redis-server /path/to/sentinel.conf --sentinel
# 查看哨兵状态
redis-cli -p 26379 SENTINEL master mymaster
# 查看主节点下的从节点
redis-cli -p 26379 SENTINEL replicas mymaster
# 查看当前的主节点地址
redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster
# 手动触发故障转移
redis-cli -p 26379 SENTINEL failover mymaster
6.4 客户端连接方式
应用程序不直接连接主节点,而是先询问哨兵获取当前主节点地址:
# Python 示例(使用 redis-py)
from redis.sentinel import Sentinel
sentinel = Sentinel([
('192.168.1.101', 26379),
('192.168.1.102', 26379),
('192.168.1.103', 26379),
])
# 获取主节点连接(自动故障转移)
master = sentinel.master_for('mymaster', socket_timeout=0.5)
master.set('key', 'value')
# 获取从节点连接(读操作)
slave = sentinel.slave_for('mymaster', socket_timeout=0.5)
value = slave.get('key')
7. 集群搭建
Redis Cluster 是 Redis 的分布式解决方案,支持数据分片和高可用。
7.1 架构说明
Redis Cluster 将所有数据划分为 16384 个哈希槽(Hash Slot),分布在多个节点上。
Client
│
├── Node A (slots 0-5460) ← Master
│ └── Node A1 ← Slave
│
├── Node B (slots 5461-10922) ← Master
│ └── Node B1 ← Slave
│
└── Node C (slots 10923-16383) ← Master
└── Node C1 ← Slave
7.2 搭建集群(6 节点,3主3从)
准备配置文件
为每个节点创建配置文件(以节点1为例):
# node1.conf
port 7001
cluster-enabled yes
cluster-config-file nodes-7001.conf
cluster-node-timeout 5000
appendonly yes
daemonize yes
logfile "redis-7001.log"
dir /data/redis/7001
依次创建 7001-7006 共 6 个配置文件。
启动所有节点
redis-server node1.conf
redis-server node2.conf
redis-server node3.conf
redis-server node4.conf
redis-server node5.conf
redis-server node6.conf
创建集群
# Redis 5+ 使用 redis-cli 创建集群
# --cluster-replicas 1 表示每个主节点配一个从节点
redis-cli --cluster create \
127.0.0.1:7001 \
127.0.0.1:7002 \
127.0.0.1:7003 \
127.0.0.1:7004 \
127.0.0.1:7005 \
127.0.0.1:7006 \
--cluster-replicas 1
7.3 集群操作
# 连接集群(-c 表示集群模式)
redis-cli -c -p 7001
# 查看集群信息
127.0.0.1:7001> CLUSTER INFO
# 查看集群节点
127.0.0.1:7001> CLUSTER NODES
# 查看 key 属于哪个槽
127.0.0.1:7001> CLUSTER KEYSLOT mykey
# 使用 Hash Tag 确保相关 key 在同一个槽
# 只有 {} 中的部分用于计算槽位
SET {user:1001}:name "张三"
SET {user:1001}:age 25
# 这两个 key 会在同一个槽中
7.4 集群扩容与缩容
# 添加新主节点
redis-cli --cluster add-node 127.0.0.1:7007 127.0.0.1:7001
# 为新节点分配槽位(从现有节点迁移部分槽)
redis-cli --cluster reshard 127.0.0.1:7001
# 添加从节点
redis-cli --cluster add-node 127.0.0.1:7008 127.0.0.1:7001 \
--cluster-slave --cluster-master-id <master-node-id>
# 移除节点(先迁走槽位)
redis-cli --cluster del-node 127.0.0.1:7001 <node-id>
8. 缓存策略
合理使用缓存策略可以最大化 Redis 的价值,避免常见问题。
8.1 缓存穿透
问题:查询一个一定不存在的数据,缓存和数据库都没有,每次请求都打到数据库。
解决方案:
# 方案1:缓存空值
# 查询数据库未命中时,将空值缓存并设置较短过期时间
SET user:99999 "" EX 60
# 方案2:布隆过滤器(Redis 模块支持)
# 在缓存前加一层布隆过滤器,快速判断 key 是否可能存在
BF.ADD user_filter "user:1001"
BF.EXISTS user_filter "user:99999"
# 返回 0 表示一定不存在,直接返回
8.2 缓存击穿
问题:某个热点 key 过期的瞬间,大量请求同时打到数据库。
解决方案:
# 方案1:互斥锁
# 用 SETNX 实现分布式锁,只允许一个请求重建缓存
SET lock:hot_key "1" NX EX 10
# 获取锁成功 → 查数据库 → 写缓存 → 释放锁
# 获取锁失败 → 等待后重试读缓存
# 方案2:逻辑过期
# 不设置物理过期时间,在 value 中记录逻辑过期时间
SET hot_key '{"data":"...","expire_at":1706246400}'
# 读取时检查逻辑过期时间
# 已过期 → 异步更新缓存,同时返回旧数据
8.3 缓存雪崩
问题:大量缓存同时过期或 Redis 宕机,请求全部打到数据库。
解决方案:
# 方案1:随机过期时间(避免同时过期)
# 基础过期时间 + 随机偏移
SET key1 "value1" EX 3600 # 基础3600秒
SET key2 "value2" EX 3623 # 3600 + 23
SET key3 "value3" EX 3587 # 3600 - 13
# 方案2:多级缓存
# L1: 本地缓存(进程内,如 Caffeine/Guava Cache)
# L2: Redis 分布式缓存
# L3: 数据库
# 方案3:熔断降级
# 缓存不可用时,直接返回默认值或降级页面
8.4 缓存更新策略
| 策略 | 说明 | 适用场景 |
|---|---|---|
| Cache Aside | 先更新数据库,再删除缓存 | 通用场景,推荐使用 |
| Read/Write Through | 缓存层统一代理读写 | 有缓存中间件时 |
| Write Behind | 只更新缓存,异步写数据库 | 写多读少,允许少量数据丢失 |
# Cache Aside 模式伪代码
# 读操作:
# 1. 先读缓存
# 2. 缓存命中 → 直接返回
# 3. 缓存未命中 → 读数据库 → 写入缓存 → 返回
# 写操作:
# 1. 先更新数据库
# 2. 再删除缓存(而非更新缓存)
9. 分布式锁
在分布式系统中,多个服务实例需要互斥访问共享资源时,需要分布式锁。
9.1 基于 SETNX 实现
# 加锁(原子操作:设置值 + 设置过期时间)
SET lock:order:1001 "holder_id_12345" NX EX 10
# NX: 只有 key 不存在时才设置(互斥)
# EX 10: 10秒后自动过期(防止死锁)
9.2 安全释放锁(Lua 脚本)
直接 DEL 删除锁可能误删别人的锁。使用 Lua 脚本保证只有持有者才能释放:
# 释放锁的 Lua 脚本
EVAL "
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
" 1 lock:order:1001 "holder_id_12345"
9.3 锁续期(看门狗机制)
对于耗时较长的任务,需要在锁过期前自动续期:
# 使用 Redisson(Java)等客户端自带的看门狗机制
# 手动续期的 Lua 脚本
EVAL "
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('PEXPIRE', KEYS[1], ARGV[2])
else
return 0
end
" 1 lock:order:1001 "holder_id_12345" 10000
9.4 Redlock 算法
单个 Redis 实例的分布式锁在主从切换时可能失效。Redlock 算法通过多个独立 Redis 实例来提高可靠性:
1. 获取当前时间 T1
2. 依次向 N 个独立 Redis 实例请求加锁(设置较短超时)
3. 计算加锁耗时 = 当前时间 T2 - T1
4. 如果在 N/2+1 个以上实例加锁成功,且耗时 < 锁的有效期 → 加锁成功
5. 锁的有效期 = 初始有效期 - 加锁耗时
6. 如果加锁失败,向所有实例发送释放锁请求
10. 消息队列
Redis 可以作为轻量级消息队列使用,适合对可靠性要求不那么极端的场景。
10.1 基于 List 的消息队列
# 生产者:发送消息
LPUSH queue:email '{"to":"user@example.com","subject":"Hello","body":"..."}'
LPUSH queue:email '{"to":"admin@example.com","subject":"Alert","body":"..."}'
# 消费者:阻塞读取(BRPOP 会阻塞直到有消息或超时)
BRPOP queue:email 30
# 输出: 1) "queue:email"
# 2) "{\"to\":\"admin@example.com\",\"subject\":\"Alert\",\"body\":\"...\"}"
10.2 基于 Pub/Sub 的发布订阅
# 订阅者(在终端1执行)
SUBSCRIBE channel:news
# 发布者(在终端2执行)
PUBLISH channel:news "Redis 7.0 正式发布!"
PUBLISH channel:news "新功能:Function 支持"
# 订阅模式匹配
PSUBSCRIBE channel:*
# 取消订阅
UNSUBSCRIBE channel:news
10.3 基于 Stream 的消息队列(Redis 5.0+)
Stream 是 Redis 5.0 引入的专门用于消息队列的数据结构,支持消费者组和消息确认。
# 添加消息
XADD mystream * name "张三" action "login"
# 输出: "1706246400000-0"(消息ID)
XADD mystream * name "李四" action "purchase" amount "99.9"
# 读取消息(从头开始)
XRANGE mystream - +
# 读取最新消息(阻塞等待)
XREAD COUNT 10 BLOCK 5000 STREAMS mystream $
# 创建消费者组
XGROUP CREATE mystream mygroup $ MKSTREAM
# 消费者组消费消息
XREADGROUP GROUP mygroup consumer1 COUNT 1 BLOCK 5000 STREAMS mystream >
# 确认消息已处理
XACK mystream mygroup 1706246400000-0
# 查看消费者组信息
XINFO GROUPS mystream
# 查看待处理消息(未确认的消息)
XPENDING mystream mygroup
# 查看 Stream 信息
XINFO STREAM mystream
# 设置 Stream 最大长度(自动裁剪)
XADD mystream MAXLEN 1000 * name "王五" action "logout"
11. 实战:秒杀系统缓存设计
下面以一个电商秒杀系统为例,展示如何综合运用 Redis 解决实际问题。
11.1 需求分析
- 秒杀商品库存有限(如 100 件)
- 瞬时并发极高(数万请求/秒)
- 要求:不超卖、高性能、用户体验好
11.2 整体架构
用户请求 → Nginx → 应用服务 → Redis(库存扣减 + 用户去重)
│
└→ 消息队列 → 订单服务 → 数据库
11.3 Redis 缓存设计
库存预热
# 秒杀开始前,将库存加载到 Redis
SET seckill:stock:1001 100
# 记录商品信息
HMSET seckill:info:1001 \
name "iPhone 15 Pro" \
original_price "8999" \
seckill_price "6999" \
start_time "1706332800" \
stock 100
原子库存扣减(Lua 脚本)
# 使用 Lua 脚本保证原子性:检查库存 + 检查重复购买 + 扣减库存
EVAL "
local stock_key = 'seckill:stock:' .. ARGV[1]
local users_key = 'seckill:users:' .. ARGV[1]
local user_id = ARGV[2]
-- 检查用户是否已购买
if redis.call('SISMEMBER', users_key, user_id) == 1 then
return -1 -- 已购买,返回-1
end
-- 检查库存
local stock = tonumber(redis.call('GET', stock_key))
if stock == nil or stock <= 0 then
return 0 -- 库存不足,返回0
end
-- 扣减库存
redis.call('DECR', stock_key)
-- 记录用户已购买
redis.call('SADD', users_key, user_id)
-- 设置用户记录过期时间(24小时)
redis.call('EXPIRE', users_key, 86400)
return 1 -- 成功,返回1
" 0 "1001" "user:1001"
投递消息到队列
# 抢购成功后,发送消息到订单队列
XADD seckill:orders * \
user_id "user:1001" \
product_id "1001" \
seckill_price "6999" \
timestamp "1706332801"
11.4 完整流程代码(Python 伪代码)
import redis
import time
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
# Lua 脚本:原子扣减库存
STOCK_LUA = """
local stock_key = 'seckill:stock:' .. ARGV[1]
local users_key = 'seckill:users:' .. ARGV[1]
local user_id = ARGV[2]
if redis.call('SISMEMBER', users_key, user_id) == 1 then
return -1
end
local stock = tonumber(redis.call('GET', stock_key))
if stock == nil or stock <= 0 then
return 0
end
redis.call('DECR', stock_key)
redis.call('SADD', users_key, user_id)
redis.call('EXPIRE', users_key, 86400)
return 1
"""
stock_script = r.register_script(STOCK_LUA)
def seckill(product_id, user_id):
# 1. 原子扣减库存
result = stock_script(args=[product_id, user_id])
if result == -1:
return {"code": 400, "msg": "您已抢购过该商品"}
elif result == 0:
return {"code": 400, "msg": "库存不足,已被抢光"}
else:
# 2. 发送订单消息到 Stream
r.xadd('seckill:orders', {
'user_id': user_id,
'product_id': product_id,
'timestamp': str(time.time())
})
return {"code": 200, "msg": "抢购成功,请在30分钟内完成支付"}
def order_consumer():
"""订单消费者:从 Stream 消费消息,创建订单"""
while True:
messages = r.xreadgroup(
'order_group', 'consumer1',
{'seckill:orders': '>'},
count=1, block=5000
)
if messages:
for stream, entries in messages:
for msg_id, fields in entries:
try:
# 创建订单(写入数据库)
create_order(fields)
# 确认消息
r.xack('seckill:orders', 'order_group', msg_id)
except Exception as e:
print(f"处理失败: {e}")
11.5 关键设计要点
- 库存预热:秒杀开始前将库存加载到 Redis,避免穿透到数据库
- 原子操作:使用 Lua 脚本保证"检查库存 + 用户去重 + 扣减库存"的原子性
- 用户去重:用 Set 记录已购买用户,防止同一用户重复抢购
- 异步下单:抢购成功后通过消息队列异步创建订单,快速返回结果
- 限流保护:在 Nginx 层或应用层做请求限流,过滤无效请求
- 兜底方案:即使 Redis 不可用,也要有降级策略(如直接返回"系统繁忙")
12. 性能优化与最佳实践
12.1 Key 命名规范
# 推荐格式: 业务:对象:ID:属性
user:1001:profile
user:1001:session
order:2024:1001:detail
product:1001:stock
# 避免的命名
userinfo # 太模糊
user1001profile # 无分隔符,难以阅读
my.key.name # 使用了错误的分隔符(点号可能被部分客户端特殊处理)
12.2 大 Key 问题
# 查找大 Key
redis-cli --bigkeys
# 扫描指定类型的大 Key
redis-cli --bigkeys --i 0.1
# 使用 MEMORY USAGE 查看单个 key 的内存占用
MEMORY USAGE user:1001
# 大 Key 拆分策略:
# Hash 太大 → 按字段分片: user:1001:basic, user:1001:extra
# Set/ZSet 太大 → 按范围分片: tags:page:1, tags:page:2
# String 太大 → 压缩或拆分
12.3 Pipeline 批量操作
# 使用 Pipeline 减少网络往返
# 普通模式:N 次请求 = N 次网络往返
# Pipeline 模式:N 次请求 = 1 次网络往返
# redis-cli 中使用 Pipeline
echo -e "SET key1 val1\nSET key2 val2\nSET key3 val3" | redis-cli --pipe
12.4 内存优化
# 查看内存使用情况
INFO memory
# 使用 OBJECT 命令查看 key 的编码
OBJECT ENCODING mykey
# 小对象使用 ziplist 编码更省内存
# 配置 ziplist 的阈值
hash-max-ziplist-entries 128
hash-max-ziplist-value 64
list-max-ziplist-size -2
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
# 整数集合(intset)优化
set-max-intset-entries 512
12.5 连接池配置
# Python redis-py 连接池示例
import redis
pool = redis.ConnectionPool(
host='localhost',
port=6379,
password='your_password',
max_connections=50, # 最大连接数
decode_responses=True,
socket_timeout=5, # 读写超时
socket_connect_timeout=2, # 连接超时
retry_on_timeout=True, # 超时时重试
)
r = redis.Redis(connection_pool=pool)
12.6 监控与运维
# 实时监控命令(生产环境慎用,影响性能)
MONITOR
# 查看慢查询日志
SLOWLOG GET 10
SLOWLOG LEN
SLOWLOG RESET
# 配置慢查询阈值(微秒)
CONFIG SET slowlog-log-slower-than 10000 # 10ms
# 查看客户端连接
CLIENT LIST
# 查看实时统计
INFO stats
# 查看键空间命中率
INFO keyspace
# 关注: keyspace_hits 和 keyspace_misses
# 命中率 = hits / (hits + misses),应保持在 90% 以上
附录:常用命令速查表
| 操作 | 命令 | 说明 |
|---|---|---|
| 通用 | KEYS pattern |
查找匹配的 key(生产环境慎用) |
| 通用 | SCAN cursor MATCH pattern COUNT 100 |
安全的迭代扫描 |
| 通用 | TYPE key |
查看 key 的类型 |
| 通用 | TTL key |
查看剩余过期时间 |
| 通用 | PERSIST key |
移除过期时间 |
| 通用 | RENAME key newkey |
重命名 key |
| 通用 | DBSIZE |
当前数据库 key 数量 |
| 通用 | FLUSHDB |
清空当前数据库(危险!) |
| 通用 | FLUSHALL |
清空所有数据库(危险!) |
| 服务器 | INFO [section] |
查看服务器信息 |
| 服务器 | CONFIG GET parameter |
获取配置 |
| 服务器 | CONFIG SET parameter value |
修改配置(运行时) |
| 服务器 | CONFIG REWRITE |
将运行时配置写回文件 |
| 服务器 | BGSAVE |
后台保存 RDB |
| 服务器 | BGREWRITEAOF |
后台重写 AOF |
| 服务器 | DEBUG SLEEP 0 |
测试连接延迟 |
总结
本教程从零基础出发,系统地讲解了 Redis 的核心知识:
- 基础:Redis 的特点、安装配置、CLI 使用
- 数据类型:String、List、Hash、Set、Sorted Set 五大类型的用法和适用场景
- 持久化:RDB、AOF、混合持久化的原理和配置
- 高可用:主从复制、哨兵模式、集群搭建
- 实战技巧:缓存策略(穿透/击穿/雪崩)、分布式锁、消息队列
- 综合项目:秒杀系统的完整缓存设计
Redis 是一个功能强大且灵活的工具,关键在于根据业务场景选择合适的数据类型和策略。建议在本地搭建 Redis 环境,动手实践每一个命令和示例,加深理解。
持续学习:Redis 官方文档 https://redis.io/docs/ 是最好的参考资料,建议收藏并经常查阅。