服务端5月31日 02:05
Redis 底层为什么这么快?核心数据结构如何支撑?Redis 底层之所以快,主要靠三件事:数据大多在内存里,命令执行路径足够短,核心数据结构针对常见操作做了大量取舍。它不是简单的“单线程所以快”,而是 SDS、dict、skiplist、quicklist、intset、事件循环、I/O 多路复用和内存分配共同配合的结果。面试里回答这个问题,最好从数据结构讲到网络模型,再落到性能边界。
## 字符串和哈希表是基础
Redis 的 String 底层不是直接使用 C 字符串,而是 SDS。SDS 记录了长度和容量,获取长度是 O(1),扩容时有预分配策略,还能安全存储二进制数据。它的取舍是多占一点元数据空间,换来更安全的修改和更少的内存重分配。
Hash、数据库 key 空间、过期字典大量依赖 dict。dict 使用哈希表加链地址法解决冲突,并通过两个哈希表做渐进式 rehash。这样不会在扩容时一次性搬完整张表,避免服务长时间卡住。踩坑点是 rehash 期间查询、写入都要同时照顾两张表,理解这一点才能解释为什么 Redis 能边服务边扩容。
```c
typedef struct dict {
dictht ht[2];
long rehashidx;
} dict;
```
## List、Set、ZSet 会按规模切换编码
Redis 不会一上来就用最通用但最重的数据结构。小集合会用 intset 或紧凑编码节省内存,元素变多或类型变化后再升级。List 在新版本主要基于 quicklist,它把多个紧凑节点串成双向链表,兼顾顺序访问和内存占用。ZSet 常见实现是 dict + skiplist:dict 保证按 member 快速查分数,skiplist 保证按 score 范围查询和排名。
跳表不是唯一能做有序结构的方案,平衡树也可以。Redis 选择跳表,是因为实现相对简单,范围查询自然,插入删除平均 O(log n),调试和维护成本低。代价是多层索引会额外占内存,score 相同还要按 member 做字典序比较。
## 单线程命令执行不等于只有一个线程
Redis 的核心命令执行长期保持单线程,这能避免复杂锁竞争,也让数据结构操作更可控。但网络 I/O、后台删除、AOF 刷盘、持久化重写等任务可能由其他线程或子进程参与。Redis 6 之后支持 I/O 多线程,主要优化网络读写,命令执行仍以单线程为主。
```bash
redis-cli INFO memory
redis-cli SLOWLOG GET 10
redis-cli --bigkeys -h 127.0.0.1 -p 6379
```
## 事件循环和过期删除决定稳定性
Redis 用事件循环处理文件事件和时间事件。文件事件负责连接读写,时间事件负责定时任务,比如过期 key 抽样删除、统计维护、复制心跳。过期删除不是每个 key 到点立刻删除,而是惰性删除加定期抽样删除。这个设计节省 CPU,但也意味着大量过期 key 可能短时间占住内存,需要合理设置 TTL 分布。
## 追问
### Redis 为什么用 SDS,不直接用 C 字符串?
C 字符串没有长度字段,求长度需要遍历,拼接时还容易写越界。SDS 把长度、剩余容量和字节数组放在一起,常见修改能先检查容量,不够再扩容。它的边界是会增加少量结构体开销,但 Redis 更在意高频操作的稳定延迟。存二进制内容时,SDS 也不会因为中间出现 `\0` 就截断。
### 渐进式 rehash 解决了什么问题?
如果哈希表扩容时一次性迁移所有 key,Redis 会在这一瞬间明显阻塞。渐进式 rehash 把迁移分摊到后续命令和定时任务里,每次搬一小部分。代价是 rehash 期间要查两张表,代码复杂一些,内存也会短暂增加。这个取舍很典型:用一点空间和实现复杂度,换线上延迟更平滑。
### ZSet 为什么通常用 dict 加 skiplist?
dict 让 `ZSCORE` 这类按成员查分数的操作接近 O(1),skiplist 让按分数范围查询、排名查询保持 O(log n)。如果只用 dict,没法高效排序;如果只用 skiplist,按 member 查找又不够快。踩坑点是 ZSet 的内存成本比普通 Set 高很多,不适合无限堆用户行为明细。排行榜要加时间维度和过期策略,否则迟早变成大 key。
### Redis 单线程会不会被慢命令拖垮?
会,所以生产环境要避免 `KEYS`、超大集合 `SMEMBERS`、大 key 删除这类操作。单线程的好处是没有锁竞争,坏处是一个慢命令就可能挡住后面的请求。线上更推荐 `SCAN` 分批扫描,用 `UNLINK` 异步删除大 key,并打开慢日志观察异常命令。Redis 很快,但前提是每个命令都足够短。
### 怎么从底层角度排查 Redis 性能问题?
先看 `INFO memory` 判断内存、碎片率和淘汰情况,再看 `SLOWLOG` 找慢命令。怀疑大 key 时用 `--bigkeys` 或离线 RDB 分析,怀疑网络瓶颈时看连接数、客户端输出缓冲区和命令 QPS。不要一上来就调线程数,很多问题其实是 key 设计或命令复杂度错了。底层原理的价值就在这里:能把“Redis 慢了”拆成结构、网络、内存和持久化几个方向。标签
Redis
Redis (Remote Dictionary Server) 是一个开源的、基于内存的高性能键值存储数据库,它通常被用作数据库、缓存或消息传递系统。由于其极高的性能和灵活的数据结构,它适用于各种场景,包括实时应用程序、高速缓存策略和任务队列。

服务端5月31日 02:05
Redis 常见应用场景有哪些?项目里该怎么选?Redis 最常见的应用场景不是“把数据放进内存”这么简单,而是用它的低延迟、原子命令和多种数据结构,去补数据库、应用进程和消息系统的短板。项目里常见用法包括缓存、分布式锁、排行榜、计数器、限流、会话存储、轻量消息队列、地理位置和集合关系计算。真正要答好这个问题,重点不是背场景清单,而是说清楚每个场景为什么适合 Redis,以及什么时候不该用。
## 缓存是最高频场景
缓存通常用 Cache-Aside 模式:读请求先查 Redis,未命中再查数据库并写回缓存;写请求先更新数据库,再删除缓存。这个模式简单,但边界很多:不存在的数据要防缓存穿透,热点 key 过期要防击穿,大量 key 同时过期要防雪崩。实际项目里我会给 TTL 加随机抖动,并对空结果设置较短缓存时间。
```java
User user = redis.get("user:" + id);
if (user == null) {
user = db.queryById(id);
redis.set("user:" + id, user, 3600 + random(300));
}
return user;
```
## 分布式锁和限流要看一致性要求
分布式锁常用 `SET key value NX EX seconds`,释放时用 Lua 比较 value 后再删除,避免删掉别人刚拿到的锁。它适合防重复提交、定时任务抢占、库存扣减前置保护,但不适合承诺绝对强一致;如果锁过期时间估短,业务没跑完锁就释放,会出现并发穿透。限流则常用 `INCR`、Sorted Set 滑动窗口或 Lua 令牌桶,固定窗口最简单,但窗口边界会有突刺。
## 排行榜、计数器和集合计算是数据结构优势
排行榜用 Sorted Set,`ZADD` 写分数,`ZREVRANGE` 查榜单,`ZREVRANK` 查名次,比数据库反复排序轻很多。阅读量、点赞数可以用 `INCR` 做原子计数,再异步落库。共同关注、标签筛选、用户分组适合用 Set 的交集和并集,但集合太大时要注意阻塞风险,线上不要随手对超大 key 做全量运算。
```bash
ZADD article:rank 1024 article_1001
ZREVRANGE article:rank 0 9 WITHSCORES
SINTER user:1:follows user:2:follows
```
## 消息队列和会话存储要谨慎取舍
简单异步任务可以用 List 的 `LPUSH` + `BRPOP`,Redis Stream 支持消费组和确认机制,更像轻量队列。它的好处是接入快、延迟低;边界是消息堆积、重试治理和跨机房容灾能力不如 Kafka、RocketMQ 这类专业消息系统。Session 存 Redis 很常见,适合多实例共享登录态,但敏感字段要加密或只存引用 ID,不能把 Redis 当成无边界的安全仓库。还有一个常被忽略的点:Redis 不是持久化数据库的替代品,AOF 和 RDB 能降低丢数据概率,却不能让所有缓存场景天然具备事务语义。设计时要先问清楚“丢一小段数据能不能接受”,再决定是否把它放在 Redis 里,这个判断很关键。
## 追问
### Redis 做缓存时,怎么处理缓存和数据库不一致?
常见做法是先更新数据库,再删除缓存,让下一次读取重新加载新值。这个方案牺牲了一点短时间一致性,换来实现简单和故障恢复容易。踩坑点是删除缓存失败会留下旧值,所以生产里通常配合消息重试、binlog 订阅或短 TTL 兜底。如果业务要求读到最新值,比如支付状态,就不要读缓存,直接读主库或走强一致链路。
### Redis 做分布式锁可靠吗?
单 Redis 节点的锁只能解决大多数工程并发问题,不能等同于严格分布式共识。锁 value 必须是唯一 token,释放锁必须用 Lua 原子校验,否则会误删别人的锁。Redisson 的看门狗能降低锁过期风险,但如果进程 STW、网络抖动或 Redis 主从切换,仍要考虑幂等和补偿。重要结论是锁只能减少并发冲突,不能替代业务层一致性设计。
### Redis Stream 能不能替代 Kafka?
低吞吐、少团队协作、希望快速落地的内部任务队列可以用 Redis Stream。它支持消费组、ACK 和消息 ID,比 Pub/Sub 可靠,但在超大吞吐、长期消息保留、多分区扩展和生态工具上不如 Kafka。项目里如果消息是核心链路,优先选专业 MQ;如果只是削峰、异步发通知或刷新缓存,Stream 够用而且维护成本低。
### 热点 key 和大 key 分别怎么处理?
热点 key 是访问太集中,可以用本地缓存、多副本读、key 分片或提前预热来分散压力。大 key 是单个 value 或集合过大,会导致网络传输慢、删除阻塞、主从同步卡顿,应该拆分存储并用 `UNLINK` 异步删除。两个问题经常混在一起,但处理方向不同:热点 key 关注流量,大 key 关注体积。上线前用 `redis-cli --bigkeys` 和慢日志排查,比故障后临时猜要靠谱。服务端5月31日 02:05
Redis 常见问题怎么排查?大 Key、热点和一致性如何处理?Redis 常见问题不要按概念清单排查,线上通常先看现象:延迟升高、内存暴涨、从库追不上、数据库被打穿、某个接口 QPS 异常。高频原因包括大 Key、热点 Key、慢命令、缓存一致性、分布式锁误用和限流算法选错。Redis 很快,但单个大对象、阻塞命令、网络抖动和错误过期策略,都会把系统拖慢。
## 追问
### Redis 为什么快,慢请求先看什么?
Redis 快在内存访问、单线程命令执行避免锁竞争、I/O 多路复用处理连接,以及高效数据结构。Redis 6 之后网络 I/O 可以多线程,但命令执行仍是核心单线程路径,一个慢命令会拖住后面的请求。排查先看 `SLOWLOG GET`、`INFO commandstats`、`LATENCY DOCTOR`,不要只盯 CPU;很多延迟其实来自阻塞命令、fork、AOF fsync、主从同步或客户端连接池耗尽。
### 大 Key 怎么发现,为什么不能直接 `DEL`?
大 Key 可能是几 MB 的字符串,也可能是几十万元素的 hash、list、set,可以用 `redis-cli --bigkeys` 抽样扫描,并在业务侧记录 value 大小。生产环境不要随手 `KEYS *`,它本身就可能阻塞实例。大 Key 的读写、序列化、网络传输和删除都可能卡主线程;删除时优先用 `UNLINK` 或后台渐进清理,核心集群要放在低峰并评估影响。
### 热点 Key 如何处理,复制多份一定安全吗?
热点 Key 是某个 key 被大量并发访问,常见于秒杀库存、首页配置、热门商品详情。只读热点可以用本地缓存、读写分离、客户端缓存,或复制多份 key 随机读取。写热点不能简单复制,否则一致性会很难保证;可累加计数可以分片后异步汇总,强一致库存更适合排队、限流或业务层削峰。
### 缓存和数据库不一致怎么处理?
常用 Cache Aside 是读缓存,未命中查数据库再写缓存;写数据时先更新数据库,再删除缓存。它简单可靠,适合大多数最终一致场景,但并发读写下仍可能短暂不一致。延迟双删能降低概率却不好选延迟时间,binlog 或消息队列更稳但链路更复杂;余额、库存扣减这类强一致路径不要依赖缓存读。
### 分布式锁和限流最容易踩哪些坑?
Redis 锁至少要用 `SET key value NX EX seconds`,释放时用 Lua 先比较 value 再删除,避免删掉别人的锁。过期时间太短会导致业务没跑完锁已释放,太长又影响故障恢复,Redisson 看门狗能缓解但不能无视 GC 暂停和网络分区。限流里固定窗口简单但有边界突刺,滑动窗口更平滑但占内存,令牌桶适合允许短时突发;无论哪种都要配合幂等和降级。
## 写段命令和配置
```bash
redis-cli SLOWLOG GET 10
redis-cli --latency -h 127.0.0.1 -p 6379
redis-cli LATENCY DOCTOR
redis-cli --bigkeys
redis-cli INFO memory
redis-cli INFO commandstats
```
```java
// 安全释放分布式锁:比较 value 后再删除
String lua = "if redis.call('GET', KEYS[1]) == ARGV[1] " +
"then return redis.call('DEL', KEYS[1]) else return 0 end";
redis.eval(lua, Collections.singletonList(lockKey), Collections.singletonList(lockValue));
```
```java
// Cache Aside:先更新数据库,再删除缓存
public void updateUser(User user) {
db.update(user);
redis.del("user:" + user.getId());
}
```
## 小结
Redis 排查要从现象回到机制:慢请求看阻塞和慢命令,内存问题看大 Key 和淘汰策略,流量尖峰看热点 Key,数据异常看缓存一致性链路。解决方案都在性能、一致性、复杂度之间取舍,提前做好 key 规模约束、慢日志监控、过期时间打散和降级预案,比报警后临时救火可靠。服务端5月28日 09:38
Redis 安全配置怎么做?生产环境加固 checklist 与漏洞防范## 核心回答
Redis 安全配置要从网络隔离、认证授权、数据保护和运行加固四个层面入手。生产环境最低要求三条:绑定内网 IP + 开启密码认证 + 禁用危险命令。做不到这三条,Redis 基本等于裸奔。
2015 年爆发的 Redis 未授权访问漏洞(CVE-2015-4335)让大量服务器被植入挖矿脚本和 SSH 公钥,根源就是默认配置下 Redis 无密码监听所有网卡。这个漏洞至今仍在被批量扫描利用,绝不是历史问题。每次安全加固的第一步,就是确保这三条底线全部到位。
## 网络层隔离
### 绑定监听地址
Redis 默认绑定 `0.0.0.0`,所有网卡都能连,这是最常见的安全隐患。修改 `redis.conf`:
```bash
bind 127.0.0.1 10.0.0.1
protected-mode yes
```
`protected-mode` 是 Redis 3.2 引入的保护机制,当 Redis 绑定了非回环地址且没有设置密码时,会拒绝外部连接。这个开关一定要保持开启。
生产环境建议只绑定内网 IP,绝不要直接暴露到公网。如果必须远程访问,走 VPN 或 SSH 隧道。很多 Redis 被入侵的案例就是因为公网暴露了 6379 端口,被扫描器批量发现后利用。
### 防火墙规则
即使绑定了内网 IP,也要用防火墙做二次防护,这是纵深防御的基本思路:
```bash
# iptables 方式
iptables -A INPUT -p tcp --dport 6379 -s 10.0.0.0/24 -j ACCEPT
iptables -A INPUT -p tcp --dport 6379 -j DROP
# firewalld 方式
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.0.0.0/24" port protocol="tcp" port="6379" accept'
firewall-cmd --reload
```
云上环境用安全组实现同样的效果,原则是端口最少放开。AWS、阿里云的安全组规则中,Redis 端口只对应用服务器所在子网开放。
### TLS 加密传输
Redis 6.0 开始原生支持 TLS,这是生产环境安全加固的重要一环。如果数据经过不可信网络(跨机房、公网),必须开启:
```bash
# 生成证书
openssl genrsa -out redis.key 2048
openssl req -new -key redis.key -out redis.csr
openssl x509 -req -days 365 -in redis.csr -signkey redis.key -out redis.crt
# redis.conf 配置
tls-port 6380
port 0
tls-cert-file /path/to/redis.crt
tls-key-file /path/to/redis.key
tls-ca-cert-file /path/to/ca.crt
```
设置 `port 0` 关闭明文端口,强制所有连接走 TLS。集群模式下还需要配置 `tls-cluster yes` 和 `tls-replication yes`。Redis 7.0 进一步增强了 TLS 支持,集群总线通信也可以走加密通道。
## 认证与授权
### 密码认证
最基本的安全措施,也是 Redis 安全加固 checklist 的第一条:
```bash
# redis.conf
requirepass your_strong_password
# 运行时设置(重启失效)
CONFIG SET requirepass your_strong_password
# 连接时指定密码
redis-cli -a your_strong_password
```
注意:`-a` 参数会触发警告,密码可能出现在进程列表和日志中。建议用 `REDISCLI_AUTH` 环境变量代替:
```bash
export REDISCLI_AUTH=your_strong_password
redis-cli
```
密码强度要求:至少 16 位,混合大小写字母、数字和特殊字符。`requirepass` 的密码以明文存储在配置文件中,所以配置文件权限也要收紧。
### ACL 精细权限控制
Redis 6.0 引入 ACL(Access Control List),替代了之前只有一个全局密码的模式,是实现 Redis 安全最小权限原则的关键:
```bash
# 创建只读用户,只能访问 user: 开头的 key
ACL SETUSER readonly on >password1 ~user:* +@read
# 创建业务用户,只能操作特定前缀的 key
ACL SETUSER app_user on >app_password ~order:* +@read +@write +@string +@hash
# 创建管理员
ACL SETUSER admin on >admin_password ~* +@all
# 查看所有用户
ACL LIST
# 删除用户
ACL DELUSER readonly
```
ACL 的权限粒度可以精确到命令组和 key 模式。`+@read` 表示所有读命令,`+@write` 表示所有写命令,`+@all` 表示全部命令。用 `ACL CAT` 查看所有命令组。
实际部署中,为每个业务应用创建独立的 ACL 用户,遵循最小权限原则。一个只做缓存的业务不需要 FLUSHALL 权限。Redis 7.0 还支持 ACL 规则持久化到文件,通过 `aclfile` 配置项指定,比每次重启都重新配置更可靠。
### 禁用和重命名危险命令
这是防止 Redis 被攻击者利用的关键配置:
```bash
# redis.conf
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command CONFIG ""
rename-command SHUTDOWN ""
rename-command DEBUG ""
# 或者重命名为难猜的名字
rename-command FLUSHALL "a9b8c7d6e5_FLUSHALL"
```
禁用比重命名更安全。如果用重命名,新的命令名不要出现在代码和日志中。CONFIG 命令尤其危险,攻击者可以通过它修改 requirepass 实现持久化后门——先把密码改成自己知道的值,再修改 dir 指向 /root/.ssh/,dbfilename 设为 authorized_keys,执行 BGSAVE 写入 SSH 公钥。这就是 CVE-2015-4335 的经典攻击链。
## 数据安全
### 持久化策略
```bash
# RDB 快照
save 900 1
save 300 10
save 60 10000
# AOF 日志
appendonly yes
appendfsync everysec
```
RDB 适合做备份,AOF 适合做数据安全。生产环境建议两者都开,AOF 保证最多丢 1 秒数据,RDB 做快速恢复的兜底。
注意:AOF 文件可能包含敏感数据(如密码明文),需要控制文件访问权限。
### 加密持久化文件
Redis 本身不提供数据加密,需要在文件系统层面解决:
```bash
# 限制文件权限
chmod 700 /var/lib/redis
chmod 600 /var/lib/redis/dump.rdb
chmod 600 /var/lib/redis/appendonly.aof
# 文件系统加密(Linux)
# 使用 LUKS 或 eCryptfs 加密 Redis 数据目录
```
如果数据敏感性高(如用户信息、Token),在写入 Redis 前做应用层加密。读取时解密,Redis 只存密文。这样即使持久化文件被窃取,也无法直接获取明文数据。
### 备份与恢复
```bash
# 定时备份 RDB
0 2 * * * cp /var/lib/redis/dump.rdb /backup/dump_$(date +\%Y\%m\%d).rdb
# 备份到远程
rsync -avz /backup/ user@remote:/backup/
```
备份文件也要控制权限,最好加密后传输。恢复时注意检查 RDB 文件完整性,避免被篡改的备份文件引入恶意数据。用 `redis-check-rdb` 工具校验 RDB 文件,用 `redis-check-aof` 校验 AOF 文件。
## 运行时加固
### 最小权限运行
```bash
# 创建专用用户
useradd -r -s /bin/false redis
# 设置文件归属
chown -R redis:redis /var/lib/redis
chown redis:redis /etc/redis/redis.conf
# 用非 root 用户启动
sudo -u redis redis-server /etc/redis/redis.conf
```
Redis 不需要 root 权限。用 root 运行 Redis 一旦被攻破,攻击者可以直接拿到服务器控制权,这就是为什么 Redis 安全加固必须包含权限降级。
### 文件权限收紧
```bash
chmod 600 /etc/redis/redis.conf
chmod 700 /var/lib/redis
chmod 600 /var/log/redis/redis.log
```
配置文件包含密码等敏感信息,必须限制读写权限。日志文件可能包含查询内容,也要保护。
### 系统级隔离
用 systemd 的安全选项加强隔离:
```ini
[Service]
User=redis
Group=redis
ExecStart=/usr/bin/redis-server /etc/redis/redis.conf
ProtectSystem=full
ReadWritePaths=/var/lib/redis
NoNewPrivileges=true
PrivateTmp=true
```
`NoNewPrivileges=true` 防止子进程提权,`PrivateTmp=true` 隔离临时目录。Docker 部署时同理,不要用 `--privileged` 参数,用 `--user` 指定非 root 用户,用 `--cap-drop=ALL` 去掉不必要的 Linux capabilities。
## 监控与审计
### 慢查询监控
```bash
# redis.conf
slowlog-log-slower-than 10000
slowlog-max-len 128
# 查看慢查询
SLOWLOG GET 10
```
慢查询日志可以帮助发现异常操作。突然出现的慢查询可能是攻击者在执行大量 KEYS * 扫描或 DEL 删除。
### Prometheus + Grafana 监控
```yaml
# prometheus.yml
scrape_configs:
- job_name: 'redis'
static_configs:
- targets: ['localhost:9121']
# 告警规则
groups:
- name: redis_alerts
rules:
- alert: RedisTooManyConnections
expr: redis_connected_clients > 100
for: 1m
labels:
severity: warning
- alert: RedisSuspiciousCommands
expr: rate(redis_commands_processed_total[5m]) > 10000
for: 2m
labels:
severity: critical
```
关键监控指标:连接数突增、内存使用异常、命令执行频率异常、主从切换。连接数异常暴增可能是在做端口扫描或暴力破解密码,命令频率异常可能是攻击者在批量导出数据。
### 操作审计
Redis 本身的审计能力有限,建议在以下层面补充:
- 网络层:记录所有连接来源 IP,排查可疑来源
- 应用层:在业务代码中记录关键操作,特别是写入和删除操作
- 系统层:用 auditd 监控 redis.conf 文件变更,防止配置被篡改
如果合规要求严格,可以考虑 Redis 企业版的审计日志功能,或用第三方审计代理。
## 集群安全
### 主从复制认证
```bash
# 主节点配置
masterauth your_master_password
requirepass your_master_password
# 从节点配置
requirepass your_slave_password
masterauth your_master_password
```
主从之间必须设置认证。没有认证的复制关系,攻击者可以伪装成从节点拉取全量数据,这相当于直接把数据库内容交给了攻击者。
### 哨兵模式安全
```bash
# sentinel.conf
sentinel auth-pass mymaster your_master_password
```
哨兵也需要配置认证密码,否则攻击者可以操控哨兵触发故障转移,把主节点切换到自己控制的服务器,实现中间人攻击。
### 集群模式安全
Redis 7.0 开始支持集群 TLS,集群总线通信也走加密通道:
```bash
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
tls-cluster yes
tls-replication yes
```
集群模式下的安全比单机更复杂,因为节点间通信也需要保护。Redis 7.2+ 还支持集群总线端口的 TLS 认证,确保集群内部通信不被窃听。
## 安全加固优先级
按紧迫程度排序,这份 Redis 安全加固 checklist 可以直接参考:
1. **立即做**:绑定内网 IP + 开启密码认证 + 禁用危险命令。不做这三条就是在等被入侵
2. **尽快做**:配置防火墙 + 使用非 root 用户 + 收紧文件权限。降低被攻破后的影响范围
3. **逐步做**:开启 TLS + 配置 ACL + 部署监控告警。提升整体安全水位
4. **持续做**:更新版本修复 CVE + 审计日志 + 定期安全扫描。保持安全性不退化
## 已知安全漏洞
Redis 历史上几个重要的安全漏洞,面试和实战都会遇到:
- **CVE-2015-4335**:未授权访问写入 SSH 公钥和 cron 任务,这是最经典也最常被利用的 Redis 安全漏洞。攻击条件极其简单:Redis 暴露公网 + 无密码
- **CVE-2022-0543**:Debian/Ubuntu 打包的 Lua 沙箱逃逸,可以在 Redis 中执行任意代码。影响范围广,因为大部分 Linux 发行版都用系统包管理器安装 Redis
- **CVE-2023-41053**:Lua 脚本库堆栈溢出,可导致拒绝服务
- **CVE-2025-32023**:Redis 7.4.x 之前的 Lua 脚本 eval 逃逸漏洞,最新一轮安全修复
面试中被问到 Redis 安全,提到 CVE-2015-4335 说明你理解问题的根源:默认配置不安全。提到 ACL 说明你跟进 Redis 6.0+ 新版本特性。提到加固优先级说明你有生产环境实战经验。
## 应急响应
发现 Redis 被入侵时的处理步骤:
1. **立即断网**:`iptables -A INPUT -p tcp --dport 6379 -j DROP`,先止血
2. **保留现场**:`SLOWLOG GET 100`、`CLIENT LIST`、`INFO` 记录当前状态,为后续分析保留证据
3. **检查后门**:查 crontab、SSH authorized_keys、.bashrc 是否被篡改,这是 Redis 被入侵后最常见的持久化手段
4. **分析入侵路径**:检查 `CONFIG GET dir` 和 `CONFIG GET dbfilename` 是否被改过,确认数据文件是否被指向了系统敏感目录
5. **清除恢复**:在确保后门清除后,修改密码重启 Redis,从备份恢复数据
6. **加固复盘**:按上面的安全加固 checklist 逐项检查,堵住入侵路径
大多数 Redis 被入侵事件的根因都是:公网暴露 + 无密码 + CONFIG 命令可用。攻击者通过 CONFIG SET dir /root/.ssh/ CONFIG SET dbfilename authorized_keys 写入 SSH 公钥实现持久化控制。理解这个攻击链,就知道为什么禁用 CONFIG 命令是 Redis 安全加固的核心措施。
服务端5月28日 09:37
Redis缓存穿透、击穿、雪崩有什么区别?Redis 缓存策略是使用 Redis 作为缓存时的核心问题,需要解决缓存穿透、缓存击穿、缓存雪崩等问题,同时需要设计合理的缓存更新策略。
## 缓存穿透:查不存在的数据怎么办?
缓存穿透是指查询一个数据库和缓存中都不存在的数据,请求每次都会穿过缓存直接打到数据库。典型场景:攻击者用不存在的 ID 批量请求接口,如 /user/-1。
**方案一:缓存空对象**
当数据库也查不到时,将空值写入缓存并设置短 TTL,避免同一 key 反复穿透。
```java
public User getUserById(Long id) {
User user = redis.get("user:" + id);
if (user != null) {
return "NULL".equals(user) ? null : user;
}
user = db.queryUserById(id);
if (user == null) {
redis.set("user:" + id, "NULL", 300); // 缓存空对象,5分钟过期
} else {
redis.set("user:" + id, user, 3600);
}
return user;
}
```
注意:空对象 TTL 不宜过长,否则该 key 对应的真实数据写入后,缓存仍是空值,导致数据不一致。可结合主动删除策略,写入数据时同步删除空缓存。
**方案二:布隆过滤器**
在缓存前加一层布隆过滤器,快速判断 key 是否可能存在。布隆过滤器说不存在则一定不存在,说存在则可能存在(有误判率)。
```java
if (!bloomFilter.mightContain("user:" + id)) {
return null; // 一定不存在,直接返回
}
User user = redis.get("user:" + id);
if (user != null) {
return user;
}
user = db.queryUserById(id);
if (user != null) {
redis.set("user:" + id, user, 3600);
}
return user;
```
选型建议:数据量小且查询模式固定 → 缓存空对象更简单;数据量大且 key 空间稀疏 → 布隆过滤器更省内存。
## 缓存击穿:热点 key 过期瞬间怎么办?
缓存击穿是指某个热点 key 在过期的一瞬间,大量并发请求同时查询该 key,全部穿透到数据库。与雪崩的区别:击穿是单个热点 key,雪崩是大面积 key 同时失效。
**方案一:互斥锁**
用分布式锁保证只有一个线程查库并回填缓存,其他线程等待后重试。
```java
public User getUserById(Long id) {
User user = redis.get("user:" + id);
if (user != null) {
return user;
}
String lockKey = "lock:user:" + id;
try {
if (redis.setnx(lockKey, "1", 10)) { // 获取锁,10秒自动释放
user = db.queryUserById(id);
redis.set("user:" + id, user, 3600);
} else {
Thread.sleep(100);
return getUserById(id); // 等待后重试
}
} finally {
redis.del(lockKey);
}
return user;
}
```
注意:锁要设超时时间防止死锁;重试要设上限防止无限递归。
**方案二:逻辑过期**
缓存永不过期,在值中存一个逻辑过期时间戳。读到逻辑过期数据时,异步更新缓存,当前请求返回旧数据。牺牲短暂一致性换取高可用。
```java
public User getUserById(Long id) {
String value = redis.get("user:" + id);
if (value != null) {
JSONObject json = JSON.parseObject(value);
if (json.getBoolean("expired")) {
asyncUpdateCache(id); // 异步更新,不阻塞当前请求
}
return json.getObject("data", User.class); // 返回旧数据
}
User user = db.queryUserById(id);
JSONObject json = new JSONObject();
json.put("data", user);
json.put("expired", false);
redis.set("user:" + id, json.toJSONString(), 3600);
return user;
}
```
选型建议:一致性要求高 → 互斥锁;可用性要求高、允许短暂脏读 → 逻辑过期。
## 缓存雪崩:大面积 key 同时失效怎么办?
缓存雪崩是指大量 key 在同一时间过期,或 Redis 宕机,导致请求全部打到数据库。
**方案一:过期时间加随机偏移**
给 TTL 加上随机值,避免大量 key 在同一时刻集中过期。
```java
int expire = 3600 + new Random().nextInt(600); // 3600~4200秒随机
redis.set("user:" + id, user, expire);
```
**方案二:缓存预热**
系统启动或低峰期提前加载热点数据到缓存,避免冷启动时流量直接打库。
```java
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点预热
public void warmUpCache() {
List<User> users = db.queryHotUsers();
for (User user : users) {
int expire = 3600 + new Random().nextInt(600);
redis.set("user:" + user.getId(), user, expire);
}
}
```
**方案三:高可用 + 降级**
- 部署 Redis Sentinel 或 Cluster,避免单点故障
- 数据库前加熔断限流,雪崩时快速失败而非拖垮数据库
- 使用本地缓存(Caffeine/Guava)作为二级缓存兜底
```java
public User getUserById(Long id) {
try {
User user = redis.get("user:" + id);
if (user != null) {
return user;
}
} catch (Exception e) {
log.error("Redis error", e);
return localCache.get("user:" + id); // 降级到本地缓存
}
User user = db.queryUserById(id);
redis.set("user:" + id, user, 3600);
return user;
}
```
## 缓存更新策略怎么选?
三种常见策略,适用场景不同:
**Cache Aside(旁路缓存)**:读时回填,写时删缓存。最常用,一致性较好。
```java
// 读
User user = redis.get("user:" + id);
if (user == null) {
user = db.queryUserById(id);
redis.set("user:" + id, user, 3600);
}
// 写
db.updateUser(user);
redis.del("user:" + user.getId()); // 删缓存而非更新缓存
```
为什么删缓存而不是更新?并发写时更新缓存可能出现旧值覆盖新值的问题,删除更安全,下次读时自然回填最新值。
**Write Through(写穿透)**:写数据时同步更新缓存和数据库。数据一致性强,但写延迟高,适合写少读多的场景。
**Write Behind(写回)**:先更新缓存,异步批量写入数据库。写性能极高,但有数据丢失风险,适合写密集且容忍少量丢失的场景(如浏览量计数)。
## 缓存和数据库不一致怎么处理?
缓存与数据库不一致是分布式系统的经典问题,根本原因是两者无法原子操作。
**方案一:延时双删**
先删缓存 → 更新数据库 → 延时再删缓存。第二次删除用于清除更新数据库期间被旧缓存回填的数据。
```java
public void updateUser(User user) {
redis.del("user:" + user.getId()); // 第一次删除
db.updateUser(user);
Thread.sleep(500); // 延时,确保读请求回填旧缓存完成
redis.del("user:" + user.getId()); // 第二次删除
}
```
缺点:延时时间难以精确设定,过长影响性能,过短仍可能不一致。
**方案二:订阅 Binlog**
通过 Canal 等中间件订阅数据库 Binlog,数据变更时自动更新或删除缓存。解耦业务代码,一致性更可靠。
```java
@CanalEventListener
public class CacheUpdateListener {
@ListenPoint(destination = "example", schema = "test", table = "user")
public void onEvent(CanalEntry.Entry entry) {
User user = parseUserFromBinlog(entry);
redis.del("user:" + user.getId()); // 或更新缓存
}
}
```
选型建议:一致性要求一般 → 延时双删够用;一致性要求高 → Binlog 方案更可靠。
## 面试怎么答?
被问到这三个概念时,建议按以下结构回答:
1. **先说定义**:穿透是数据不存在,击穿是热点 key 过期,雪崩是大面积 key 同时失效
2. **再说区别**:穿透查的是不存在的数据,击穿和雪崩查的是存在的数据;击穿是单个 key,雪崩是批量 key
3. **最后说方案**:穿透用空缓存或布隆过滤,击穿用互斥锁或逻辑过期,雪崩用随机过期+预热+高可用
追问方向:生产环境怎么监控缓存健康度?关注缓存命中率(低于 80% 需告警)、慢查询日志、内存使用率和 key 过期分布。服务端5月28日 09:37
Redis 如何进行监控和运维?Redis 监控和运维是保障线上稳定性的核心能力,面试中常从"监控哪些指标""用什么工具""遇到问题怎么排查"三个角度考察。
## 关键监控指标
### 内存指标
内存是 Redis 最核心的资源,重点监控以下项:
```bash
INFO memory
# 关键字段
used_memory # Redis 分配器分配的内存
used_memory_rss # 操作系统实际分配的内存
mem_fragmentation_ratio # 内存碎片率 = used_memory_rss / used_memory
maxmemory # 配置的最大内存限制
```
**碎片率阈值解读**:碎片率 > 1.5 说明碎片严重,需触发 `MEMORY PURGE` 或重启;碎片率 < 1.0 说明使用了 Swap,性能会急剧下降,应立即排查。
命中率直接反映缓存有效性:
```bash
keyspace_hits / (keyspace_hits + keyspace_misses)
```
命中率低于 90% 时需排查是否有过期 key 未清理、缓存穿透等问题。
### 性能与延迟指标
```bash
INFO stats
# instantaneous_ops_per_sec — 当前 QPS
# total_commands_processed — 累计处理命令数
# 延迟监控(Redis 2.8.13+)
CONFIG SET latency-monitor-threshold 100 # 超过 100ms 记录
LATENCY LATEST # 查看最近延迟事件
LATENCY DOCTOR # 诊断延迟原因
```
QPS 突降或延迟突增是故障的前兆,应设置基线告警。
### 连接与复制指标
```bash
INFO clients
# connected_clients — 当前连接数
# blocked_clients — 阻塞等待的客户端数
INFO replication
# master_repl_offset / slave_repl_offset — 主从复制偏移量差即同步延迟
# master_link_down_since_seconds — 主从断连时长,应为 0
```
连接数超过 maxclients 的 80% 就应告警;主从偏移量差持续增大说明同步瓶颈。
### 持久化指标
```bash
INFO persistence
# rdb_last_save_time — 最后 RDB 保存时间
# rdb_changes_since_last_save — 上次保存后变更数,过大说明 RDB 间隔过长
# aof_rewrite_in_progress — AOF 重写是否进行中
# aof_current_size / aof_base_size — AOF 文件大小,重写触发比默认 100%
```
## 监控工具选型
### 原生命令
- **INFO**:全局状态快照,按 section 查看内存、客户端、复制等
- **SLOWLOG**:慢查询日志,`SLOWLOG GET 10` 查看最近 10 条,关注 usec 字段
- **MONITOR**:实时命令流,生产环境慎用(会降低吞吐约 50%),仅用于紧急排查
- **LATENCY**:延迟监控框架,可记录延迟事件并给出诊断建议
### 可观测性体系
**Prometheus + Redis Exporter + Grafana** 是生产环境主流方案:
```bash
# 部署 Redis Exporter
docker run -d --name redis-exporter \
-e REDIS_ADDR=redis://localhost:6379 \
prom/redis-exporter
# Prometheus 抓取配置
scrape_configs:
- job_name: redis
static_configs:
- targets: ["localhost:9121"]
```
Grafana 可导入 Redis Dashboard(ID: 11835),覆盖内存、QPS、命中率、延迟等核心面板。
**Redis Insight** 是 Redis 官方可视化工具,支持内存分析、CLI、Profiler,适合开发调试阶段。
### 哨兵监控
使用 Sentinel 时,除上述指标外还需关注:
```bash
SENTINEL masters # 主节点状态
SENTINEL slaves <master> # 从节点状态
SENTINEL sentinels <master> # 哨兵节点状态
```
重点监控主观下线/客观下线事件、故障转移耗时。
## 常见故障排查
### 内存不足
现象:`OOM command not allowed when used memory > maxmemory`
```bash
INFO memory # 查看内存使用
redis-cli --bigkeys # 扫描大 Key
MEMORY USAGE <key> # 查看单个 key 内存占用
```
解决:调整 `maxmemory`、设置淘汰策略 `allkeys-lru`、用 `UNLINK` 替代 `DEL` 异步删除大 Key(避免阻塞主线程)、分批 `SCAN` 删除。
### 慢查询
```bash
SLOWLOG GET 10 # 查看慢查询
CONFIG GET slowlog-log-slower-than # 查看阈值
```
常见原因:`KEYS *` 全量扫描、大 Key 操作(HGETALL 千万级 hash)、`SORT` 命令。替代方案:`SCAN` 替代 `KEYS`,`HSCAN` 替代 `HGETALL`,Pipeline 减少网络往返。
### 主从同步延迟
```bash
INFO replication
# master_repl_offset 与 slave_repl_offset 的差值
```
增大 `repl-backlog-size`、优化网络、避免主节点大 Key 写入导致积压。
### 连接数打满
```bash
INFO clients
CONFIG GET maxclients
```
设置 `timeout` 自动断开空闲连接、排查连接泄漏、适当调大 `maxclients`。
## 运维操作要点
### 备份恢复
RDB 备份用 `BGSAVE`(后台执行,不阻塞主线程),AOF 备份直接拷贝 `.aof` 文件。恢复时停止 Redis → 替换文件 → 启动。生产建议 RDB + AOF 混合持久化(Redis 4.0+)。
### 数据迁移
- **MIGRATE** 命令:迁移单个或多个 key,原子操作
- **RedisShake**:阿里开源工具,支持全量+增量同步,适合大规模迁移
```bash
# RedisShake 配置
source.type: standalone
source.address: source.redis.com:6379
target.type: standalone
target.address: target.redis.com:6379
./redis-shake.linux -type=sync -conf=shake.conf
```
### 集群扩缩容
```bash
# 添加节点
redis-cli --cluster add-node <new-ip>:<port> <exist-ip>:<port>
# 重新分配槽位
redis-cli --cluster reshard <exist-ip>:<port> \
--cluster-from <src-node-id> \
--cluster-to <dst-node-id> \
--cluster-slots 1000
```
删除节点前必须先迁走其槽位,否则拒绝删除。
## 性能优化配置
```bash
# 内存
maxmemory 2gb
maxmemory-policy allkeys-lru
echo never > /sys/kernel/mm/transparent_hugepage/enabled # 关闭 THP
# 持久化
appendonly yes
appendfsync everysec # 折中方案,最多丢 1 秒数据
auto-aof-rewrite-min-size 64mb
# 网络
tcp-backlog 511
tcp-keepalive 300
timeout 300 # 空闲连接超时
```
告警建议:内存使用率 > 80%、QPS 下降 > 50%、延迟 > 100ms、连接数 > 80% maxclients。
以上内容覆盖了 Redis 监控运维的核心考察点,面试中回答时应遵循"指标→工具→排查→优化"的递进逻辑,展示系统性思维而非零散知识点。服务端5月28日 09:36
Redis 的过期策略和内存淘汰机制是什么?如何选择合适的策略?Redis 的过期策略和内存淘汰机制是两个不同层面的问题:过期策略决定「过期 key 何时被删」,内存淘汰策略决定「内存不够时删谁」。
## 过期策略
Redis 采用**惰性删除 + 定期删除**的组合策略,不使用定时删除。
**惰性删除**:访问 key 时才检查是否过期。由 `expireIfNeeded()` 函数实现,所有读写命令执行前都会调用。优点是 CPU 友好,缺点是过期 key 若不被访问就永远占内存。
**定期删除**:每秒执行约 10 次(受 `hz` 配置控制),每次随机抽取 20 个设置了过期时间的 key 检查,若过期则删除。若本轮过期 key 超过 25%,则继续抽样,直到低于 25% 或超时(25ms)。由 `activeExpireCycle()` 函数实现。
为什么不单独用定时删除?创建大量定时器会严重消耗 CPU 资源,Redis 出于性能考虑弃用此方案。
二者配合的效果:定期删除保证过期 key 不会长期滞留,惰性删除兜底处理定期删除遗漏的 key。
## 内存淘汰策略
当 Redis 内存使用达到 `maxmemory` 限制时,根据淘汰策略决定删除哪些 key。共 8 种策略:
| 策略 | 淘汰范围 | 算法 | 适用场景 |
|------|---------|------|----------|
| noeviction | 不淘汰 | - | 数据不能丢失 |
| allkeys-lru | 全部 key | LRU | 纯缓存,热点集中 |
| allkeys-lfu | 全部 key | LFU | 纯缓存,访问频率差异大 |
| allkeys-random | 全部 key | 随机 | 所有 key 访问概率相近 |
| volatile-lru | 有过期时间的 key | LRU | 混合存储,保留持久数据 |
| volatile-lfu | 有过期时间的 key | LFU | 同上,优先淘汰低频临时数据 |
| volatile-random | 有过期时间的 key | 随机 | 临时数据随机淘汰 |
| volatile-ttl | 有过期时间的 key | TTL 最短 | 优先淘汰即将过期的 key |
### LRU 与 LFU 的实现
Redis 使用**近似 LRU**,并非精确 LRU。每个 key 记录最后访问时间戳(24bit lru 字段),淘汰时随机采样 N 个 key(默认 5 个),删除其中最久未访问的。采样数越大越接近精确 LRU,但 CPU 开销也越大。
LFU 在 Redis 4.0 引入,复用 lru 字段的高 16 位记录衰减时间、低 8 位记录访问计数器。计数器会随时间衰减,避免历史高频 key 永远不被淘汰。
### 如何选择
- **纯缓存场景**(数据全可丢失):`allkeys-lru`(推荐)或 `allkeys-lfu`
- **部分数据持久化**(重要数据不设过期时间):`volatile-lru` 或 `volatile-lfu`
- **数据绝对不能丢**:`noeviction`
- **所有 key 访问概率均匀**:`allkeys-random`
生产环境推荐优先考虑 `allkeys-lru`,绝大多数缓存场景都适用。若使用 Redis 4.0+ 且访问频率差异明显,`allkeys-lfu` 更精准。
用 `INFO memory` 命令监控内存使用,关注 `used_memory`、`used_memory_peak`、`maxmemory` 等指标。
## 追问
**Q:过期策略和内存淘汰策略的关系?**
过期策略处理的是「已过期的 key 何时删除」,是时间驱动的;内存淘汰策略处理的是「内存不足时删谁」,是空间驱动的。两者互补:即使过期策略遗漏了部分过期 key,内存淘汰策略也能在内存紧张时兜底清理。
**Q:为什么 Redis 的 LRU 是近似的?**
精确 LRU 需要维护全局链表,每次访问都要移动节点,O(1) 的 key 访问变成 O(n) 的链表操作。近似 LRU 随机采样 5 个 key 淘汰最旧的,牺牲少量精度换取 O(1) 的访问性能。实测表明近似 LRU 的命中率接近精确 LRU。
**Q:volatile 系列策略的一个潜在问题?**
如果没有 key 设置过期时间,volatile 策略等同于 noeviction,不会淘汰任何 key,可能导致内存满后写入全部失败。服务端5月28日 09:35
Redis 主从复制、哨兵和集群模式有什么区别?高可用方案怎么选?Redis 的主从复制、哨兵模式和集群模式是三种不同层次的高可用方案,它们解决的核心问题不同,选择时需要根据业务的数据规模、可用性要求和读写瓶颈来决定。
## 主从复制:解决读瓶颈和数据备份
主从复制是最基础的方案。一个 Redis 节点作为 Master 负责写操作,一个或多个 Slave 节点从 Master 同步数据,只提供读服务。
同步过程分两个阶段:首次连接时 Master 执行 `BGSAVE` 生成 RDB 快照发送给 Slave(全量同步);之后的写命令通过 repl-backlog 缓冲区持续发送给 Slave(增量同步)。如果 Slave 断开时间过长,缓冲区被覆盖,就会再次触发全量同步。
```bash
slaveof 192.168.1.100 6379
```
主从复制的核心价值是读写分离——把读流量分散到多个 Slave 上,同时 Slave 作为数据副本提供冗余。但它的致命问题是 Master 挂了之后需要人工介入,把某个 Slave 手动提升为 Master,还要修改应用配置指向新地址,恢复时间完全取决于运维响应速度。
适用场景:数据量不大、读多写少、能容忍短暂不可用的内部系统。比如配置中心、排行榜这类读远多于写的业务。
## 哨兵模式:在主从基础上实现自动故障转移
哨兵模式本质是主从复制 + 自动化运维。部署一组 Sentinel 进程(至少3个,奇数个),它们持续监控 Master 和 Slave 的状态,Master 故障时自动完成故障转移。
### 故障转移的核心机制:主观下线与客观下线
单个 Sentinel 检测到 Master 无响应(超过 `down-after-milliseconds`),标记为主观下线(SDOWN)。但这可能只是网络抖动,所以需要多个 Sentinel 互相确认——当超过 `quorum` 个数的 Sentinel 都认为 Master 下线时,才标记为客观下线(ODOWN),此时才会触发真正的故障转移。
### 故障转移流程
1. Sentinel 集群通过 Raft 协议选出一个 Leader Sentinel 执行转移
2. Leader 从 Slave 中选出新 Master(优先判断复制偏移量最大、连接最稳定的节点)
3. 对新 Master 执行 `SLAVEOF NO ONE`,将其提升为主节点
4. 通知其他 Slave 复制新 Master
5. 更新 Sentinel 的监控配置
```bash
port 26379
sentinel monitor mymaster 192.168.1.100 6379 2
sentinel down-after-milliseconds mymaster 30000
sentinel failover-timeout mymaster 180000
```
配置中 `2` 表示至少需要 2 个 Sentinel 同意才能判定客观下线。生产环境建议至少部署 3 个 Sentinel,跨机房分布,避免 Sentinel 自身成为单点。
客户端需要使用 Sentinel 模式连接——先从 Sentinel 获取当前 Master 地址,再连接 Master。当故障转移完成后,Sentinel 会通知客户端切换到新 Master。
### 局限性
哨兵解决了自动故障转移,但写操作仍然只能走 Master 单点,存储容量也受限于单机内存。当数据量超过单机内存,或者写 QPS 成为瓶颈时,哨兵模式就不够用了。
适用场景:数据量在单机内存范围内,但需要高可用、要求故障自动恢复的业务。大部分中小规模的生产环境用哨兵模式就足够了。
## 集群模式:解决写瓶颈和存储瓶颈
Redis Cluster 是真正的分布式方案,通过数据分片把数据分散到多个 Master 节点上,每个 Master 持有部分数据,同时可以有自己的 Slave 做副本。
### 哈希槽分片机制
Cluster 把整个键空间划分为 16384 个哈希槽,每个 Master 负责一部分槽。客户端写入一个 key 时,用 `CRC16(key) % 16384` 计算出槽编号,然后路由到负责该槽的节点。
当集群需要扩容时,只需把一部分哈希槽从旧节点迁移到新节点,不需要停服。收缩时反向操作。
### 请求重定向:MOVED 和 ASK
客户端向错误节点发送请求时,该节点会返回重定向指令:
- **MOVED**:表示该槽已经永久迁移到新节点,客户端应更新本地缓存
- **ASK**:表示该槽正在迁移中(临时状态),客户端本次请求转发到目标节点,但不更新缓存
```bash
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
```
创建集群:
```bash
redis-cli --cluster create \
192.168.1.1:7000 192.168.1.2:7001 192.168.1.3:7002 \
192.168.1.4:7003 192.168.1.5:7004 192.168.1.6:7005 \
--cluster-replicas 1
```
### 节点通信与故障检测
Cluster 采用 Gossip 协议进行节点间通信,每个节点定期向其他节点发送 PING 消息交换集群状态。当某个 Master 被超过半数 Master 标记为故障(PFAIL → FAIL),其 Slave 会自动提升为新 Master。
### 限制
- 不支持跨槽的多键操作(`MGET`、`MSET` 等操作的 key 必须在同一槽,可用 hash tag `{tag}` 解决)
- 不支持跨槽的 `WATCH` 事务
- `KEYS`、`SCAN` 等命令只能看到当前节点的数据
- 最少需要 6 个节点(3 主 3 从)才能保证高可用
适用场景:数据量超过单机内存、写 QPS 单机无法承载、需要在线扩缩容的大规模业务。
## 三种方案的核心区别
| 特性 | 主从复制 | 哨兵模式 | 集群模式 |
|------|---------|---------|---------|
| 故障转移 | 手动 | 自动(Sentinel 选举) | 自动(Gossip + 投票) |
| 写扩展 | 不支持 | 不支持 | 支持(数据分片) |
| 存储扩展 | 受限于单机 | 受限于单机 | 支持(水平扩容) |
| 最小部署 | 2 节点 | 2 Redis + 3 Sentinel | 6 节点(3主3从) |
| 运维复杂度 | 低 | 中 | 高 |
| 客户端要求 | 普通 | 支持 Sentinel 协议 | 支持 Cluster 协议 |
## 面试中如何回答
面试官问这个问题,核心考察的是你对 Redis 高可用演进路径的理解,而不只是背诵概念。建议从问题驱动的角度组织回答:
**第一步**,先说清楚每种方案解决什么问题——主从解决读瓶颈,哨兵解决自动故障转移,集群解决写瓶颈和存储瓶颈。这三个是递进关系,不是并列选择。
**第二步**,结合实际场景给出选型依据:数据量小 + 可容忍短时不可用 → 主从;数据量小 + 要求高可用 → 哨兵;数据量大或写 QPS 高 → 集群。
**第三步**,补充生产经验。比如哨兵部署要跨机架、集群扩容时注意槽迁移对大 key 的影响、客户端重试策略要处理 MOVED/ASK 重定向等。这些细节能体现你的实战深度。服务端5月28日 09:35
Redis 是什么?核心特点有哪些?## Redis 是什么?
Redis(Remote Dictionary Server)是用 C 语言编写的开源高性能键值存储系统。它将数据全部驻留在内存中,通过单线程事件驱动模型处理请求,单机 QPS 可达 10 万+。与 Memcached 等纯缓存不同,Redis 提供持久化、事务、Lua 脚本、发布订阅等能力,既能做缓存,也能承担消息队列、会话存储、排行榜等业务角色。
## 主要特点
### 基于内存的高性能读写
Redis 所有数据存储在内存中,内存访问延迟在纳秒级,远低于磁盘的毫秒级延迟。配合 I/O 多路复用(epoll/kqueue)和单线程事件循环,避免了线程切换和锁竞争的开销,这是 Redis 高吞吐的根本原因。
Redis 6.0 引入了多线程 I/O,但命令执行仍是单线程——多线程只负责网络读写,执行命令本身仍串行执行,既保证了原子性,又提升了网络 I/O 瓶颈下的吞吐。
### 丰富的数据结构
Redis 远不止简单的 key-value,它原生支持 9 种数据类型:
| 类型 | 典型场景 |
|------|---------|
| String | 缓存、计数器、分布式锁 |
| Hash | 对象存储(用户信息、商品属性) |
| List | 消息队列、最新消息排行 |
| Set | 去重、交集/并集运算(共同关注) |
| ZSet | 排行榜、延迟队列 |
| Bitmap | 签到、在线状态 |
| HyperLogLog | UV 统计(允许误差) |
| Geo | 附近的人、距离计算 |
| Stream | 消息队列(支持消费者组) |
每种类型底层有对应的编码实现,Redis 会根据数据量自动选择编码以节省内存。例如 List 在元素少时使用 ziplist(紧凑列表),元素多时切换为 quicklist。
### 双模式持久化
Redis 提供两种持久化机制,可以单独使用也可以组合使用:
**RDB(快照)**:在指定时间间隔内将内存数据集写入磁盘的二进制文件。优点是文件紧凑、恢复速度快;缺点是两次快照之间的数据可能丢失。
**AOF(追加日志)**:将每个写命令追加到日志文件末尾。数据安全性更高,最多丢失 1 秒数据(everysec 策略);但日志文件体积更大,恢复速度较慢。
生产环境通常两者同时开启:RDB 用于快速恢复,AOF 用于保证数据完整性。Redis 4.0+ 的混合持久化模式,在 AOF 重写时将当前数据以 RDB 格式写入,后续增量命令以 AOF 格式追加,兼顾了恢复速度和数据安全。
### 原子性操作与事务
Redis 单个命令是原子性的,要么执行成功要么不执行。对于需要多个命令原子执行的场景,Redis 提供了事务机制:
```
MULTI # 开启事务
SET key1 v1
SET key2 v2
EXEC # 提交事务
```
`MULTI` 到 `EXEC` 之间的命令会按顺序串行执行,不会被其他客户端打断。但需要注意,Redis 事务不支持回滚——如果某条命令执行失败,其余命令仍会继续执行。这是 Redis 设计者有意为之,目的是保持简单高效。
配合 `WATCH` 命令可以实现乐观锁:在事务执行前监控 key,若 key 被其他客户端修改,事务自动取消。
### 主从复制与高可用
Redis 通过主从复制实现读写分离和数据冗余:
- **全量同步**:从库首次连接主库时,主库生成 RDB 快照发送给从库
- **增量同步**:主库将新的写命令持续发送给从库(基于 replication offset 和 repl_backlog)
Redis Sentinel 在主从基础上实现自动故障转移:监控主库状态,主库宕机时自动选举新主库并通知客户端切换。选举依据优先级、复制偏移量、run_id 三个维度排序。
### 集群与水平扩展
Redis Cluster 通过哈希槽(Hash Slot)实现数据分片,共 16384 个槽位分配到不同节点:
```bash
# 集群中查看 key 所属槽位
CLUSTER KEYSLOT mykey
```
每个节点负责一部分槽位,客户端通过 `MOVED` 重定向找到目标节点。集群支持自动故障检测和转移,当某个主节点不可用时,其从节点自动升主。
### 发布订阅与 Stream
Redis 内置 Pub/Sub 模式,支持频道订阅和模式匹配订阅:
```bash
SUBSCRIBE channel1 # 订阅频道
PUBLISH channel1 "hello" # 发布消息
```
Pub/Sub 的局限是消息不持久化,离线客户端无法收到历史消息。Redis 5.0 引入的 Stream 类型弥补了这一缺陷——它支持消息持久化、消费者组、消息确认(ACK),可以作为轻量级消息队列使用。
### Lua 脚本支持
Redis 支持在服务端执行 Lua 脚本,脚本在执行期间不会被其他命令打断,适合需要原子性的复合操作:
```lua
-- 限流脚本示例:每秒最多允许 N 次请求
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local count = redis.call("INCR", key)
if count == 1 then
redis.call("EXPIRE", key, 1)
end
if count > limit then
return 0
end
return 1
```
### 内存优化机制
Redis 使用多种策略控制内存使用:共享对象池复用小整数(0-9999)、ziplist/listpack 紧凑编码节省小数据内存、惰性删除避免大 key 阻塞主线程。配合 `maxmemory` 配置和淘汰策略(如 allkeys-lru、volatile-lfu),可以在内存不足时自动回收低价值 key。
## 面试追问方向
**Redis 为什么快?** 内存存储 + I/O 多路复用 + 单线程避免锁竞争 + 高效数据结构编码。
**Redis 为什么早期用单线程?** CPU 不是瓶颈,内存和网络才是;单线程避免上下文切换和锁开销,实现简单可靠。
**RDB 和 AOF 怎么选?** 对数据完整性要求高选 AOF,对恢复速度要求高选 RDB,生产环境建议混合持久化同时开启。服务端5月28日 09:34
Redis 有哪些数据类型?各自的底层实现和使用场景是什么?Redis 提供了丰富的数据类型,面试中经常考察每种类型的底层编码、转换条件以及典型场景。下面从五大数据类型讲起,再覆盖后续新增的特殊类型。
## String:最基础的键值类型
String 是 Redis 最简单的数据类型,可以存储字符串、整数、浮点数,最大 512MB。
**底层实现**:Redis 没有直接使用 C 语言字符串,而是自己实现了 SDS(Simple Dynamic String)。SDS 在 C 字符串末尾 \0 的基础上增加了 len 和 alloc 字段:len 记录已用长度,alloc 记录分配总空间。这样做带来了三个好处——获取字符串长度从 O(N) 降为 O(1)、二进制安全(不依赖 \0 判断结尾)、空间预分配和惰性释放减少内存重分配次数。String 有三种编码:int(8字节以内整数)、embstr(44字节以内短字符串,一次分配内存)、raw(超过 44 字节,两次分配)。
**使用场景**:缓存用户信息或配置、分布式锁(SETNX + 过期时间)、计数器(INCR 原子自增)、Session 共享。
## List:有序可重复的列表
List 是一个按插入顺序排序的字符串元素集合,支持从两端推入和弹出。
**底层实现**:Redis 3.2 之前用 ziplist(元素少时)或 linkedlist(元素多时)。3.2 之后统一改用 quicklist,它是一个由 ziplist 节点组成的双向链表,兼顾了 ziplist 的省内存和 linkedlist 的快速插入删除。Redis 7.0 进一步将 ziplist 替换为 listpack,解决了 ziplist 的级联更新问题。
**使用场景**:消息队列(LPUSH + BRPOP 实现阻塞队列)、最新消息列表(如朋友圈时间轴)、栈(LPUSH + LPOP)和队列(LPUSH + RPOP)操作。
## Hash:字段-值映射
Hash 是键值对的集合,适合存储对象属性,类似 Java 的 HashMap。
**底层实现**:元素数量少且单个元素体积小时使用 listpack(原 ziplist),超过阈值切换为 hashtable。hashtable 采用链地址法解决哈希冲突,Redis 还实现了渐进式 rehash——维护 ht[0] 和 ht[1] 两个哈希表,rehash 期间每次增删改查操作都会顺带迁移一部分桶,避免一次性迁移造成阻塞。
**使用场景**:存储对象(用户信息、商品详情,比 String+JSON 更节省内存,可以只读写单个字段)、购物车(用户ID为key,商品ID为field,数量为value)。
## Set:无序唯一集合
Set 是字符串元素的无序集合,元素不重复。
**底层实现**:当所有元素都是整数且数量不超过 512 时使用 intset(有序整数数组,内存紧凑),否则切换为 hashtable(value 存 null,只用 key)。
**使用场景**:标签系统、共同好友/共同关注(SINTER 取交集)、抽奖(SRANDMEMBER 随机取)、去重(SADD 自动去重)。
## ZSet:有序唯一集合
ZSet 在 Set 的基础上每个元素关联一个 score,按 score 排序,是 Redis 中最复杂的数据类型之一。
**底层实现**:元素少且小时使用 listpack,否则使用 skiplist + hashtable 的组合。hashtable 提供 O(1) 的成员查找,skiplist 提供范围查询能力。跳表的层高通过随机算法确定(每层晋升概率 1/4),平均 O(logN) 查找。为什么不用红黑树?跳表实现更简单、范围查询更方便(只需要在底层链表上遍历)、插入删除只需修改相邻节点指针。
**使用场景**:排行榜(游戏积分、热搜榜)、延时队列(score 存到期时间,ZRANGEBYSCORE 取到期任务)、带权重的消息队列。
## 五大类型对比
| 类型 | 底层编码 | 是否有序 | 是否可重复 | 核心操作复杂度 |
|------|----------|----------|------------|----------------|
| String | int/embstr/raw | - | - | O(1) |
| List | listpack/quicklist | 插入序 | 可重复 | 两端 O(1),中间 O(N) |
| Hash | listpack/hashtable | 无序 | field不可重复 | O(1) |
| Set | intset/hashtable | 无序 | 不可重复 | O(1) |
| ZSet | listpack/skiplist+hashtable | score有序 | 不可重复 | 查找O(1),范围O(logN+M) |
## Bitmap:位级操作
Bitmap 不是独立的数据类型,而是 String 上的位操作扩展。
**底层实现**:基于 String(SDS),每个 bit 对应一个偏移量。SETBIT 将指定偏移位设为 0 或 1,BITCOUNT 统计设为 1 的位数。
**使用场景**:用户每日签到(一个用户一年只需 365 bit ≈ 46 字节)、统计连续签到天数、在线状态判断、布隆过滤器。
## HyperLogLog:基数估算
HyperLogLog 用极小的内存估算集合基数(不重复元素数量),标准误差约 0.81%。
**底层实现**:基于概率算法,固定占用 12KB 内存(16384 个桶,每个 6 bit),不会随元素增多而增长。
**使用场景**:网站 UV 统计(百万级 UV 只需 12KB)、大屏数据去重计数。不需要精确值时优先选择,比 Set 存储节省几个数量级内存。
## Geo:地理位置
Geo 用于存储地理位置信息并进行距离计算。
**底层实现**:基于 ZSet,使用 GeoHash 编码将经纬度转为 52 位整数作为 score。GEOADD 本质是 ZADD,GEORADIUS 本质是 ZRANGEBYSCORE + 距离计算。
**使用场景**:附近的人/店铺、距离计算、地理围栏。
## Stream:消息流
Stream 是 Redis 5.0 新增的数据类型,专门为消息队列场景设计,可以看作轻量版 Kafka。
**底层实现**:使用 radix tree(基数树)+ listpack 实现。每条消息有全局递增的 ID(时间戳-序号),支持消费组(Consumer Group),同一条消息可被不同消费组各消费一次。
**使用场景**:消息队列(相比 List 的 BRPOP,Stream 支持消费组、消息确认、历史回溯,解决了 List 无法 ACK 和无法回溯的问题)、事件日志、实时数据管道。
**与 List 做消息队列的区别**:List 不支持消费组,一条消息只能被一个消费者取走;Stream 支持消费组,多条消息可分发到组内不同消费者并行处理,且消费者断线后未确认的消息可以转交给其他消费者。
---
面试高频追问:Redis 为什么用跳表不用红黑树实现 ZSet?——跳表实现简单、范围查询只需遍历底层链表、插入删除只需修改相邻指针,而红黑树的旋转操作更复杂且范围查询需要中序遍历。Hash 和 List 的编码转换阈值是多少?——Hash 在 field 数量超过 hash-max-ziplist-entries(默认128)或单个 field 超过 hash-max-ziplist-value(默认64字节)时从 listpack 转为 hashtable;List 的 quicklist 每个 node 的 ziplist 大小由 list-max-ziplist-size 控制。服务端5月28日 09:33
Redis 性能优化有哪些策略?Redis 性能优化是面试中的高频考点,也是生产环境中必须掌握的实战技能。本文从内存、网络、CPU、持久化、集群、监控、OS、客户端、架构九大维度系统梳理优化策略,并结合面试常见追问给出关键知识点。
## 内存优化
**选择合适的数据结构**是内存优化的第一步:
- 用 Hash 存储对象字段,比多个 String 节省内存(底层 ziplist/listpack 编码)
- 用 ZSet 做排行榜,避免 List 排序开销
- 用 Bitmap 存布尔型标记位,空间仅为 Set 的 1/64
- 用 HyperLogLog 做基数统计,固定 12KB 即可统计亿级去重
**控制键名长度**:键名占用内存不可忽视,但不宜过度缩写。`user:1001:profile` 比 `u:1001:pf` 更可维护,而 `user:profile:1001:detail:info` 则过长。
**调整紧凑编码阈值**:Hash、List 在元素少时自动使用 ziplist(Redis 7.0+ 为 listpack),通过 `hash-max-ziplist-entries`、`hash-max-ziplist-value` 调整触发阈值,在内存和性能间取平衡。
**设置过期时间**:为临时数据设置 TTL,避免无用数据长期占内存。用 `EXPIRE`/`EXPIREAT` 管理,配合惰性删除 + 定期删除策略回收内存。
**淘汰策略选择**:Redis 提供八种淘汰策略(Redis 4.0+),面试常考:
- `allkeys-lru`:通用场景首选,淘汰最久未用的 key
- `volatile-lru`:只淘汰设了过期时间的 key,适合缓存+持久共存
- `allkeys-lfu`(Redis 4.0+):淘汰访问频率最低的 key,热点数据友好
- `noeviction`:默认策略,内存满拒绝写入,数据不能丢的场景使用
**延迟删除(Lazy Free)**:Redis 4.0+ 引入异步删除,避免大 key 删除阻塞主线程:
```
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
replica-lazy-free yes
```
用 `UNLINK` 替代 `DEL` 删除大 key,后台线程异步回收内存。
**内存碎片治理**:监控 `INFO memory` 中 `mem_fragmentation_ratio`,超过 1.5 说明碎片严重。开启主动碎片整理:
```
activedefrag yes
```
## 网络优化
**Pipeline 批量执行**:将多个命令打包一次发送,减少网络 RTT。适合批量写入、批量查询场景:
```bash
# Pipeline 批量设置
echo -e "SET key1 value1\nSET key2 value2\nSET key3 value3" | redis-cli --pipe
```
注意 Pipeline 不是原子操作,中间命令失败不影响其他命令执行。
**连接池管理**:客户端使用连接池复用连接,避免频繁 TCP 握手。池大小建议按 `连接数 = (RTT × QPS) / 命令数` 估算,一般 50-200 即可覆盖大多数场景。
**大 Key 拆分**:大 Key 导致网络传输慢、阻塞主线程、影响同步。排查方式:
```bash
redis-cli --bigkeys # 扫描各类型最大 key
redis-cli MEMORY USAGE key # 查看单个 key 内存占用
```
拆分策略:将大 Hash 拆为多个小 Hash(按字段分桶),大 List 用 LRANGE 分段读取。
**禁用 THP**:Transparent Huge Pages 会导致 fork 耗时剧增,影响 RDB/AOF 重写:
```bash
echo never > /sys/kernel/mm/transparent_hugepage/enabled
```
## CPU 优化
**禁用 KEYS 命令**:KEYS 会遍历所有 key,时间复杂度 O(N),生产环境必须禁用。用 SCAN 增量迭代替代:
```bash
SCAN 0 MATCH user:* COUNT 100
```
**避免慢命令**:SORT 大集合、SUNION/SINTER 大集合操作、LRANGE 0 -1 全量读取等都是常见性能杀手。核心原则:单次操作时间控制在毫秒级。
**Lua 脚本减少往返**:Lua 在服务端原子执行,减少网络 RTT,适合复合原子操作:
```lua
-- 原子性扣库存并返回余量
local stock = tonumber(redis.call("GET", KEYS[1]))
if stock and stock > 0 then
redis.call("DECR", KEYS[1])
return stock - 1
else
return -1
end
```
注意 Lua 脚本执行期间 Redis 阻塞,脚本必须简短。
**Redis 6.0+ 多线程 IO**:Redis 6.0 引入多线程处理网络读写,命令执行仍是单线程。开启方式:
```
io-threads 4
io-threads-do-reads yes
```
适合网络 IO 成为瓶颈的场景(高并发小命令),CPU 密集型操作无法加速。
## 持久化优化
**RDB 调优**:调整保存频率,低峰期触发;关闭 RDB 压缩(`rdbcompression no`)可降低 CPU 开销,但文件会变大。`save ""` 可完全禁用 RDB。
**AOF 调优**:
- `appendfsync everysec` 是性能与安全的平衡点,最多丢 1 秒数据
- 调整 `auto-aof-rewrite-percentage` 和 `auto-aof-rewrite-min-size` 控制重写触发
- 低峰期手动 `BGREWRITEAOF` 减少影响
**混合持久化**(Redis 4.0+):`aof-use-rdb-preamble yes`,AOF 重写时前半段写 RDB 格式(快),后半段写 AOF 增量(全),兼顾恢复速度和数据安全。
**纯缓存场景关闭持久化**:如果 Redis 只做缓存、数据可从 DB 重建,关闭 RDB 和 AOF 可大幅提升性能。
## 集群优化
**数据分片与避免倾斜**:Redis Cluster 按 hash slot 分片(16384 个),用 `CLUSTER SLOT` 查看分布。避免倾斜的方法:合理设计 key 的 hash tag(`{tag}`),热点 key 拆分为多个 key 分散到不同 slot。
**读写分离**:从节点分担读流量,注意从节点默认拒绝写操作。复制延迟可能导致读到旧数据,强一致性场景慎用。
**故障转移**:哨兵模式(Sentinel)适合主从架构的自动故障转移;Redis Cluster 自带故障检测和转移能力。面试常问两者的区别:Sentinel 是独立进程监控主从,Cluster 是去中心化的分片+高可用方案。
## 监控与调优
**慢查询日志**:
```bash
CONFIG SET slowlog-log-slower-than 10000 # 阈值 10ms
CONFIG SET slowlog-max-len 128
SLOWLOG GET 10 # 查看最近 10 条慢查询
```
**INFO 命令关键指标**:
```bash
INFO memory # used_memory、fragmentation_ratio
INFO commandstats # 命令调用次数和耗时
INFO replication # 主从复制状态和延迟
INFO stats # keyspace_hits/misses 算命中率
```
**redis-benchmark 压测**:
```bash
redis-benchmark -h 127.0.0.1 -p 6379 -c 50 -n 10000 -t set,get
```
## 操作系统优化
**文件描述符**:
```bash
ulimit -n 65535
# 或永久配置 /etc/security/limits.conf
```
**TCP 参数**:
```bash
echo 3 > /proc/sys/net/ipv4/tcp_fastopen
# 调整 TCP 缓冲区
sysctl -w net.core.somaxconn=65535
```
**关闭 THP**:已在网络优化中提及,这里再次强调,这是 Redis 生产部署的必要步骤。
**绑定 CPU**:`taskset` 将 Redis 绑定到特定 CPU 核,减少上下文切换,对延迟敏感场景有效。
## 客户端优化
**客户端选型**:
- Jedis:同步阻塞,简单场景够用
- Lettuce:基于 Netty 的异步非阻塞,Spring Boot 2.x 默认
- Redisson:分布式锁、限流器等高级功能开箱即用
**多级缓存**:本地缓存(Caffeine/Guava Cache)+ Redis 二级缓存,热点数据本地缓存 1-5 秒,减少 Redis 访问量。
**避免 N+1 查询**:用 `MGET`/`Pipeline` 批量查询,用 Hash 结构化存储减少查询次数。
## 架构优化
**Redis Proxy**:Twemproxy、Predixy 等代理层实现分片和连接池管理,对客户端透明。缺点是多一跳网络开销。
**高可用方案选型**:
- 主从 + Sentinel:适合数据量不大、读多写少
- Redis Cluster:适合数据量大、需要水平扩展
- 两者不兼容,架构设计时需提前选型
**多实例部署**:单机多实例可充分利用多核 CPU,每个实例绑定不同核,独立持久化互不影响。
## 面试答题思路
面试官问 Redis 性能优化,建议按优先级回答:
1. **最优先**:禁用 KEYS、大 Key 拆分、Pipeline、设置合理 TTL 和淘汰策略
2. **持久化**:AOF everysec、混合持久化、纯缓存可关闭
3. **架构层**:读写分离、Cluster 分片、多级缓存
4. **运维层**:慢查询监控、内存碎片治理、OS 参数调优
每个点说出原理和具体操作,面试官追问时能展开即可。不要泛泛而谈"多维度优化",要落地到具体配置项和命令。
服务端5月28日 09:33
Redis 的 RDB 和 AOF 持久化有什么区别?如何选择?Redis 提供两种持久化机制将内存数据写入磁盘:RDB(快照)和 AOF(追加日志)。理解两者的原理和取舍是后端面试的高频考点,也是生产环境配置的基础。
## RDB 持久化:定时快照
RDB 在指定时间间隔内对数据集生成时间点快照,写入压缩的二进制文件 `dump.rdb`。
**触发方式**:
- 手动触发:执行 `SAVE`(阻塞主进程)或 `BGSAVE`(fork 子进程后台执行)
- 自动触发:配置 `save <seconds> <changes>` 条件满足时自动执行 `BGSAVE`
- `shutdown` 时若开启 RDB 且无 AOF,默认执行 `BGSAVE`
**核心原理 — COW(Copy-On-Write)**:
`BGSAVE` 时 Redis 通过 `fork()` 创建子进程,子进程共享父进程的内存页。当主进程收到写请求,操作系统会将待修改的内存页复制一份,子进程继续在原页面写入 RDB 文件。这就是为什么 RDB 对主进程性能影响小——只有真正被修改的页才会产生额外内存开销。
**优点**:
1. **文件紧凑**:二进制压缩格式,体积远小于 AOF,适合备份和传输
2. **恢复速度快**:直接加载二进制文件,比 AOF 重放命令快一个数量级
3. **对主进程影响小**:由子进程执行,COW 机制保证主进程正常处理请求
4. **适合冷备份**:单文件结构,方便定时拷贝到远程存储
**缺点**:
1. **数据丢失风险高**:两次快照之间的数据变更全部丢失,最坏情况丢失数分钟数据
2. **fork 耗时**:数据量大时 fork 本身可能阻塞主进程(通常与数据集大小成正比)
3. **无法实时持久化**:基于时间间隔,做不到每秒甚至每次写的持久化
**关键配置**:
```conf
save 900 1 # 900秒内至少1个key变化
save 300 10 # 300秒内至少10个key变化
save 60 10000 # 60秒内至少10000个key变化
rdbcompression yes # 压缩RDB文件
rdbchecksum yes # 文件校验
stop-writes-on-bgsave-error yes # BGSAVE失败时拒绝写入
```
## AOF 持久化:追加日志
AOF 记录每一条写操作命令,以文本格式追加到 `appendonly.aof` 文件末尾。Redis 重启时逐条重放命令恢复数据。
**写入流程**:命令追加(append) → 写入缓冲区(aof_buf) → 同步到磁盘(fsync)
**同步策略**(`appendfsync` 配置项):
- `always`:每次写操作都 fsync,最多丢一条命令,但性能最差
- `everysec`:每秒 fsync 一次,最多丢 1 秒数据,**推荐生产配置**
- `no`:由操作系统决定何时 fsync,性能最好但丢失风险不可控
**AOF 重写机制**:
AOF 文件会持续膨胀。Redis 通过 `BGREWRITEAOF` 命令在后台重写:fork 子进程遍历当前数据库状态,用最少命令重新生成 AOF 文件。例如对同一 key 执行 100 次 `SET`,重写后只保留最后一条。重写期间新的写命令同时写入旧 AOF 和重写缓冲区,重写完成后将缓冲区追加到新文件。
**优点**:
1. **数据安全性高**:`everysec` 策略下最多丢失 1 秒数据
2. **可读可修复**:文本格式,可直接查看;误操作后可手动编辑删除错误命令
3. **自动重写压缩**:配置阈值触发重写,控制文件体积
**缺点**:
1. **文件体积大**:同等数据量下 AOF 文件通常比 RDB 大数倍
2. **恢复速度慢**:逐条重放命令,大规模数据恢复耗时显著
3. **性能开销大**:频繁的磁盘 I/O,`always` 策略下吞吐量明显下降
**关键配置**:
```conf
appendonly yes # 开启AOF
appendfsync everysec # 同步策略
auto-aof-rewrite-percentage 100 # AOF文件大小增长100%时触发重写
auto-aof-rewrite-min-size 64mb # AOF文件重写的最小大小
aof-load-truncated yes # 忽略末尾不完整的AOF文件
```
## RDB + AOF 混合持久化
Redis 4.0 引入混合持久化,兼顾两者优势:AOF 重写时将 RDB 格式的全量数据写入 AOF 文件开头,后续增量命令以 AOF 格式追加。
**效果**:恢复时先快速加载 RDB 部分(快),再重放增量 AOF 命令(完整),数据安全性与恢复速度兼得。
```conf
aof-use-rdb-preamble yes # 开启混合持久化(需同时开启AOF)
```
**恢复优先级**:当 RDB 和 AOF 文件同时存在时,Redis **优先加载 AOF**,因为 AOF 的数据完整性更高。
## 面试怎么答:对比速记表
| 维度 | RDB | AOF |
|------|-----|-----|
| 原理 | 定时快照(二进制) | 追加写命令(文本日志) |
| 数据安全性 | 可能丢失数分钟数据 | 最多丢 1 秒(everysec) |
| 恢复速度 | 快 | 慢 |
| 文件体积 | 小 | 大 |
| 性能影响 | 小(COW) | 较大(频繁 I/O) |
| 适用场景 | 冷备份/灾难恢复 | 实时持久化/数据安全优先 |
## 生产环境选择建议
1. **数据安全优先**(金融、支付):AOF + `appendfsync everysec`,或混合持久化
2. **性能优先、可容忍少量丢失**(缓存场景):仅 RDB,配置合理的 save 间隔
3. **两者兼顾**(通用生产环境):RDB + AOF 混合持久化,这是 Redis 4.0+ 的推荐方案
4. **纯缓存无需持久化**:关闭 RDB 和 AOF,重启后从数据源重新加载
实际生产中,大多数场景采用混合持久化。关闭 RDB 的 save 配置(设为空字符串)可以避免自动触发快照,仅保留 AOF 的实时性,同时定期手动 `BGSAVE` 做冷备。服务端5月28日 07:19
Redis 事务、Lua 脚本和分布式锁的实现原理和使用场景是什么?Redis 事务、Lua 脚本和分布式锁是 Redis 面试中出现频率最高的三个高级特性,很多候选人只能说出命令用法,却讲不清背后的原理和边界,面试官一追问就卡壳。下面逐一拆解。
## Redis 事务的原理与局限
Redis 事务通过 MULTI、EXEC、DISCARD、WATCH 四个命令协作完成。MULTI 开启事务后,后续命令进入队列而非立即执行;EXEC 一次性提交队列中的所有命令;DISCARD 放弃事务;WATCH 实现乐观锁,监控 key 是否在事务提交前被修改。
```bash
WATCH balance
MULTI
DECRBY balance 100
INCRBY expense 100
EXEC
```
如果 WATCH 监控的 key 在 EXEC 之前被其他客户端修改,整个事务会被丢弃,返回 nil。
事务的核心特点:
- **命令按顺序执行,不会被其他客户端插入**,这是隔离性的保证。
- **不支持回滚**。如果队列中某条命令执行失败(比如对字符串执行 LPUSH),其余命令照常执行。Redis 官方的设计哲学是:命令失败属于编程错误,应在开发阶段发现,不值得为此牺牲性能。
- **无法使用中间结果**。事务中的命令不能引用前一条命令的返回值,这极大限制了事务的表达能力。
这些局限正是 Lua 脚本存在的理由。
### 追问:Redis 事务为什么不支持回滚?
Redis 作者 antirez 的原话是:回滚需要保存命令执行前的状态,这会引入与 AOF 持久化类似的复杂度,而命令失败本质是 bug,不应该出现在生产环境。所以 Redis 选择不支持回滚,换来更简单、更快的实现。
## Lua 脚本为什么能替代事务
Lua 脚本在 Redis 服务端以原子方式执行,执行期间不会处理其他客户端的命令。和事务相比,Lua 脚本的核心优势在于**可以读取中间结果并做条件判断**。
```bash
EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey myvalue
```
先用 SCRIPT LOAD 获取脚本 SHA1 校验和,后续用 EVALSHA 执行,避免每次传输完整脚本:
```bash
SCRIPT LOAD "return redis.call('SET', KEYS[1], ARGV[1])"
# 返回 sha1sum
EVALSHA <sha> 1 mykey myvalue
```
几个典型的 Lua 脚本应用场景:
**原子性 CAS 操作**——只有当值等于预期时才更新:
```lua
local current = redis.call('GET', KEYS[1])
if current == ARGV[1] then
redis.call('SET', KEYS[1], ARGV[2])
return 1
else
return 0
end
```
**滑动窗口限流器**——一次执行完成过期清理、计数检查、记录添加三个步骤:
```lua
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
redis.call('ZREMRANGEBYSCORE', key, '-inf', window)
local count = redis.call('ZCARD', key)
if count < limit then
redis.call('ZADD', key, window, ARGV[3])
redis.call('EXPIRE', key, math.ceil((window - tonumber(ARGV[4])) / 1000))
return 1
else
return 0
end
```
Lua 脚本需要注意三点:执行时间不能太长,否则阻塞整个 Redis 实例(默认 5 秒超时);不能使用随机函数(如 math.random),否则主从复制结果不一致;不能执行阻塞命令(如 BLPOP)。
### 追问:Lua 脚本出错会怎样?
Lua 脚本运行时出错会立即停止,但已执行的 Redis 命令不会被撤销——这点和事务一致,都不满足严格意义上的原子性。从外部看,脚本的执行是不可分割的;从内部看,部分成功部分失败是可能的。
## 分布式锁的三种实现与踩坑
分布式锁要解决的核心问题:在多进程、多机器环境下,保证同一时刻只有一个客户端能操作共享资源。
### SETNX + EXPIRE 的问题
最早的做法是先 SETNX 获取锁,再 EXPIRE 设置过期时间。这两步不是原子操作——如果 SETNX 成功后客户端崩溃,锁永远不释放,造成死锁。
### SET NX EX(推荐的基础方案)
Redis 2.6.12 起 SET 命令支持 NX 和 EX 参数,一条命令完成加锁和设置过期时间:
```java
public boolean tryLock(String key, String value, int expireSeconds) {
String result = jedis.set(key, value, "NX", "EX", expireSeconds);
return "OK".equals(result);
}
```
释放锁必须用 Lua 脚本保证原子性——先判断值是否为自己持有,再删除:
```lua
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
```
为什么不能直接 DEL?因为锁可能已经过期并被其他客户端获取,直接 DEL 会删掉别人的锁。
### 追问:主从切换导致锁丢失怎么办?
考虑这个时序:客户端 A 获取锁 → 主节点写入成功但尚未同步到从节点 → 主节点宕机 → 从节点升为主节点 → 客户端 B 获取同一把锁 → 两个客户端同时持有锁。
Redlock 算法就是为解决这个问题设计的。
### Redlock 算法
Redlock 向 N 个(通常 5 个)独立的 Redis 实例获取锁,只要在大多数实例(≥3)上成功,且总耗时未超过锁的有效期,就认为加锁成功。这依赖的是时钟同步和多数派决策,不依赖主从复制。
Redlock 的争议:Martin Kleppmann 在《How to do distributed locking》一文中指出 Redlock 依赖系统时钟,当时钟跳变时可能出错,建议使用 fencing token 方案。antirez 专门写了长文反驳。实际工程中,大多数团队选择接受 Redlock 的概率性安全保证,或直接使用单节点锁 + 幂等设计来规避风险。
### Redisson 的工程级实现
Redisson 是 Java 生态最成熟的 Redis 分布式锁实现,提供了可重入锁、公平锁、读写锁、联锁等多种锁类型。核心设计:
**看门狗机制**:默认锁过期时间 30 秒,Redisson 会启动一个后台线程,每 10 秒(过期时间的 1/3)自动续期,直到显式释放或客户端宕机。这解决了长任务执行期间锁过期被其他客户端获取的问题。
```java
RLock lock = redisson.getLock("myLock");
try {
if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
// 执行业务逻辑
}
} finally {
lock.unlock();
}
```
**可重入性**:Redisson 使用 Hash 结构存储锁信息,field 是客户端 ID + 线程 ID,value 是重入次数,加锁时重入次数 +1,解锁时 -1,减到 0 才真正释放。整个逻辑用 Lua 脚本保证原子性。
## 事务、Lua 脚本与分布式锁的选型
| 维度 | 事务 | Lua 脚本 | 分布式锁 |
|------|------|----------|---------|
| 原子性 | 命令不可插入,但不支持回滚 | 同事务 | 依赖实现方式 |
| 条件判断 | 不支持 | 完全支持 | 通过 Lua 脚本支持 |
| 网络开销 | 多次 RTT(除非 pipeline) | 一次 RTT | 多次 RTT |
| 典型场景 | 简单批量操作 | CAS、限流、复杂业务逻辑 | 跨进程互斥 |
选择原则:事务适合不需要中间结果的简单批量操作;Lua 脚本适合需要条件判断、依赖中间结果的场景;分布式锁解决的是跨进程互斥问题,本质上依赖 Lua 脚本保证操作的原子性。
## 面试中容易被追问的几个点
**Redis 事务和 MySQL 事务有什么区别?** Redis 事务不支持回滚,没有隔离级别,不保证持久性——它只是批量执行命令的机制,和 ACID 事务有本质区别。
**Lua 脚本执行期间 Redis 宕机怎么办?** 如果开启了 AOF 持久化,已执行的命令会被记录,重启后重放。如果没有持久化,数据丢失。关键点是 Lua 脚本的"原子性"只保证执行期间不被打断,不保证持久性和严格的原子性。
**分布式锁的过期时间怎么设置?** 太短容易导致任务未完成锁就被释放,太长会导致客户端宕机后其他客户端等待过久。Redisson 的看门狗机制是最佳实践,动态续期比固定过期时间更可靠。
服务端5月28日 07:17
Redis 与 MySQL、MongoDB、Memcached 有什么区别?如何选择?Redis、MySQL、MongoDB、Memcached 是后端开发中最常用的四种数据存储方案,面试中经常被放在一起考察。它们的设计目标完全不同,理解本质差异才能做出正确的技术选型。
## 核心区别:一张表看懂
| 维度 | Redis | MySQL | MongoDB | Memcached |
|------|-------|-------|---------|-----------|
| 存储介质 | 内存为主,可持久化 | 磁盘为主 | 磁盘(内存映射) | 纯内存 |
| 数据模型 | Key-Value + 多种数据结构 | 关系型表 | 文档型(BSON) | Key-Value(仅String) |
| 事务支持 | 有限事务(MULTI/EXEC) | 完整ACID | 4.0起支持多文档事务 | 无 |
| 持久化 | RDB + AOF | 天然持久化 | 天然持久化 | 无,宕机即丢 |
| 查询能力 | 按Key操作,有限范围查询 | SQL,复杂关联聚合 | MQL,支持索引和聚合 | 仅GET/SET |
| 单机QPS | 10万+ | 数千~数万 | 数万 | 10万+ |
| 一致性模型 | 最终一致性 | 强一致性 | 可配置(最终/强) | 无一致性保证 |
| 水平扩展 | Cluster分片 | 分库分表(复杂) | 原生分片 | 客户端一致性Hash |
## Redis vs MySQL:缓存与持久化的抉择
**本质区别在于存储介质和一致性模型。**
Redis 数据驻留内存,读写延迟在亚毫秒级,但默认不保证数据持久化——即使开启 AOF,也存在最多 1 秒的数据丢失窗口。MySQL 数据落盘,通过 redo log 和 doublewrite 机制保证数据安全,但读写延迟在毫秒到十毫秒级。
面试中常问的一个问题:**Redis 能否替代 MySQL?**
答案是不能。两者的定位完全不同:
- Redis 适合做缓存层和实时数据层,数据可以丢失或从源头重建
- MySQL 适合做持久化存储层,数据不能丢失且需要事务保证
**生产中的经典架构是 Redis + MySQL 组合**:读请求先查 Redis,命中则直接返回;未命中则查 MySQL,结果回写 Redis。但这里有几个关键问题需要注意:
1. **双写一致性**:先更新 MySQL 再删缓存,存在短暂不一致窗口。对一致性要求高的场景,可用 Canal 监听 binlog 同步更新 Redis
2. **缓存穿透**:查询不存在的数据,绕过缓存直击数据库。用布隆过滤器或缓存空值解决
3. **缓存击穿**:热点 Key 过期瞬间大量请求涌入数据库。用互斥锁或永不过期+异步刷新
4. **缓存雪崩**:大批 Key 同时过期。用随机过期时间打散
## Redis vs MongoDB:两种 NoSQL 的不同思路
Redis 和 MongoDB 虽然都属于 NoSQL,但设计哲学完全不同。
**Redis 追求极致性能**,数据全在内存中,数据结构精心设计,每种操作的时间复杂度都有明确保证。它更像一个高性能的数据结构服务器。
**MongoDB 追求灵活性**,文档模型允许 schema 自由变化,嵌套文档减少了关联查询的需要。它更像一个增强版的 MySQL。
关键区别:
- **数据量**:Redis 受内存限制,通常存热点数据;MongoDB 可存储 TB 级数据
- **查询复杂度**:Redis 只支持基于 Key 的操作;MongoDB 支持条件查询、聚合管道
- **适用场景**:Redis 做缓存/计数器/排行榜/分布式锁;MongoDB 做内容管理/日志/IoT 数据/用户画像
## Redis vs Memcached:缓存之争的终局
这组对比面试频率极高。核心结论:**新项目直接选 Redis,没有理由选 Memcached。**
Memcached 的仅存优势:
- 多线程架构,在 value 超过 100KB 时吞吐量更高
- 更简单的部署,适合纯缓存场景
Redis 全面碾压的点:
- 支持丰富数据结构(Memcached 只有 String)
- 支持持久化(Memcached 宕机数据全丢)
- 支持主从复制和集群(Memcached 靠客户端分片)
- 支持发布订阅、Lua 脚本、Stream(Memcached 无)
- 单线程模型反而简化了并发控制,小数据性能更优
Memcached 在 2015 年之前还有市场份额,现在基本已被 Redis 完全取代。面试中回答"选 Redis"即可,但要说清楚为什么。
## 技术选型:根据场景做决策
选型不是二选一,而是根据业务特征匹配最合适的工具。
**选 Redis 的场景**:需要亚毫秒级响应、数据可以容忍短暂丢失、操作模式简单(按 Key 读写)。典型用例:缓存、Session、排行榜、计数器、分布式锁、限流器。
**选 MySQL 的场景**:数据必须持久化且保证一致性、需要复杂关联查询和事务、业务模型稳定。典型用例:用户系统、订单系统、支付系统。
**选 MongoDB 的场景**:数据结构频繁变化、单文档较大且需要灵活查询、需要水平扩展。典型用例:CMS、日志分析、IoT、用户画像。
**选 Memcached 的场景**:基本没有了。除非维护旧系统,否则没有理由新项目选 Memcached。
## 生产架构中的组合模式
实际项目中几乎不会只用一种存储,常见的组合方案:
**Redis + MySQL(最经典)**:Redis 缓存热点数据,MySQL 持久化存储。注意做好双写一致性、缓存穿透/击穿/雪崩的防护。
**Redis + MySQL + MongoDB**:Redis 做缓存,MySQL 存核心业务数据,MongoDB 存非结构化数据(日志、配置、内容)。
**Redis + MySQL + Elasticsearch**:Redis 缓存,MySQL 存储,ES 负责全文检索和复杂搜索。
无论哪种组合,核心原则是:**每种存储只做它最擅长的事,不要让 MySQL 做缓存,也不要让 Redis 做持久化。**
## 追问:缓存与数据库的一致性如何保证?
这是上述选型之后必然的追问,也是面试高频考点。
**先更新数据库,再删缓存** 是最常用的策略。为什么是删缓存而不是更新缓存?因为更新可能涉及复杂计算,且并发场景下容易产生脏数据。
**一致性保证的三个层级**:
1. **弱一致性**:先更新DB再删缓存,容忍短暂不一致,适合大多数业务
2. **最终一致性**:通过消息队列或 Canal 监听 binlog 异步更新缓存,保证最终一致
3. **强一致性**:读写都走数据库,或使用分布式锁,牺牲性能换一致性,仅金融等场景使用
面试时回答到第二层即可,重点是说清楚各方案的 trade-off。