文章目录
一、数据结构篇(必考题!)
1. String类型的底层实现
你以为的字符串其实暗藏玄机!Redis的String类型由SDS(Simple Dynamic String)实现,这个结构比C语言的char数组多了三个关键字段:
- free:剩余可用空间(预分配策略的关键)
- len:已用长度(O(1)时间复杂度获取长度)
- buf:字节数组(兼容C字符串)
举个实际案例:当执行APPEND
命令时,Redis会先检查free空间是否足够。如果不够就会触发扩容——不是简单的翻倍,而是采用空间预分配策略(小于1MB时翻倍,超过后每次+1MB)
2. Hash的渐进式rehash
当哈希表的负载因子超过阈值时,Redis会怎么做?传统做法是瞬间迁移数据,但这样会导致服务阻塞!于是Redis搞了个骚操作:
// 源码中的字典结构
typedef struct dict {
dictType *type;
dictht ht[2]; // 双哈希表结构
long rehashidx; // rehash进度索引
} dict;
迁移过程分为三步走:
- 分配ht[1]空间(新容量是ht[0].used*2)
- 将rehashidx设为0(开始迁移标志)
- 每次增删改查时顺带迁移一个桶(渐进式迁移)
(注意!)此时如果执行BGSAVE
,Redis会强制完成rehash,避免父进程和子进程写时复制冲突
二、持久化陷阱大全
1. RDB vs AOF生死抉择
![对比表格建议用文字描述]
维度 | RDB | AOF |
---|---|---|
数据完整性 | 可能丢失几分钟数据 | 最多丢失1秒数据 |
恢复速度 | 快 | 慢 |
文件体积 | 小(二进制压缩) | 大(文本命令) |
写盘方式 | 内存快照 | 追加日志 |
适用场景 | 灾难恢复 | 实时持久化 |
(致命坑点!)当同时开启两种持久化时,重启加载流程是:
- 优先加载AOF文件
- 如果AOF不存在才加载RDB
- 如果两者都不存在→空数据库
2. AOF重写黑科技
你以为的AOF重写是读旧文件?Too young!Redis直接基于内存数据生成新AOF:
# 查看当前重写状态
redis-cli info persistence | grep aof_rewrite_in_progress
触发条件:
- 手动执行
BGREWRITEAOF
- 自动触发(根据配置的增长率阈值)
(血泪教训)当执行FLUSHALL
后立即触发AOF重写,会导致所有数据永久丢失!解决方案:立即SHUTDOWN NOSAVE
然后手动删除最后的FLUSHALL命令
三、集群模式深水区
1. 槽位分配算法
16384个槽不是随便定的!这个数字的奥秘在于:
- 集群节点数最大1000时,每个节点约16个槽
- 心跳包大小限制(16384/8=2KB)刚好不超过MTU
迁移槽位的正确姿势:
# 查看槽位分布
CLUSTER SLOTS
# 开始迁移
CLUSTER SETSLOT <slot> IMPORTING <node-id>
CLUSTER SETSLOT <slot> MIGRATING <node-id>
# 逐个迁移key
CLUSTER GETKEYSINSLOT <slot> <count>
MIGRATE <host> <port> "" 0 5000 KEYS key1 key2...
2. 脑裂问题终极解决方案
当网络分区发生时,Redis集群可能出现双主写入。Redis的应对策略是:
- 节点超时(cluster-node-timeout)
- 从节点数据延迟检查
- 最小主节点数验证(cluster-migration-barrier)
(必杀技)配置min-slaves-to-write 1
可以强制主节点必须有至少1个从节点才能写入,有效降低数据不一致风险
四、实战场景三连击
场景1:缓存雪崩
错误做法:大量key设置相同过期时间
正确姿势:
def set_cache(key, value, expire):
# 基础过期时间
base_expire = 3600
# 随机增加0-300秒抖动
real_expire = base_expire + random.randint(0, 300)
redis_client.set(key, value, ex=real_expire)
场景2:热点Key发现
使用redis-cli --hotkeys
命令(需开启LFU算法)
监控代码示例:
// 使用Jedis的监控功能
jedis.monitor(new JedisMonitor() {
@Override
public void onCommand(String command) {
if(command.startsWith("GET ")) {
String key = command.split(" ")[1];
hotKeyCache.increment(key);
}
}
});
场景3:分布式锁进阶
Redlock算法的正确实现姿势:
- 获取当前毫秒级时间戳
- 依次向N个实例申请锁(使用相同key和随机值)
- 计算获取锁耗时(必须小于锁有效期)
- 当且仅当半数以上节点获取成功,并且总耗时小于有效期时才算成功
(超级重点!)一定要用pexpire
设置毫秒级过期时间,避免服务器间时钟不同步问题
五、刁钻问题反杀指南
面试官:Redis为什么这么快?
菜鸟答案:因为是内存数据库
高手答案:
- IO多路复用+单线程架构(避免上下文切换)
- 高效数据结构(跳表、压缩链表等)
- 直接访问内存(相比磁盘快5个数量级)
- 渐进式Rehash保证平滑扩容
- pipeline批处理减少网络往返
面试官:如何实现延迟队列?
青铜方案:ZSET时间戳作为score
王者方案:
-- 使用Lua脚本保证原子性
local jobId = redis.call('INCR', 'delay:job:id')
redis.call('ZADD', 'delay:queue', ARGV[1], jobId)
redis.call('HSET', 'delay:jobs', jobId, ARGV[2])
return jobId
配合zrangebyscore
和hget
实现精准投递
下次面试被问Redis时,把这些知识点甩出来,面试官绝对眼前一亮!(记得自己动手写demo验证哦)