Java面试-08-分布式缓存Redis
Redis 面试题汇总
目录
- 一、基础与数据类型
- 1. Redis有哪些数据类型?底层实现和应用场景是什么?
- 2. Redis常用的命令有哪些?
- 二、持久化
- 1. Redis提供了哪几种持久化方式?如何选择?
- 三、高性能与线程模型
- 1. Redis为什么快?
- 2. Redis为什么是单线程的?现在还是单线程吗?
- 四、Redis事务
- 1. Redis事务的ACID特性
- 2. Redis事务的常用命令
- 3. Redis事务的实现原理
- 4. Redis事务与数据库事务的区别
- 5. Redis事务的适用场景
- 五、高可用与集群
- 1. Redis集群方案有哪些?
- 2. 哨兵模式工作流程
- 3. Redis Cluster哈希槽原理
- 六、缓存常见问题
- 1. 缓存穿透、击穿、雪崩的区别与解决方案
- 2. 布隆过滤器误判怎么办?
- 3. 互斥锁方案下,其他线程等待多久?
- 七、数据一致性
- 1. Redis和MySQL数据一致性如何保证?
- 2. 项目实战:先更新DB还是先删缓存?
- 3. 最终一致还是强一致?
- 八、分布式锁与项目实战
- 1. Redis分布式锁实现原理
- 2. 可能出现的问题及解决方案
- 3. RedLock算法的争议
- 4. 可重入锁实现
- 5. 看门狗机制
- 6. 项目实战:军用多模态检索系统数据更新
一、基础与数据类型
1. Redis有哪些数据类型?底层实现和应用场景是什么?
| 数据类型 | 底层实现 | 项目实战场景 |
|---|---|---|
| String | 简单动态字符串(SDS)。 -embstr(短字符串):SDS和RedisObject连续存储,省内存。 -raw(长字符串):两者分开存储。 | 项目实战: 1.分布式锁: set article:1001:lock uuid nx ex 102.计数器:文章浏览量 incr article:9527:view3.Token存储:用户登录态 |
| List | -ziplist(元素少且值小):连续内存,节省空间。 -linkedlist(元素多):双向链表,方便两端操作。 | 项目实战: 1.消息队列: LPUSH+BRPOP实现生产者-消费者。2.最新动态: LPUSH user:1001:news+LTRIM保留前100条。 |
| Hash | -ziplist(字段少且值小)。 -hashtable(字段多):哈希表,链地址法解决冲突。 | 项目实战: 1.对象缓存:存储用户信息 hmset user:1001 name "张三" age 252.购物车: hincrby cart:1001 article:2001 1 |
| Set | -intset(整数且少):有序无重复整数数组,二分查找。 -hashtable(非整数或多):元素作键,值为NULL。 | 项目实战: 1.共同好友: sinter user:1001:friends user:1002:friends2.抽奖去重: sadd lottery:2024 userid |
| ZSet | -ziplist(元素少且值小)。 -skiplist + hashtable:跳跃表保证有序,哈希表快速查分值。 | 项目实战: 1.排行榜: zadd rank:day:20240301 100 user12.延迟队列:时间戳作为score, zrangebyscore拉取到期任务 |
2. Redis常用的命令有哪些?
- 通用:
keys(生产禁用)、scan(推荐)、expire、ttl、del - String:
set、get、incr、setnx、mset - Hash:
hset、hget、hgetall、hincrby - List:
lpush、rpop、brpop、lrange - Set:
sadd、smembers、sinter、sunion - ZSet:
zadd、zrange、zrevrange、zscore
二、持久化
1. Redis提供了哪几种持久化方式?如何选择?
RDB(快照)
- 原理:每隔一段时间,将内存中的数据集写到磁盘Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程不进行任何IO操作
- 保存策略:
save(同步阻塞)、bgsave(异步)、配置文件自动触发。save x x 几秒内有几个key的变化就触发 - 优点:文件紧凑,恢复快,对主进程性能影响小。
- 缺点:可能丢失最后一次快照后的数据。
AOF(追加文件)
- 原理:以日志形式记录每个写操作,重启时重放。
- 刷盘策略:
always:每次写命令都同步,最安全,性能最差everysec:每秒同步,最多丢1秒数据(默认推荐)no:由操作系统决定,性能最好,最不安全
- 优点:数据安全性高。
- 缺点:文件大,恢复慢,IO压力大。
混合持久化
- 原理:RDB做全量快照 + AOF做增量日志。
- 优势:结合两者优点,重启加载RDB快,数据丢失少。
生产环境选择策略
- 方案一(推荐):RDB + AOF混合持久化。RDB做冷备和快速恢复,AOF保证数据可靠性。
- 方案二(缓存场景):完全关闭持久化,只做内存缓存,性能最高。
- 方案三(主从架构):主库关闭持久化(避免fork影响性能),从库开启AOF。
三、高性能与线程模型
1. Redis为什么快?
- 内存操作:基于内存操作,读写快。
- 单线程模型:避免多线程的锁竞争,保证操作原子性。
- 数据结构简单:对数据操作也简单。使用底层模型不同:
- 构建了自己的VM 机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求
- I/O 多路复用:使用I/O多路复用模型,非阻塞IO
2. Redis为什么是单线程的?现在还是单线程吗?
核心澄清:
- Redis的命令执行仍然是单线程的。
- Redis 6.0+ 引入了多线程IO,用于网络数据的读写和解析,但命令执行依然是单线程。
为什么命令执行保持单线程?
- 避免锁竞争,简化数据结构和逻辑。
- 瓶颈通常不在CPU,而在内存和网络IO。
- 多线程IO解决了网络吞吐的瓶颈。
多线程IO配置:
io-threads 4 # 开启4个IO线程 io-threads-do-reads yes # 开启读线程四、Redis事务
1. Redis事务的ACID特性
Redis事务的ACID特性与传统数据库事务的区别:
核心结论:Redis事务不是严格意义上的ACID事务,它更像是一个批量执行命令的机制。
2. Redis事务的实现原理
事务队列:
- 执行MULTI后,Redis会将后续的命令放入一个队列中,不立即执行。
- 命令入队时,Redis会检查语法错误,如果有语法错误,会立即返回错误。
- 语法正确的命令会被放入队列,等待EXEC执行。
命令执行:
- 执行EXEC时,Redis会按顺序执行队列中的所有命令。
- 执行过程中,如果某个命令执行失败(如对非字符串类型执行INCR),后续命令仍会继续执行。
- EXEC返回所有命令的执行结果,包括错误信息。
WATCH机制的实现:
- WATCH命令会将key标记为"被监视"。
- 在EXEC执行前,Redis会检查被监视的key是否被修改。
- 如果被修改,EXEC会返回nil,事务执行失败。
- 如果未被修改,EXEC正常执行,并清除所有监视标记。
5. Redis事务的适用场景
五、高可用与集群
1. Redis集群方案有哪些?
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 主从复制 | 一主多从,从库同步主库数据。 | 读写分离,读扩展,数据备份。 | 手动故障转移,写单点。 |
| 哨兵模式 | 在主从基础上加监控、通知、自动故障转移。 | 高可用,自动主从切换。 | 写单点,数据量受单机内存限制。 |
| Redis Cluster | 去中心化,数据分片(16384个槽),每个节点负责一部分槽。 | 分布式,线性扩展,自动故障转移。 | 架构复杂,跨节点操作受限。 |
2. 哨兵模式工作流程
- 主观下线:单个哨兵发现节点无响应。
- 客观下线:多个哨兵(>=quorum)都认为主节点下线。
- 选举领头哨兵:基于Raft算法选出一个执行故障转移。
- 故障转移:
- 从从库中选一个新主库(优先级 > 复制偏移量 > runid)
- 其他从库指向新主库
- 通知客户端
3. Redis Cluster哈希槽原理
- 集群有16384个哈希槽。
key通过 CRC16(key) % 16384 计算属于哪个槽。- 每个节点负责一段连续的槽范围(如 0-5000)。
- 槽是数据迁移和负载均衡的基本单位。
为什么不使用一致性哈希?
哈希槽简化了数据分布和迁移的实现,节点增减只需迁移部分槽,不用全部重新哈希。
六、缓存常见问题
1. 缓存穿透、击穿、雪崩的区别与解决方案
| 问题 | 现象 | 原因 | 解决方案 |
|---|---|---|---|
| 缓存穿透 | 请求数据在缓存和DB都不存在,每次都打到DB。 | 恶意攻击(查不存在的ID)。 | 1.布隆过滤器:拦截不存在请求。 2.缓存空值:key-null,短过期时间。 |
| 缓存击穿 | 热点key过期瞬间,大量请求打到DB。 | 热点key过期 + 高并发。 | 1.永不过期:后台更新。 2.互斥锁:只让一个线程查DB更新缓存。 |
| 缓存雪崩 | 大量key同时过期,或Redis宕机,DB被打垮。 | 集中过期/缓存故障。 | 1.过期时间加随机值。 2.集群高可用。 3.限流降级。 |
2. 布隆过滤器误判怎么办?
背景:布隆过滤器判断"不存在"一定准确,判断"存在"可能误判。
解决方案:
- 容忍:业务允许少量误判(如推荐系统)。
- 兜底:即使布隆说过"存在",查DB没数据,按穿透处理(缓存空值)。
- 定期重建:定时从DB重新构建布隆过滤器。
七、数据一致性
1. Redis和MySQL数据一致性如何保证?
常见方案对比:
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 旁路缓存 | 读:先查缓存,没有查DB并回写。 写:先更新DB,再删缓存。 | 简单,适合读多写少。 | 删除缓存可能失败,需重试。 |
| 读写穿透 | 缓存层封装DB操作,应用只操作缓存。 | 应用逻辑简单。 | 缓存层复杂,实现难度大。 |
| 异步消息 | 更新DB后发消息,消费者更新缓存。 | 解耦,最终一致。 | 消息可能丢失或重复。 |
| Canal监听binlog | 监听MySQL binlog变更,异步更新缓存。 | 与业务代码解耦,可靠。 | 引入Canal组件,复杂度高。 |
2. 项目实战:先更新DB还是先删缓存?
经典问题:先删缓存再更新DB,可能导致脏数据。
缓存更新策略:
- 旁路缓存:读时先查 Redis,无则查 MySQL 并更新 Redis;写时先更新 MySQL,再删 Redis缓存。
- 延迟双删策略,更新完数据库后删除缓存,休眠一小会儿-几百毫秒,再删一次缓存。(原因:线程1更新数据库的瞬间,线程2查询到了旧数据,然后线程1删除缓存,此时网络延迟线程2把查询到的旧数据写到缓存里面了,导致读取到旧数据)
- 读写穿透:读、写操作由缓存处理,缓存负责和数据库同步。异步缓存写入:写操作直接更新 Redis,Redis 异步更新到 MySQL。
- 消息队列:更新 MySQL 时发消息到队列,消费者据此更新 Redis,实现最终一致。
- 分布式事务:如两阶段提交,协调者让参与者准备和提交;补偿事务(TCC)分尝试、确认、取消阶段。
- 定期同步:用定时任务定期从 MySQL 取数据更新到 Redis
八、分布式锁与项目实战
1. Redis分布式锁实现原理
使用场景:
定时任务操作数据库,是多实例的,所以需要使用到分布式锁
大模型问答的时候,同一个用户问答流式回答过程中,不能开启两个页面去问答,使用到了分布式锁
使用流程
- 用set nx ex 原子命令加锁,key是锁的唯一键(可以使用业务:方法:唯一值),value是线程id,执行成功说明获取到了锁,可以执行业务
- 在finally里面使用lua脚本校验线程id是否一致,然后再释放锁
- 问题1:死锁问题:业务代码崩溃,长时间不释放锁,导致死锁
- 解决:加上过期时间
- 问题2:锁误释放问题:A线程获取到了锁,但是执行时间比较长,导致锁过期了,B线程获取到了锁,此时A线程执行完了要释放锁,却把B的锁释放了
- 解决:给每个锁设置唯一标识,在finally里面使用lua脚本校验线程id是否一致,然后再释放锁
- 问题3:锁过期导致并发问题:业务还没执行完,锁被释放,导致并发问题
- 解决:使用redission的看门狗机制,锁会自动续期,30秒
- 问题4:集群下数据一致性问题:主从复制延迟,导致锁失效
- 解决:使用redission的红锁算法,客户端向一半以上的节点获取锁,获取到了说明成功,释放的时候向所有节点释放锁
2. 看门狗机制
作用:业务执行时间可能超过锁过期时间,自动续期。
实现思路:
- 获取锁成功后,启动一个守护线程。
- 每隔 1/3 过期时间(如过期10秒,每3秒检查一次),如果任务还在执行,就续期。
- Redisson框架已实现此机制。
