📌 一、Redis基础篇
1.1 什么是Redis?为什么要用Redis?
✅ 正确回答思路:
面试官您好,我从三个方面来回答这个问题:
首先说Redis是什么 : Redis是一个基于内存的NoSQL数据库,全称是RE mote DI ctionary Server(远程字典服务器)。它的数据都存在内存中,所以读写速度非常快,经常被用作缓存、消息队列、分布式锁等场景。
然后说为什么要用Redis,我结合实际项目来说:
-
性能极高: Redis所有数据都在内存中,读写速度比MySQL这种基于磁盘的数据库快几个数量级。MySQL单机QPS(每秒查询数)一般在几千,而Redis单机轻松上10万+。
在我之前的电商项目中,商品详情页每天有几百万的访问量,如果每次都查MySQL肯定扛不住。我们把商品信息缓存到Redis后,响应时间从几百毫秒降到了几毫秒,数据库压力也大大降低。
-
数据类型丰富: Redis不像Memcached只支持简单的key-value,它支持String、Hash、List、Set、ZSet等多种数据结构,还有Bitmap、HyperLogLog、GEO、Stream等高级类型。
比如我们做排行榜功能,用ZSet(有序集合)就特别方便,不需要自己写排序逻辑。
-
支持持久化: 虽然Redis是内存数据库,但它支持RDB和AOF两种持久化方式,可以把数据保存到磁盘,重启后恢复数据,不像Memcached重启就丢了。
-
支持集群和高可用: Redis支持主从复制、哨兵模式、Cluster集群,可以实现高可用和水平扩展。
最后总结实际应用: 在我的项目中,Redis主要用在这几个地方:
- 缓存: 缓存热点数据,减轻数据库压力
- Session共享: 分布式系统中存储用户Session
- 分布式锁: 用SETNX实现分布式锁,解决并发问题
- 消息队列: 用List或Stream实现简单的消息队列
- 计数器: 用INCR实现文章阅读数、点赞数等
💡 加分项: 可以补充说"当然Redis也不是万能的,它的缺点是成本较高(内存贵),数据量太大时需要考虑持久化和内存淘汰策略。所以要根据业务场景合理使用Redis。"
1.2 Redis为什么这么快?
✅ 正确回答思路:
Redis快的原因我总结了五个方面:
1. 纯内存操作 这是最核心的原因!内存的读写速度比磁盘快几个数量级。磁盘IO是毫秒级的,而内存是纳秒级的,差了几百万倍。所以只要数据在内存里,读写自然就快。
2. 单线程模型,避免上下文切换 Redis的核心工作线程是单线程的(注意:Redis 6.0后引入了多线程,但只是用来处理网络IO,核心的命令执行还是单线程)。
单线程有什么好处?
- 不需要考虑线程安全问题,不用加锁
- 避免了线程切换和锁竞争的开销
- 代码实现简单,易于维护
有人可能会问:单线程怎么能处理高并发?这就要靠第三点。
3. IO多路复用 Redis使用了IO多路复用技术(在Linux下用epoll,其他系统用select/kqueue),可以让单个线程同时监听成千上万个客户端连接。
简单来说就是:
- 传统模式下,一个线程只能处理一个连接,要处理1000个连接就需要1000个线程,开销巨大
- IO多路复用让一个线程可以同时监听1000个连接,哪个连接有数据就处理哪个,效率很高
4. 高效的数据结构 Redis内部使用了高度优化的数据结构:
- SDS(Simple Dynamic String): 比C语言原生字符串更高效
- 跳表(SkipList): 实现有序集合,查询效率O(logN)
- 压缩列表(ZipList/ListPack): 节省内存
- 哈希表: 快速的键值查找
而且Redis会根据数据量的大小自动选择最优的底层实现,在性能和内存之间做平衡。
5. 简单高效的协议 Redis使用RESP(REdis Serialization Protocol)协议,这个协议非常简单,解析速度快,序列化和反序列化的开销很小。
实际数据对比: 根据官方测试,Redis单机QPS可以达到:
- GET/SET操作: 10万+ QPS
- List的LPUSH/LPOP: 8万+ QPS
- ZSet的ZADD: 8万+ QPS
而MySQL单机一般只有几千QPS,差距非常明显。
💡 记忆口诀: "内存快,单线程,多路复用,数据结构优,协议简单"
1.3 Redis是单线程的吗?Redis 6.0为什么要引入多线程?
✅ 正确回答思路:
这个问题要分版本来说,而且要澄清"单线程"的具体含义:
Redis 6.0之前:
严格来说,Redis并不是完全的单线程:
- 命令执行是单线程: 这是核心,所有的Redis命令(GET、SET、INCR等)都是单线程执行的
- 其他任务是多线程: 比如持久化(RDB/AOF)、异步删除(UNLINK)、集群数据同步等,这些都是用后台线程处理的
所以当我们说"Redis是单线程"时,准确的说法是"Redis的命令执行是单线程的"。
单线程的好处:
- 避免了线程切换的开销
- 不需要考虑锁的问题,代码简单
- 所有命令都是串行执行,天然支持ACID中的原子性
Redis 6.0引入多线程:
但是单线程也有瓶颈!随着硬件性能的提升(多核CPU、万兆网卡),单线程逐渐成为瓶颈。尤其是网络IO处理,成为了性能瓶颈。
所以Redis 6.0引入了多线程,但只是用来处理网络IO(接收请求、发送响应),核心的命令执行还是单线程!
具体来说:
客户端请求到来
↓
多线程处理网络IO(读取数据) ← 这里是多线程
↓
单线程执行命令(SET、GET等) ← 这里还是单线程
↓
多线程处理网络IO(发送响应) ← 这里是多线程
为什么这样设计?
- 网络IO可以并行: 读取和发送数据包可以多线程处理,互不干扰
- 命令执行必须串行: 保证数据一致性,避免并发冲突
是否要开启多线程?
Redis 6.0默认是关闭多线程的,需要手动配置:
conf
# redis.conf
io-threads-do-reads yes # 开启多线程
io-threads 4 # 设置IO线程数,建议不超过CPU核心数
什么时候开启?
- 单机Redis QPS达到10万+,网络IO成为瓶颈时
- 机器是多核CPU,比如8核16核
- 有大量的网络IO操作
我的项目经验: 我们项目的Redis QPS一般在5万左右,用的还是单线程模式,性能就够用了。只有在大促期间,QPS突破10万时,才考虑开启多线程,能提升20%-30%的性能。
💡 总结:
- Redis 6.0之前,命令执行是单线程,但其他任务(持久化等)是多线程
- Redis 6.0引入了多线程网络IO,但命令执行还是单线程
- 多线程主要解决网络IO瓶颈,不改变Redis的核心模型
1.4 Redis支持哪些数据类型?分别有什么应用场景?
✅ 正确回答思路:
Redis的数据类型我分两部分来说:五种基础类型和四种高级类型。
一、五种基础数据类型
1. String(字符串)
-
特点: 最基础的类型,二进制安全,可以存储任何数据,最大512MB
-
常用命令: SET、GET、INCR、DECR、MGET、MSET
-
应用场景
:
java// ① 缓存对象(序列化成JSON)redisTemplate.opsForValue().set("user:1001", JSON.toJSONString(user));// ② 计数器(阅读数、点赞数)redisTemplate.opsForValue().increment("article:1001:views");// ③ 分布式锁Boolean result = redisTemplate.opsForValue() .setIfAbsent("lock:order:1001", "1", 10, TimeUnit.SECONDS);// ④ Session共享redisTemplate.opsForValue().set("session:" + sessionId, userInfo, 30, TimeUnit.MINUTES);
2. Hash(哈希)
-
特点: 键值对集合,适合存储对象
-
常用命令: HSET、HGET、HMGET、HGETALL、HINCRBY
-
应用场景
:
java// 存储用户信息(比String更节省空间)Map<String, String> userMap = new HashMap<>();userMap.put("name", "张三");userMap.put("age", "25");userMap.put("city", "北京");redisTemplate.opsForHash().putAll("user:1001", userMap);// 购物车// key: cart:userId// field: productId// value: 数量redisTemplate.opsForHash().increment("cart:1001", "product:2001", 1);
对比String存储对象:
- String: 把整个对象序列化成JSON,修改某个字段要把整个对象读出来、修改、再写回去
- Hash: 可以直接修改某个字段,更高效,也更省内存
3. List(列表)
-
特点: 有序、可重复,底层是双向链表(Redis 3.2后改为quicklist)
-
常用命令: LPUSH、RPUSH、LPOP、RPOP、LRANGE、BLPOP
-
应用场景
:
java// ① 消息队列(简单场景)// 生产者redisTemplate.opsForList().rightPush("queue:order", orderJson);// 消费者String order = redisTemplate.opsForList().leftPop("queue:order");// ② 最新动态列表(朋友圈、微博)redisTemplate.opsForList().leftPush("timeline:1001", postJson);List<String> posts = redisTemplate.opsForList().range("timeline:1001", 0, 9); // 取最新10条// ③ 阻塞队列(BLPOP)String order = redisTemplate.opsForList().leftPop("queue:order", 10, TimeUnit.SECONDS);
4. Set(集合)
-
特点: 无序、不重复,底层是哈希表
-
常用命令: SADD、SMEMBERS、SISMEMBER、SINTER、SUNION、SDIFF
-
应用场景
:
java// ① 去重(用户签到、文章标签)redisTemplate.opsForSet().add("tags:article:1001", "Java", "Redis", "MySQL");// ② 共同关注(交集)Set<String> common = redisTemplate.opsForSet().intersect("following:1001", "following:1002");// ③ 抽奖(随机弹出)String winner = redisTemplate.opsForSet().pop("lottery:2025");// ④ 点赞(判断是否点过赞)Boolean liked = redisTemplate.opsForSet().isMember("liked:article:1001", "user:1001");
5. ZSet(有序集合)
-
特点: 有序、不重复,每个元素关联一个分数(score),按分数排序
-
常用命令: ZADD、ZRANGE、ZREVRANGE、ZRANK、ZINCRBY
-
应用场景
:
java// ① 排行榜(最常用!)// 添加分数redisTemplate.opsForZSet().incrementScore("rank:game", "player:1001", 100);// 获取TOP 10Set<ZSetOperations.TypedTuple<String>> top10 = redisTemplate.opsForZSet().reverseRangeWithScores("rank:game", 0, 9);// 获取某个玩家的排名Long rank = redisTemplate.opsForZSet().reverseRank("rank:game", "player:1001");// ② 延时队列(按时间戳排序)redisTemplate.opsForZSet().add("delay:queue", taskJson, System.currentTimeMillis() + 3600000);
二、四种高级数据类型
1. Bitmap(位图) - Redis 2.2+
-
特点: 用bit位来存储0/1状态,非常节省空间
-
应用场景
:
java// 用户签到(一年365天只需要46字节!)// 1月1日签到redisTemplate.opsForValue().setBit("sign:user:1001:2025", 0, true);// 1月2日签到redisTemplate.opsForValue().setBit("sign:user:1001:2025", 1, true);// 统计1月份签到了几天Long count = (Long) redisTemplate.execute( (RedisCallback<Long>) con -> con.bitCount("sign:user:1001:2025".getBytes(), 0, 30));// 在线用户统计(10亿用户只需要119MB!)redisTemplate.opsForValue().setBit("online:users", userId, true);
2. HyperLogLog - Redis 2.8+
-
特点: 用于基数统计(去重计数),误差率0.81%,但极省内存(12KB)
-
应用场景
:
java// 统计UV(独立访客数)redisTemplate.opsForHyperLogLog().add("uv:page:home:20250213", "user:1001", "user:1002");// 获取UVLong uv = redisTemplate.opsForHyperLogLog().size("uv:page:home:20250213");// 适合场景:数据量大、允许小误差// 比如统计网站每天的UV,几百万用户,用HyperLogLog只需12KB,用Set可能需要几十MB
3. GEO(地理位置) - Redis 3.2+
-
特点: 存储地理位置坐标,支持计算距离、范围查询
-
应用场景
:
java// 附近的人/店铺// 添加位置redisTemplate.opsForGeo().add("restaurants", new Point(116.404, 39.915), "restaurant:1001");// 查找附近5公里内的餐厅Circle circle = new Circle(new Point(116.404, 39.915), new Distance(5, Metrics.KILOMETERS));GeoResults<GeoLocation<String>> results = redisTemplate.opsForGeo().radius("restaurants", circle);// 计算两地距离Distance distance = redisTemplate.opsForGeo().distance( "restaurants", "restaurant:1001", "restaurant:1002", Metrics.KILOMETERS);
4. Stream(流) - Redis 5.0+
-
特点: 消息队列,支持消费组,比List实现的队列更强大
-
应用场景
:
java// 发送消息redisTemplate.opsForStream().add("order:stream", Collections.singletonMap("orderId", "1001"));// 消费消息(支持消费组,支持ACK)List<MapRecord<String, Object, Object>> messages = redisTemplate.opsForStream().read(Consumer.from("group1", "consumer1"), StreamReadOptions.empty().count(10), StreamOffset.create("order:stream", ReadOffset.lastConsumed()));
总结对比表:
| 数据类型 | 特点 | 典型应用 |
|---|---|---|
| String | 最基础,最通用 | 缓存、计数器、分布式锁、Session |
| Hash | 键值对集合 | 存储对象、购物车 |
| List | 有序可重复 | 消息队列、最新列表、Timeline |
| Set | 无序不重复 | 去重、共同关注、抽奖、点赞 |
| ZSet | 有序不重复 | 排行榜、延时队列 |
| Bitmap | 位级操作 | 签到、在线状态、布隆过滤器 |
| HyperLogLog | 基数统计 | UV统计 |
| GEO | 地理位置 | 附近的人、LBS |
| Stream | 消息流 | 高级消息队列 |
💡 面试技巧 : 不要只说"String可以存字符串",要说具体的业务场景!比如"在我的项目中,用String存储用户Session,用ZSet实现游戏排行榜"。这样面试官会觉得你真正用过Redis。
📌 二、持久化篇
2.1 Redis的持久化机制有哪些?RDB和AOF的区别?
✅ 正确回答思路:
Redis虽然是内存数据库,但为了防止数据丢失,提供了两种持久化机制:RDB和AOF。我从原理、优缺点、实际使用三方面来说:
一、RDB(Redis Database)
1. 原理: RDB就是定期把内存中的数据以快照(snapshot)的形式保存到磁盘上,生成一个dump.rdb文件。
触发时机:
conf
# redis.conf配置
save 900 1 # 900秒内至少1个key被修改,就保存
save 300 10 # 300秒内至少10个key被修改,就保存
save 60 10000 # 60秒内至少10000个key被修改,就保存
也可以手动触发:
SAVE: 同步保存,会阻塞Redis,生产环境别用!BGSAVE: 后台保存,fork一个子进程去做,不阻塞主进程
2. 工作流程:
1. Redis主进程fork一个子进程
2. 子进程把内存数据写入临时RDB文件
3. 写完后,用临时文件替换旧的dump.rdb
4. 子进程退出
重点: fork使用了**写时复制(COW, Copy-On-Write)**技术,fork时不会立即复制所有数据,只有在数据被修改时才复制,所以fork很快。
3. 优点:
- 恢复速度快: RDB是二进制文件,加载速度快,适合大数据量的恢复
- 性能好: fork子进程去做,不影响主进程处理请求
- 文件小: 经过压缩,文件体积小,适合备份和灾难恢复
4. 缺点:
- 数据丢失风险: 如果Redis挂了,最后一次快照之后的数据会丢失。比如设置5分钟保存一次,挂了可能丢5分钟的数据
- fork耗时: 数据量大时,fork会比较慢,可能影响性能
二、AOF(Append Only File)
1. 原理: AOF是把每一个写命令(SET、DEL等)都追加到aof文件里,相当于记录操作日志。恢复时重新执行一遍这些命令就行了。
配置:
conf
appendonly yes # 开启AOF
appendfilename "appendonly.aof" # AOF文件名
2. 同步策略(重要!):
有三种策略,决定了数据安全性:
conf
# appendfsync always # 每个命令都立即同步到磁盘,最安全但最慢
appendfsync everysec # 每秒同步一次(默认,推荐)
# appendfsync no # 由操作系统决定何时同步,最快但可能丢数据
- always: 最安全,几乎不丢数据,但性能差
- everysec: 折中方案,最多丢1秒数据,性能也还行(推荐)
- no: 最快,但可能丢几十秒数据
3. AOF重写:
问题: AOF文件会越来越大,比如对同一个key执行了100次SET,AOF就记录了100条命令,但其实只需要最后一条。
解决: AOF重写(Rewrite),把冗余命令合并:
conf
auto-aof-rewrite-percentage 100 # AOF文件比上次重写后大100%时,自动重写
auto-aof-rewrite-min-size 64mb # AOF文件至少要64MB才重写
重写流程:
1. fork子进程
2. 子进程根据内存数据生成新的AOF文件
3. 主进程继续处理请求,新的写命令写到AOF重写缓冲区
4. 子进程写完后,主进程把缓冲区的命令追加到新AOF文件
5. 用新文件替换旧文件
4. 优点:
- 数据更安全: everysec策略最多丢1秒数据,比RDB安全多了
- 可读性强: AOF是文本文件,可以直接看到命令,出问题方便排查
- 支持修复: AOF文件损坏了,可以用redis-check-aof工具修复
5. 缺点:
- 文件大: 相同数据,AOF文件比RDB大
- 恢复慢: 需要重新执行命令,恢复比RDB慢
- 性能稍差: 写操作需要追加到文件,有一定开销
三、RDB vs AOF 对比表
| 对比项 | RDB | AOF |
|---|---|---|
| 文件大小 | 小(二进制压缩) | 大(文本文件) |
| 恢复速度 | 快 | 慢 |
| 数据安全性 | 差(可能丢几分钟) | 好(最多丢1秒) |
| 性能影响 | 小 | 稍大 |
| 可读性 | 不可读 | 可读 |
四、混合持久化(Redis 4.0+)
Redis 4.0引入了混合持久化,结合RDB和AOF的优点:
conf
aof-use-rdb-preamble yes # 开启混合持久化
原理: AOF重写时,把RDB格式的数据写到AOF文件开头,后面再追加增量的AOF命令。
[RDB格式的全量数据] + [AOF格式的增量命令]
优点:
- 加载速度快(前面是RDB)
- 数据丢失少(后面是AOF)
五、实际项目选择
我的项目中,一般这样配置:
conf
# 同时开启RDB和AOF
# RDB用于快速恢复和备份
save 900 1
save 300 10
save 60 10000
# AOF用于数据安全
appendonly yes
appendfsync everysec
aof-use-rdb-preamble yes # 开启混合持久化
不同场景的选择:
- 缓存场景: 只用RDB或者不持久化,丢了就从数据库加载
- 数据重要: 用AOF或者RDB+AOF混合,保证数据安全
- 高性能要求: 用RDB,但要接受可能丢数据
- 折中方案: 混合持久化(推荐)
💡 记忆口诀:
- RDB: 快照备份,快速恢复,可能丢数据
- AOF: 日志追加,安全可靠,文件较大
- 混合: 两者结合,各取所长
2.2 如果Redis挂了,如何快速恢复数据?
✅ 正确回答思路:
这个问题考察的是持久化和高可用,我从两方面回答:
一、单机Redis的数据恢复
1. 基于RDB恢复
bash
# 1. 停止Redis
redis-cli shutdown
# 2. 把备份的dump.rdb复制到Redis数据目录
cp /backup/dump.rdb /var/lib/redis/dump.rdb
# 3. 启动Redis,自动加载RDB文件
redis-server
优点 : 恢复快,几秒到几分钟 缺点: 会丢失最后一次快照之后的数据
2. 基于AOF恢复
bash
# 1. 停止Redis
redis-cli shutdown
# 2. 把备份的appendonly.aof复制到数据目录
cp /backup/appendonly.aof /var/lib/redis/appendonly.aof
# 3. 如果AOF文件损坏,先修复
redis-check-aof --fix appendonly.aof
# 4. 启动Redis,自动加载AOF文件
redis-server
优点 : 数据更完整,最多丢1秒 缺点: 恢复慢,需要重新执行命令
3. 如果同时有RDB和AOF
Redis启动时的加载顺序:
1. 检查是否有AOF文件
2. 有AOF: 加载AOF(AOF优先级更高,数据更完整)
3. 没有AOF: 加载RDB
二、高可用架构下的快速恢复
在生产环境,不能依赖单机恢复,太慢了!应该用高可用架构:
1. 主从复制 + 哨兵模式
Master(主节点)
↓ 复制
Slave1 Slave2 Slave3(从节点)
↓
Sentinel1 Sentinel2 Sentinel3(哨兵,监控)
工作流程:
1. Master挂了,哨兵检测到(主观下线→客观下线)
2. 哨兵投票选举一个Slave提升为新Master
3. 其他Slave自动切换到新Master
4. 应用无感知,自动故障转移
恢复时间: 几秒到几十秒(取决于哨兵的down-after-milliseconds配置)
我的项目配置:
conf
# sentinel.conf
sentinel monitor mymaster 192.168.1.100 6379 2 # 2个哨兵认为下线才算下线
sentinel down-after-milliseconds mymaster 5000 # 5秒检测不到就认为下线
sentinel failover-timeout mymaster 60000 # 故障转移超时时间
2. Redis Cluster集群
Master1 Master2 Master3
↓ ↓ ↓
Slave1 Slave2 Slave3
工作流程:
1. 某个Master挂了
2. 它的Slave自动提升为Master
3. 集群自动重新分配槽位
4. 应用无感知
恢复时间: 几秒(cluster-node-timeout配置,默认15秒)
三、实际项目的最佳实践
我们的生产环境是这样做的:
1. 架构:
- 主从 + 哨兵模式(3主3从3哨兵)
- 数据重要程度高的业务用Cluster
2. 持久化配置:
conf
# Master: 开启AOF,保证数据安全
appendonly yes
appendfsync everysec
aof-use-rdb-preamble yes
# Slave: 只开启RDB,减轻负担
save 900 1
3. 备份策略:
- 每天凌晨2点,用crontab定时备份RDB文件到远程服务器
- 保留最近7天的备份
- 重要数据做异地备份
4. 监控告警:
- 用Prometheus监控Redis,主节点挂了立即告警
- 监控内存使用率、命中率、持久化状态
5. 演练:
- 每个季度做一次故障演练,手动kill掉Master,测试恢复时间
- 我们测试下来,从Master挂掉到新Master上线,平均10秒左右
四、真实故障案例
我们去年双11期间,主库机器硬盘故障,挂了:
处理流程:
1. 哨兵立即检测到,8秒后切换到从库
2. 业务几乎无感知,只有少量请求超时
3. 第二天更换硬盘,把修复好的节点作为新的从库加入
经验教训:
- 高可用架构太重要了,单机就是定时炸弹
- 持久化也要做,但高可用更重要
- 定期演练,不演练等于没准备
💡 总结:
- 单机: 靠持久化恢复,慢,不推荐生产使用
- 高可用: 主从+哨兵,或者Cluster,秒级恢复
- 最佳实践: 高可用架构 + 持久化 + 备份 + 监控 + 演练