一、Redis 概述
Redis(Remote Dictionary Server)是一个基于内存的高性能键值数据库,在 Linux 环境下通常作为缓存、分布式锁、消息队列和实时统计等场景的核心组件。它支持丰富的数据结构,并提供持久化、主从复制、事务等能力。以下内容以 Linux 环境为背景,围绕 Redis 的核心知识点进行系统整理,适合作为个人复习资料使用。
Redis 官方通常以 Linux 作为主要测试与部署环境,因此在复习时需要重点关注 Linux 下的安装启动、配置文件 redis.conf、命令行工具 redis-cli 以及进程守护方式。
1.1 Redis 在 Linux 下的安装与启动
在 Linux 环境下部署 Redis 主要有两种方式:通过包管理器快速安装,或通过源码编译安装。源码编译安装更便于控制版本和编译参数,也是学习 Redis 启动流程、systemd 托管方式和配置文件的最佳路径。下面以 Ubuntu/Debian 系发行版为例进行说明。
1.1.1 源码编译安装
首先安装编译依赖,然后下载官方源码包、解压、编译并安装到指定目录:
bash
# 1. 安装编译依赖
sudo apt update
sudo apt install -y build-essential tcl
2. 下载并解压 Redis 源码包
wget https://download.redis.io/releases/redis-7.2.5.tar.gz
tar -xzf redis-7.2.5.tar.gz
cd redis-7.2.5
3. 编译并安装
make -j$(nproc)
sudo make install PREFIX=/usr/local
编译安装完成后,可执行文件会安装到 /usr/local/bin,核心文件包括 redis-server、redis-cli、redis-benchmark 和 redis-check-rdb 等。
1.1.2 systemd 服务配置
生产环境或作为长期复习环境,建议使用 systemd 托管 Redis 进程。创建服务单元文件 /etc/systemd/system/redis.service,示例配置如下:
bash
[Unit]
Description=Redis In-Memory Data Store
After=network.target
[Service]
Type=simple
User=redis
Group=redis
ExecStart=/usr/local/bin/redis-server /etc/redis/redis.conf
ExecStop=/usr/local/bin/redis-cli shutdown
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
写入后依次加载配置、设置开机自启并启动服务:
bash
sudo systemctl daemon-reload
sudo systemctl enable redis
sudo systemctl start redis
sudo systemctl status redis
注意:使用 systemd 托管时,redis.conf 中的 daemonize 通常应设置为 no,由 systemd 负责前台进程管理;如果 daemonize yes,systemd 可能无法正确跟踪进程状态。
1.1.3 redis-cli 连接测试
启动成功后,可通过 redis-cli 验证连接和读写能力:
bash
# 最简单的方式:本机、默认端口 6379
redis-cli ping
# 期望返回:PONG
写入并读取一个测试 key
redis-cli set test "hello redis"
redis-cli get test
期望返回:"hello redis"
指定主机、端口和密码连接
redis-cli -h 127.0.0.1 -p 6379 -a yourpassword ping
查看服务器基础信息
redis-cli INFO server
1.1.4 redis.conf 关键配置项说明
redis.conf 是 Redis 的核心配置文件。以下是与安装启动和基础运维紧密相关的关键配置项:
| 配置项 | 示例值 | 作用说明 |
|---|---|---|
| bind | 127.0.0.1 或 0.0.0.0 | 指定监听的网卡地址;多个地址用空格分隔 |
| protected-mode | yes / no | 保护模式;未设置 bind 和密码时默认拒绝外部连接 |
| port | 6379 | Redis 服务监听端口 |
| daemonize | yes / no | 是否以守护进程后台运行;systemd 托管时建议 no |
| dir | /var/lib/redis | RDB、AOF 文件的保存目录 |
| logfile | /var/log/redis/redis.log | 日志文件路径;留空表示输出到标准输出 |
| appendonly | yes / no | 是否开启 AOF 持久化 |
| requirepass | yourpassword | 客户端访问密码;设置后连接需使用 -a 参数或 AUTH 命令 |
| maxmemory | 512mb | 最大使用内存;达到上限后根据淘汰策略处理写入 |
| maxmemory-policy | allkeys-lru | 内存淘汰策略;缓存场景常使用 allkeys-lru |
1.1.5 常见启动报错及排查方法
启动 Redis 或连接失败时,优先查看启动命令输出、systemd 状态和日志文件。以下是常见报错与排查思路:
| 典型报错 | 可能原因 | 排查方法 |
|---|---|---|
| Could not create server TCP listening socket *:6379: bind: Address already in use | 端口 6379 已被占用,或 Redis 已经启动 | 执行 ss -lntp | grep 6379 查看占用进程,停掉旧进程或修改 port |
| FATAL CONFIG FILE ERROR | redis.conf 语法错误或路径不存在 | 检查最近修改的配置项,确认配置文件路径正确,必要时逐行排查 |
| Can't open the log file: Permission denied | 运行用户对日志目录没有写权限 | 将 logfile 指向可写目录,或为 redis 用户授权日志目录 |
| Redis is configured to save RDB snapshots, but it's currently unable to persist to disk | 磁盘空间不足,或 dir 目录权限不足 | 检查 df -h 磁盘空间和 dir 目录权限,清理空间或修改 dir |
| Protected mode enabled, no bind set, no password specified | 保护模式阻止外部客户端连接 | 按需配置 bind、requirepass,或将 protected-mode 设为 no |
| jemalloc/jemalloc.h: No such file or directory | 编译依赖缺失或源码包解压不完整 | 安装 build-essential,使用完整源码包,执行 make distclean 后重新编译 |
| Failed to start redis.service: Unit redis.service not found | systemd 单元文件路径错误或未加载 | 检查 /etc/systemd/system/redis.service 是否存在,执行 systemctl daemon-reload |
二、五种基本数据类型
Redis 的五种基本数据类型分别为 String、Hash、List、Set、Sorted Set。它们的共同点是以 key-value 形式存储,但 value 的内部编码和操作命令不同。
2.1 String(字符串)
**特性:**String 是最基础的类型,key 对应一个字符串,最大可存储 512MB。它既可以存储普通文本,也可以存储数字、二进制数据、JSON 序列化后的对象等。
常用命令:
bash
SET key value
GET key
MSET key1 value1 key2 value2
MGET key1 key2
INCR key
DECR key
APPEND key value
SETEX key seconds value
SETNX key value
**使用场景:**缓存对象、计数器、分布式 ID、限流计数、Session 存储、简单分布式锁。
**性能特点:**String 使用简单动态字符串实现,常见 GET/SET 操作的时间复杂度为 O(1)。当保存整数时,可使用 INCR/DECR 原子地完成自增自减,避免并发问题。
**注意事项:**不要频繁存储大对象并整段更新;如果 value 较大,应评估网络传输和内存占用;数字运算必须在原值可被解析为整数时进行,否则会报错。
2.2 Hash(哈希)
**特性:**Hash 是一个 string 类型的 field 和 value 映射表,适合存储对象。相比把对象序列化后存入 String,Hash 支持按字段读写,粒度更细。
常用命令:
bash
HSET key field value
HGET key field
HMSET key field1 value1 field2 value2
HMGET key field1 field2
HGETALL key
HKEYS key
HVALS key
HDEL key field1 field2
HINCRBY key field increment
**使用场景:**用户信息、商品详情、购物车、配置项分组存储、需要按字段更新的对象缓存。
**性能特点:**哈希在元素较少时使用紧凑结构存储,元素较多时转为哈希表。单字段读写复杂度为 O(1),HGETALL 的复杂度与字段数量成正比。
**注意事项:**当字段非常多时,避免频繁 HGETALL 拉取全部字段;缓存过期机制通常只作用于整个 key,无法对单个 field 单独设置过期时间。
2.3 List(列表)
**特性:**List 是一个有序、可重复的字符串链表,可以从左右两端插入或弹出元素。它常被用作简单的队列或栈。
常用命令:
bash
LPUSH key value1 value2
RPUSH key value1 value2
LPOP key
RPOP key
LRANGE key start stop
LINDEX key index
LLEN key
LTRIM key start stop
BLPOP key timeout
BRPOP key timeout
**使用场景:**消息队列、最新消息列表、时间线、延迟任务、商品评论列表等。
**性能特点:**在链表头尾插入和弹出元素的时间复杂度为 O(1),但按索引访问中间元素为 O(N)。阻塞命令 BLPOP/BRPOP 可用于实现消费者等待队列。
**注意事项:**List 并不是严格的消息队列,缺乏确认机制、重复消费控制和复杂路由能力;实际高可靠消息场景应结合 Redis Stream 或其他消息中间件。
2.4 Set(集合)
**特性:**Set 是 String 类型的无序集合,元素不可重复,支持交、并、差等集合运算。
常用命令:
bash
SADD key member1 member2
SREM key member1
SMEMBERS key
SISMEMBER key member
SCARD key
SINTER key1 key2
SUNION key1 key2
SDIFF key1 key2
SPOP key
SRANDMEMBER key count
**使用场景:**标签系统、共同关注、共同好友、抽奖去重、独立访客统计、黑白名单、集合关系分析。
**性能特点:**加入、删除、判断成员是否存在等操作的时间复杂度通常为 O(1)。集合运算的复杂度取决于参与集合的大小。
**注意事项:**集合元素无序,不能依赖遍历顺序;当集合非常大时,SMEMBERS 或集合运算可能阻塞较长时间,应优先考虑使用 SSCAN 或拆分 key。
2.5 Sorted Set(有序集合)
**特性:**Sorted Set 在 Set 的基础上为每个 member 绑定一个 score,根据 score 进行排序。score 可以重复,member 必须唯一。
常用命令:
bash
ZADD key score1 member1 score2 member2
ZRANGE key start stop
ZREVRANGE key start stop
ZRANGEBYSCORE key min max
ZCOUNT key min max
ZREM key member
ZINCRBY key increment member
ZRANK key member
ZREVRANK key member
ZSCORE key member
**使用场景:**排行榜、热搜榜、延迟队列、按时间排序的 Feed、带权重的标签排序、分页查询。
**性能特点:**Sorted Set 通过跳跃表和哈希表实现,添加、删除、更新分数的时间复杂度通常为 O(logN),按排名范围查询效率较高。
**注意事项:**当 member 数量巨大时,ZADD 和范围查询的内存与 CPU 消耗会增加;需要区分按 score 查询和按排名查询,两者排序规则不同。
三、Redis 持久化机制
Redis 是基于内存的数据库,断电或进程退出后数据会丢失,因此需要通过持久化机制把内存数据写入磁盘。Redis 提供 RDB、AOF 两种主要持久化方式,并在较新版本中支持混合持久化。
3.1 RDB(Redis Database)
**工作原理:**RDB 通过生成某一时刻全量数据的快照文件 dump.rdb 来实现持久化。触发方式包括手动执行 SAVE/BGSAVE 命令,以及配置 save 规则自动触发。BGSAVE 会 fork 一个子进程,由子进程负责写入快照,主进程继续处理请求,不会长时间阻塞。
常用配置:
bash
# redis.conf 中 RDB 相关配置示例
save 900 1
save 300 10
save 60 10000
dbfilename dump.rdb
dir /var/lib/redis
**优点:**文件紧凑,适合备份和灾难恢复;恢复大数据量时速度较快;主进程通过 fork 子进程写入,对读写性能影响相对较小。
**缺点:**两次快照之间发生故障会丢失最近一段时间的数据;fork 子进程时如果数据量很大,可能造成短暂停顿;快照写入期间也可能占用较多磁盘 I/O。
**适用场景:**对数据完整性要求不是极端严格、允许分钟级数据丢失、需要定期冷备份和快速恢复的场景。
3.2 AOF(Append Only File)
**工作原理:**AOF 以追加写的方式记录 Redis 接收到的每一条写命令,重启时通过重新执行这些命令恢复数据。AOF 文件可以通过 appendfsync 控制刷盘策略,同时支持 BGREWRITEAOF 进行日志重写,压缩冗余命令。
常用配置:
bash
# redis.conf 中 AOF 相关配置示例
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
**优点:**数据安全性更高,默认 everysec 策略最多可能丢失约 1 秒的数据;AOF 写入采用追加方式,写入性能较稳定;日志相对易于理解和修复。
**缺点:**相同数据量下 AOF 文件通常比 RDB 大;恢复速度通常慢于 RDB;写入频繁时对性能和磁盘 I/O 有一定影响。
**适用场景:**对数据丢失容忍度低、需要较高数据安全性的场景,例如订单、账户信息等关键数据。
3.3 RDB 与 AOF 对比
| 对比维度 | RDB | AOF |
|---|---|---|
| 持久化方式 | 全量快照 | 追加写命令日志 |
| 文件体积 | 通常较小 | 通常较大 |
| 数据安全性 | 可能丢失最近一段时间数据 | 默认最多丢失约 1 秒数据 |
| 恢复速度 | 快 | 相对慢 |
| 资源消耗 | fork 时可能短暂停顿 | 持续写盘带来一定 I/O 压力 |
| 适用场景 | 冷备份、快速恢复、允许少量丢失 | 关键数据、低丢失容忍 |
**注意事项:**同时开启 RDB 和 AOF 时,Redis 重启会优先使用 AOF 文件恢复数据。Redis 4.0 之后还可以开启混合持久化,在 AOF 重写时使用 RDB 格式作为前缀,加快恢复速度。生产环境应结合业务对数据丢失的容忍度选择组合策略,并定期演练恢复流程。
四、Redis 锁机制
Redis 常用于实现分布式场景下的锁机制,主要分为基于版本号的乐观锁和基于 SETNX 的悲观锁两类。
4.1 乐观锁:基于版本号
**实现原理:**乐观锁假设冲突不会频繁发生,在读取数据时同时获取一个版本号,或使用 Redis 事务中的 WATCH 命令。提交更新前检查版本号是否改变,如果已改变说明其他客户端修改过数据,则放弃或重试。
示例:
bash
# 客户端 A 和客户端 B 同时修改库存
WATCH stock:1001
GET stock:1001
# 客户端 A 计算出新库存后提交事务
MULTI
SET stock:1001 49
EXEC
# 如果 stock:1001 在 WATCH 之后被修改,EXEC 返回 nil,客户端需要重试
**使用场景:**读多写少、冲突概率低、希望用重试换取更高并发的场景,例如库存扣减、配置更新等。
**优点:**不加锁,读操作不被阻塞,并发吞吐较高。
**缺点:**冲突时需要应用层重试,重试会带来额外开销;在写冲突频繁时可能造成大量无效尝试。
**注意事项:**WATCH 需要在 MULTI 之前调用;UNWATCH 可以取消监视;乐观锁需要应用自己编写重试逻辑,不适合需要严格互斥的场景。
4.2 悲观锁:基于 SETNX
**实现原理:**悲观锁假设冲突必然可能发生,访问共享资源前必须先获得锁。Redis 中常使用 SETNX(SET if Not eXists)尝试创建锁 key,只有第一个客户端能设置成功,其他客户端进入等待或快速失败。
示例:
bash
# 获取锁,value 使用唯一标识,便于安全释放
SET lock:order:1001 client-001 NX PX 30000
释放锁时需要验证 value 是否属于自己,避免误删他人锁
可通过 Lua 脚本保证判断和删除的原子性
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
**使用场景:**写多并发高、需要强互斥的关键资源,例如订单防重、任务抢占、分布式定时任务调度等。
**优点:**能提供明确的互斥语义,业务逻辑简单直观。
**缺点:**需要处理锁超时、锁失效和误删问题;如果持锁任务执行时间超过锁过期时间,可能出现锁提前释放导致的并发安全问题。
**注意事项:**锁 key 应设置过期时间,避免死锁;释放锁前必须校验 value 是否是自身标识;对于锁续期需求,可以结合 Redisson 等客户端提供的看门狗机制,或使用 Lua 脚本扩展过期时间。
五、Redis 事务
Redis 事务允许把多个命令打包一次执行。事务通过 MULTI 开始,之后命令进入队列,EXEC 时按顺序执行,DISCARD 可放弃队列,WATCH 用于实现乐观锁。
5.1 ACID 特性在 Redis 中的体现
Redis 事务并不完全满足传统数据库的 ACID 特性:
- **原子性:**EXEC 执行时命令不会被其他客户端插入,具备一定的隔离性;但某条命令执行失败不会回滚其他已执行命令,因此不是严格的原子性。
- **一致性:**由 Redis 命令自身的正确性和顺序性保障,但应用需要自己保证业务一致性。
- **隔离性:**事务队列执行期间命令按顺序执行,不会被其他客户端交错插入,具备单线程下的隔离特征。
- **持久性:**取决于持久化策略,使用 RDB 或无持久化时事务结果并非一定落盘。
5.2 常用命令
bash
MULTI
SET key1 value1
INCR counter
EXEC
放弃事务
MULTI
SET key2 value2
DISCARD
使用 WATCH 实现条件提交
WATCH counter
MULTI
INCR counter
EXEC
5.3 事务的局限性
Redis 事务的主要局限包括:不支持回滚;如果某条命令语法错误,整个事务不会执行;如果是运行时错误,则只有错误命令失败,其余命令继续执行;WATCH 在并发冲突时会直接让 EXEC 返回 nil,需要业务自行重试。
5.4 解决方法
对于需要严格原子性和回滚语义的场景,可以优先使用 Lua 脚本。Redis 会原子地执行整个 Lua 脚本,脚本执行期间不会被其他命令插入。示例:
lua
local current = redis.call("GET", KEYS[1])
if tonumber(current) >= 1 then
return redis.call("DECR", KEYS[1])
else
return -1
end
**注意事项:**Lua 脚本不宜执行过长,避免阻塞 Redis 单线程;应避免在脚本中依赖非常不确定的外部状态;生产环境需测试脚本的执行时间和 key 访问范围。
六、Redis 主从复制
主从复制是 Redis 实现高可用和读写分离的基础。主节点负责写入,从节点保存数据副本并提供读服务,从而提升读并发能力并增强数据冗余。
6.1 主从架构工作原理
主从复制的基本流程包括:从节点向主节点发起同步请求,主节点生成快照并发送给从节点完成全量同步;之后主节点持续将写命令发送给从节点进行增量同步。较新版本使用基于偏移量和复制积压缓冲区的部分重同步机制,减少全量同步频率。
6.2 配置方法
bash
# 主节点 redis.conf 关键配置
bind 0.0.0.0
protected-mode no
port 6379
从节点 redis.conf 关键配置
replicaof 192.168.1.100 6379
masterauth "your-master-password"
replica-read-only yes
启动后可通过以下命令查看复制状态:
bash
INFO replication
6.3 数据同步过程
**全量同步:**当从节点首次连接或复制偏移量无法通过积压缓冲区补齐时,主节点执行 BGSAVE 生成 RDB 快照并发送给从节点,从节点载入快照后继续同步后续写命令。
**增量同步:**主节点把每个写命令传播给已建立连接的从节点,从节点持续应用这些命令,保持数据一致。
6.4 故障转移机制
主从复制本身不会自动完成故障转移。生产环境通常使用 Redis Sentinel 或 Redis Cluster 来实现监控和自动切换。Sentinel 会监控主节点和从节点状态,当主节点不可达时自动选举新的主节点,并通知客户端变更连接地址。
6.5 读写分离实现
应用层可以把写请求发送到主节点,读请求发送到从节点。实现方式包括客户端手动路由、中间件代理或使用支持读写分离的客户端库。示例客户端层面可根据命令类型选择连接:
java
// 示例:写操作使用主节点连接,读操作使用从节点连接
if (isWriteCommand(cmd)) {
masterConnection.execute(cmd);
} else {
replicaConnection.execute(cmd);
}
**注意事项:**主从复制是异步的,从节点数据可能存在短时间延迟,因此强一致性业务不能依赖从节点读到最新数据;写入很少但读流量极大时,可通过增加从节点分摊读压力;需要关注主从切换期间的数据丢失和客户端重连。
七、缓存常见问题及解决方案
在 Redis 作为缓存使用时,经常遇到缓存穿透、缓存击穿和缓存雪崩三类问题。它们的触发条件不同,解决方案也需要分别设计。
7.1 缓存穿透
**定义:**查询一个数据库和缓存中都不存在的数据,导致每次请求都直接打到数据库,缓存形同虚设。
**产生原因:**恶意构造大量不存在的 key,或者业务逻辑对不存在的数据没有缓存策略。
解决方案:
- **布隆过滤器:**在缓存前增加布隆过滤器,快速判断 key 是否可能存在,明显降低不存在 key 打到数据库的概率。
- **空值缓存:**对于查询不到的数据,缓存一个短期空值,避免重复穿透。
- **参数校验:**在入口拦截明显非法的 key,例如非法 ID、越界分页。
7.2 缓存击穿
**定义:**某个热点 key 在过期瞬间收到大量并发请求,全部绕过缓存直接访问数据库,导致数据库压力骤增。
**产生原因:**热点数据设置过期时间不合理,或热点 key 因为缓存更新、淘汰等原因突然失效。
解决方案:
- **互斥锁:**多个请求同时发现缓存失效时,只允许一个请求回源数据库重建缓存,其余请求等待或返回旧的降级数据。
- **热点数据永不过期:**逻辑上使用独立线程异步更新缓存,避免物理过期导致击穿。
- **提前预热:**在活动开始前把热点数据加载到缓存,并设置错峰更新。
7.3 缓存雪崩
**定义:**大量缓存在同一时间失效,或者缓存服务整体宕机,导致请求集中打到数据库,造成数据库压力过大甚至崩溃。
**产生原因:**大量 key 设置了相同或相近的过期时间,或缓存集群出现故障。
解决方案:
- **过期时间随机化:**在基础过期时间上增加随机值,避免同时失效。
- **多级缓存:**本地缓存、分布式缓存和数据库逐层兜底,降低单一缓存层故障的影响。
- **熔断降级:**当数据库压力过大时,对非核心服务进行降级,核心读路径启用限流、熔断和降级策略。
- **高可用部署:**使用 Redis Cluster 或 Sentinel 提升缓存服务本身的可用性。
八、常见面试题整理
以下面试题按题型分类,覆盖数据类型、持久化、锁、事务、主从复制和缓存问题等核心知识点。每道题均补充参考答案与解析,便于系统复习。
8.1 概念理解题
**题目 1:**Redis 是单线程模型吗?为什么它还能有较高的性能?
**答案:**Redis 的核心命令执行采用单线程模型,网络 I/O 和命令执行都在主线程中依次完成。Redis 6.0 之后引入多线程 I/O,主要用于网络读写,但命令的实际执行仍然在单线程中完成。
**解析:**Redis 性能高并不是因为多线程,而是因为纯内存读写、单线程避免了锁竞争和上下文切换、基于 epoll 等 IO 多路复用,以及 SDS、跳表、哈希表等高效数据结构。单线程还让命令天然串行化,避免了并发修改带来的复杂度。面试时需要区分"命令执行单线程"和"网络 I/O 多线程",不要简单回答"Redis 已经改成多线程了"。
**题目 2:**String 和 Hash 都常用于缓存对象,它们有什么区别?
**答案:**String 会把对象序列化成 JSON 等字符串后整体存储,读写操作简单,但更新任意字段都需要取出整个对象、修改后整体写回;Hash 以 field-value 形式存储,可以按字段读写和更新,更新粒度更细。
**解析:**两者选择取决于对象的字段数量、更新频率和资源开销。字段多且经常只改其中一个字段时,Hash 更合适;对象小、以整体读写为主时 String 更简单。Hash 字段很多时会带来较大的内存开销,HGETALL 拉取全部字段的成本也更高;String 每次更新都需要反序列化、修改、再序列化,写入成本随对象变大而增加。
**题目 3:**RDB 和 AOF 分别是什么?两者最核心的区别是什么?
**答案:**RDB 是定时生成的某个时间点全量数据快照,恢复速度快、文件紧凑;AOF 是追加写入的每一条写命令日志,数据安全性高,但文件通常更大、恢复更慢。两者最核心的区别在于持久化方式不同:RDB 记录"某一时刻的数据",AOF 记录"数据的变化过程"。
**解析:**RDB 丢失数据的窗口取决于快照间隔,适合备份和允许分钟级丢失的场景;AOF 默认 everysec 最多丢约 1 秒数据,适合对数据完整性要求更高的场景。生产环境可以两者同时开启,Redis 重启时默认优先使用 AOF 恢复,以保证更完整的数据。
**题目 4:**什么是缓存穿透、缓存击穿和缓存雪崩?三者的区别是什么?
**答案:**缓存穿透是查询缓存和数据库都不存在的数据;缓存击穿是单个热点 key 过期瞬间被大量并发请求打穿到数据库;缓存雪崩是大量 key 同时失效或缓存服务整体宕机,导致请求集中打到数据库。
**解析:**三者都会导致数据库压力过大,但触发对象和范围不同:穿透针对"不存在的数据",每次都绕过缓存;击穿针对"单个热点 key";雪崩针对"大量 key 同时失效或整个缓存不可用"。对应方案分别是布隆过滤器、空值缓存;互斥锁、热点不过期;过期时间随机化、多级缓存、高可用部署等。
**题目 5:**简述 Redis 主从复制的角色分工,从节点可以写入吗?
**答案:**主节点负责处理写请求,并将数据同步给从节点;从节点保存数据副本,提供读服务,从而提升读并发能力并增强数据冗余。从节点默认只读,不能写入。
**解析:**从节点默认配置 replica-read-only yes,写入从节点会被拒绝。主从复制是异步的,从节点数据可能存在延迟,因此强一致性业务不能依赖从节点读到最新数据。读多写少的场景可以通过增加从节点来分摊读压力。
8.2 原理分析题
**题目 1:**请说明 Sorted Set 的底层实现,如何理解它的排序和范围查询?
**答案:**Sorted Set 在小数据量时使用紧凑结构 ziplist(Redis 7 中为 listpack);当数据量较大或元素长度超过阈值时会转换为"跳跃表 + 哈希表"。跳跃表负责按 score 维护有序关系,支持高效的范围查询;哈希表负责根据 member 快速定位 score。
**解析:**跳跃表可以理解为多层有序链表,查找、插入、删除的平均时间复杂度为 O(logN)。范围查询时先通过跳跃表定位起点排名,再沿层级向后遍历。同分成员按 member 的字典序排序。哈希表提供 O(1) 的 member 到 score 映射,配合 ZSCORE、ZRANK 等命令使用。
**题目 2:**请分析 BGSAVE 为什么不会长时间阻塞主进程,可能带来什么问题?
**答案:**BGSAVE 通过 fork 创建一个子进程,由子进程执行 RDB 快照写入,主进程继续处理客户端请求,因此不会长时间阻塞主进程。
**解析:**fork 之后父子进程依赖操作系统的 copy-on-write 机制:刚开始共享内存页,当主进程修改数据时才复制对应的内存页。因此可能带来的问题包括:fork 操作本身在数据量很大时可能造成短暂停顿;写操作频繁时 COW 会复制较多内存页,可能造成内存峰值升高;写快照时也会产生磁盘 I/O 压力。
**题目 3:**AOF 的 appendfsync 策略有哪些?分别如何影响数据安全和性能?
**答案:**appendfsync 有三种策略:always、everysec、no。always 每次写命令后都同步刷盘,数据最安全但性能最差;everysec 每秒同步一次,最多可能丢失约 1 秒数据,是性能和安全性的折中方案;no 由操作系统决定何时刷盘,性能最好但数据安全性最不可控。
**解析:**appendfsync 的默认值是 everysec,生产环境通常也使用 everysec,在性能与安全之间取得平衡。always 适合对每一条写记录都要求落盘的强一致业务,但会明显降低吞吐;no 一般不作为关键业务的选择。
**题目 4:**Redis 事务为什么不支持回滚?如果某条命令运行时报错会发生什么?
**答案:**Redis 事务不支持回滚,核心原因是 Redis 追求简单和高效,命令执行速度非常快,回滚需要引入复杂的 undo 机制,违背其轻量设计定位。如果事务中存在语法错误,Redis 会在 EXEC 前识别并让整个事务不执行;如果是运行时错误,则只有这条错误命令失败,其余命令继续执行且不会回滚。
**解析:**例如对字符串类型的 key 执行 INCR,属于运行时错误,Redis 只让该条命令失败,其他命令照常执行。因此 Redis 事务并不满足传统数据库的原子性,不能把 MySQL 的事务语义直接套用到 Redis 上。
**题目 5:**主从复制的全量同步和增量同步分别发生在什么阶段?
**答案:**全量同步发生在从节点首次连接主节点,或复制偏移量无法通过复制积压缓冲区补齐时。主节点执行 BGSAVE 生成 RDB 快照并发送给从节点,从节点载入快照后继续同步后续写命令;增量同步发生在全量同步完成之后,主节点持续把新的写命令传播给从节点。
**解析:**Redis 2.8 以后,增量同步基于 replication offset 和复制积压缓冲区。如果从节点断线后偏移量仍落在缓冲区范围内,可以通过部分重同步补齐,而不是每次断线都触发全量同步。只有当偏移量丢失后才进行全量同步。
8.3 实际应用题
**题目 1:**如何用 Redis 实现一个排行榜?请写出关键命令并解释分数相同时如何处理。
**答案:**使用 Sorted Set,以 score 表示排名分值,member 表示用户或商品。关键命令如下:
bash
ZADD rank 100 user:a
ZADD rank 200 user:b
ZADD rank 150 user:c
ZREVRANGE rank 0 -1 WITHSCORES
分数相同时,Sorted Set 会按 member 的字典序从小到大排序。如果希望新上榜的成员排在前面,可以在 score 中编码时间信息,例如使用"主分值 + 时间戳小数位"的组合;或把时间转换为一个适合的数值,使更晚的时间获得更高的排序优先级。
**解析:**排行榜通常用 ZREVRANGE 按 score 从高到低取排名。同分用户的先后顺序由 Redis 按字典序决定,业务一般会追加"分数 + 时间戳或其他优先级"来明确排名,避免同分排序不可控。也可以使用 ZADD 的 XX/NX 参数控制只更新或只新增。
**题目 2:**如何使用 Redis 实现一个简单的分布式锁?列出获取锁和释放锁的完整流程。
答案: 获取锁使用 SET 命令的 NX 和 PX 参数:SET lock_key unique_value NX PX 30000。NX 保证只有第一个客户端能创建成功,PX 设置过期时间避免死锁。释放锁时必须先判断 value 是否属于当前客户端,再执行 DEL,保证不会误删他人的锁。
**解析:**释放锁必须保证"判断 + 删除"的原子性,通常使用 Lua 脚本:
lua
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
还需要考虑锁续期问题。当任务执行时间可能超过锁过期时间时,可通过 Redisson 提供的看门狗机制自动续期,或由业务通过定时任务延长锁的过期时间。
**题目 3:**如何避免大量优惠券库存同时过期导致缓存击穿?请给出具体设计。
**答案:**可以采用"过期时间随机化 + 互斥锁重建 + 热点数据预热"。每个 key 的过期时间在基础时间上增加随机值,避免同一时刻大量失效;当某个缓存失效时,只允许一个请求回源数据库重建缓存,其他请求等待;活动开始前提前把热点库存加载到缓存。
**解析:**更稳妥的方案是使用"逻辑过期 + 异步刷新":缓存 key 物理上不设置过期时间,在 value 中记录逻辑过期时间。请求发现逻辑过期后先返回旧值,再由后台线程异步更新,从而避免大量请求同时回源数据库。
**题目 4:**如何在一个 Spring Boot 项目中配置 Redis 读写分离?需要注意哪些一致性问题?
**答案:**在 Spring Boot 中可配置主从两个 RedisConnectionFactory,或使用 Lettuce 的读写分离配置,通过判断命令类型将写请求路由到主节点、读请求路由到从节点。
**解析:**需要注意主从复制是异步的,从节点可能存在延迟。强一致性读、事务或"写后立即读"的场景应走主节点;一般查询可以路由到从节点。主从切换期间可能出现连接抖动或数据短暂不一致,需要通过客户端重试、超时控制和业务幂等来兜底。
**题目 5:**如何使用 Set 计算两个用户的共同关注列表?
答案: 使用 SINTER 命令计算两个用户关注列表的交集:SINTER user:1:follows user:2:follows,返回结果就是两个用户的共同关注列表。
**解析:**Set 天然支持集合运算。若关注数据量很大,直接 SINTER 可能阻塞,可以考虑分页或使用 SSCAN 拆分,或把关注关系分片存储,降低单次运算规模。类似地,SDIFF 可用于计算"A 关注了而 B 没有关注"的用户。
8.4 场景设计题
**题目 1:**设计一个支持高并发的商品库存扣减方案,要求避免超卖,并说明为什么不能只依赖 Redis 事务。
**答案:**核心思路是使用 Lua 脚本原子地完成"读取库存、判断是否充足、扣减库存",确保库存不会被减到负数。脚本示例:
lua
local current = redis.call("GET", KEYS[1])
if tonumber(current) >= tonumber(ARGV[1]) then
return redis.call("DECRBY", KEYS[1], ARGV[1])
else
return -1
end
**解析:**不能只依赖 Redis 事务,因为 Redis 事务不支持回滚,WATCH 乐观锁在并发冲突时需要应用层重试,高并发下冲突率很高,吞吐不稳定。更可靠的做法是用 Lua 脚本原子扣减,同时保留数据库库存作为最终一致性的兜底,配合异步落库、异步对账和幂等控制。下单成功后异步更新数据库,失败时补偿库存。
**题目 2:**设计一个短链接系统,说明如何选择 Redis 数据类型、如何避免缓存穿透和如何控制内存增长。
答案: 可以用 String 保存短码到长链接的映射:SET short:abc123 "https://long-url"。为避免缓存穿透,可以在查询前使用布隆过滤器判断短码是否可能存在,不存在直接返回;为控制内存增长,可以设置合理的过期时间,对冷数据使用 LRU/LFU 淘汰策略,并根据短码长度和访问量做容量预估。
**解析:**如果某个短码对应的数据需要存储多个字段,也可以改用 Hash,按字段读写更灵活。短码生成要保证唯一性,可以采用发号器、哈希取位后加重试机制。布隆过滤器会存在一定误判率,因此即使判定存在,仍需要回源查询确认。
**题目 3:**设计一个分布式限流方案,要求支持滑动窗口,并说明 Redis 在其中承担的职责。
**答案:**使用 Sorted Set 实现滑动窗口限流,以时间戳作为 score、请求唯一标识作为 member。每个请求到达时,先移除窗口外的旧记录,再统计窗口内请求数,超限则拒绝;未超限则加入当前请求并设置过期时间。为保证"移除、统计、写入"的原子性,通常封装为 Lua 脚本执行。
**解析:**Redis 在该方案中承担时间窗口存储、精确计数和原子执行。固定窗口算法存在临界突刺问题,滑动窗口比固定窗口更平滑。生产环境还需要考虑清理过期 key、使用管道降低往返次数,以及按接口、用户、IP 等维度分级配置限流规则。
**题目 4:**设计一个高可用缓存架构,要求同时考虑主从复制、哨兵、缓存穿透和缓存雪崩。
**答案:**高可用缓存架构可以采用"主从复制 + Sentinel"实现自动故障转移;流量更大时使用 Redis Cluster 分片。缓存穿透使用布隆过滤器和空值缓存;缓存雪崩通过过期时间随机化、多级缓存和熔断降级缓解。
**解析:**读路径可以设计为"本地缓存 → Redis → 数据库"逐层兜底。本地缓存降低 Redis 压力,Redis 分摊数据库压力。数据更新时通过缓存失效、消息驱动刷新或 binlog 异步更新保持一致性。关键是根据业务容忍度选择最终一致,还是对强一致数据强制读主节点。
**题目 5:**设计一个用户在多个设备登录时只保留一个有效会话的机制,并说明如何用 Redis 配合过期时间完成。
答案: 用户登录时为该用户生成新的 token,并用 SET session:user:123 current_token EX 1800 覆盖旧值;后续请求校验 token 是否与 Redis 中的最新值一致,旧设备的 token 会因不一致而被拒绝。每次校验成功后可刷新过期时间,实现滑动续期。
**解析:**这样每个用户只保留一个有效 token,新设备登录会把旧设备挤下线。还可以在 token 中编码服务器 IP、设备 ID 等信息以增强安全性。需要注意并发登录时以最后一次写入为准,避免两个登录请求互相覆盖导致用户被异常踢下线。
**复习建议:**在回答 Redis 相关问题时,除了复述概念,还应结合"为什么这样设计""有什么代价""如何选型"三个角度展开,这样既能体现原理理解,也能体现工程实践能力。
九、总结与参考资料
9.1 全文总结
本文以 Linux 环境为背景,系统整理了 Redis 的核心知识体系:五种基本数据类型的特性、常用命令、使用场景与性能特点;RDB 与 AOF 持久化的原理、优缺点及对比选型;基于版本号的乐观锁与基于 SETNX 的悲观锁;Redis 事务的 ACID 表现、局限性及 Lua 脚本解决思路;主从复制的数据同步、读写分离与故障转移;缓存穿透、缓存击穿、缓存雪崩的成因与解决方案;最后通过概念理解、原理分析、实际应用和场景设计四类面试题,帮助在复习时做到"记命令"与"懂原理"并重。
Linux 环境下的实践要点需要特别关注:通过 redis.conf 完成持久化、主从复制、内存和安全策略配置;借助 redis-cli 排查运行状态、验证命令;理解 BGSAVE 依赖 fork 与 copy-on-write 机制,避免对内存峰值产生误判;AOF 默认 everysec 在性能与数据安全之间取得折中;主从切换通常配合 Sentinel 或 Cluster 实现自动故障转移与高可用部署。复习时应把命令操作与设计原理结合起来,围绕数据类型选型、持久化组合、锁的可靠性、缓存一致性和高可用方案形成工程化判断。
9.2 参考资料
- Redis 官方文档: Docs------最权威的命令说明、配置项和架构介绍,适合作为日常查阅的第一手资料。
- **《Redis 设计与实现》黄健宏:**深入讲解 SDS、跳跃表、持久化、主从复制等底层实现,适合系统复习原理和应对面试追问。
- **《Redis 开发与运维》付磊、张益军:**偏向生产环境实战,覆盖安装部署、参数调优、高可用和监控运维,适合补齐工程实践能力。
- Redis 官方 GitHub 仓库: https://github.com/redis/redis------可阅读源码和 Release Notes,了解版本演进与新特性。
- Redis 命令参考: Commands | Docs------按数据类型分类的快速命令手册,方便对照练习和查漏补缺。
- Redisson 官方文档: Redisson -- Valkey & Redis Java Client (Locks, Cache, Queues)------重点讲解分布式锁、看门狗续期和 Java 客户端集成,适合实践分布式锁相关场景。
- **小林 coding 图解 Redis:**图文并茂地梳理 Redis 核心原理与常见面试题,适合快速串讲和考前复习。
- **美团技术团队博客:**缓存穿透、击穿、雪崩以及高并发缓存架构等生产实践文章较多,适合理解真实业务中的取舍与优化。