Redis面试复盘:从连接到扩容与数据定位的极致详解(含RedisTemplate交互及底层剖析)
最近参加了一场Redis技术面试,面试官围绕Redis的核心机制提问,从请求连接到扩容策略,再到主从模式和数据定位,问题深入且全面。之前的复盘已涵盖大部分内容,但面试官特别要求对数据定位(问题6)和扩容后读数据(问题7)的底层实现进行剖析。这次,我将进一步完善内容,聚焦底层原理,加入RedisTemplate
的Java交互实现,并确保每个环节都细致入微。以下是博客风格的超详细复盘,供准备Redis面试的你参考。
前置知识:什么是RESP协议?
在讲解连接过程前,先介绍RESP(Redis Serialization Protocol),它是Redis通信的核心协议。
- 定义 :RESP是Redis的文本序列化协议,用于客户端与服务器交互,以
\r\n
分隔。 - 类型 :
- 简单字符串:
+OK\r\n
- 错误:
-ERR message\r\n
- 整数:
:100\r\n
- 批量字符串:
$5\r\nhello\r\n
- 数组:
*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n
- 简单字符串:
- 作用 :客户端编码命令,服务器解析并返回结果,Java客户端(如
RedisTemplate
)自动处理。
1. Redis从一次请求到服务器返回的连接过程(含RedisTemplate交互)
-
客户端连接 :
-
RedisTemplate
通过JedisConnectionFactory
建立TCP连接。 -
代码 :
javaimport org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; RedisTemplate<String, String> template = new StringRedisTemplate(new JedisConnectionFactory()); template.afterPropertiesSet(); System.out.println(template.getConnectionFactory().getConnection().ping()); // +PONG\r\n
-
-
发送命令 :
template.opsForValue().set("key", "value")
转为*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n
。 -
服务器解析与执行:单线程解析RESP,内存操作。
-
响应返回 :
+OK\r\n
,template
解码。
2. Redis怎么做横向和纵向扩容,什么情况下做(含RedisTemplate适配)
-
纵向 :升级单节点,
template
更新host。 -
横向 :加节点,用Redis Cluster。
-
代码 :
javaimport org.springframework.data.redis.connection.RedisClusterConfiguration; RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration(); clusterConfig.clusterNode("node1", 6379); RedisTemplate<String, String> template = new StringRedisTemplate(new JedisConnectionFactory(clusterConfig));
-
-
场景:内存满、QPS不足。
3. 两个Redis节点20G已满,横向还是纵向扩?子节点如何加入集群?
-
横向 :
-
加2节点,用
redis-cli --cluster add-node new_ip:6379 existing_ip:6379
。 -
子节点通过现有节点IP获取集群元数据(Gossip协议)。
-
代码 :
javaclusterConfig.clusterNode("new_ip", 6379); template.opsForValue().set("key", "value");
-
-
redis-cli环境:任意可达节点的机器。
4. 主从模式两个主一定有两个从吗?
-
不固定,灵活配置。
-
代码 :
javaRedisTemplate<String, String> master = new StringRedisTemplate(new JedisConnectionFactory("master", 6379)); RedisTemplate<String, String> slave = new StringRedisTemplate(new JedisConnectionFactory("slave", 6379)); master.opsForValue().set("key", "value");
5. 主节点能读吗?什么情况下读?
-
可读,适用于从故障或强一致性。
-
代码 :
javaString value = master.opsForValue().get("key");
6. 数据怎么定位到哪个节点?(底层剖析)
Redis的数据定位取决于部署模式,这里剖析Redis Cluster的底层实现,并结合RedisTemplate
。
-
表面交互(RedisTemplate):
javatemplate.opsForValue().set("key", "value"); // 自动定位 String value = template.opsForValue().get("key");
-
底层原理:
- 分片机制 :
- Redis Cluster将数据分为16384个哈希槽(Hash Slots)。
- 每个槽分配给一个主节点,存储在集群的槽映射表中。
- 键到槽的计算 :
- 对键应用
CRC16
算法,取模16384。 - 公式:
slot = CRC16(key) % 16384
。 - 示例:
CRC16("key")
可能是12345,12345 % 16384 = 12345
(若超出则取模)。 - 哈希标签 :
{tag}key
只对tag
计算,确保相关键在同一槽。
- 对键应用
- 节点定位 :
- 每个节点维护槽映射表(如
0-5460
在node1,5461-10922
在node2)。 - 客户端发送命令时,底层(Jedis或Lettuce)查询本地缓存的映射。
- 每个节点维护槽映射表(如
- 交互流程 :
RedisTemplate
调用opsForValue().set("key", "value")
。JedisConnectionFactory
的RedisClusterConnection
计算CRC16("key") % 16384
。- 检查槽映射,找到目标节点(如
node1:6379
)。 - 发送
*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n
。
- 重定向 :
- 若槽不在当前节点,服务器返回
-MOVED slot ip:port
。 - 示例:
-MOVED 12345 192.168.1.2:6379
。 RedisTemplate
更新映射,重新发送。
- 若槽不在当前节点,服务器返回
- 底层数据结构 :
- 节点用
clusterState
结构体维护槽和节点关系(C语言实现)。 - 客户端缓存
ClusterNode
映射,定期通过CLUSTER SLOTS
同步。
- 节点用
- 分片机制 :
-
代码验证:
javaimport org.springframework.data.redis.connection.RedisClusterConnection; RedisClusterConnection connection = template.getConnectionFactory().getClusterConnection(); System.out.println(connection.clusterGetNodes()); // 查看节点
-
手动分片:
-
客户端用一致性哈希:
javaint slot = Math.abs("key".hashCode() % 2); RedisTemplate<String, String> node = (slot == 0) ? node1Template : node2Template; node.opsForValue().set("key", "value");
-
-
关键点:
RedisTemplate
屏蔽了底层复杂度,依赖Lettuce/Jedis实现槽定位。- 网络延迟或映射过期可能触发重定向。
7. 扩主节点后怎么知道数据从哪个节点读?(底层剖析)
扩容后数据分布变化,这里剖析底层如何更新并定位。
-
表面交互(RedisTemplate):
javaclusterConfig.clusterNode("new_master", 6379); String value = template.opsForValue().get("key"); // 自动适应
-
底层原理:
-
扩容过程 :
- 新节点加入:
redis-cli --cluster add-node new_ip:6379 existing_ip:6379
。 - 槽迁移:
redis-cli --cluster reshard
,手动指定槽(如0-1000
到新节点)。 - 数据迁移:源节点将键值对发送到目标节点(
MIGRATE
命令)。
- 示例:
MIGRATE new_ip 6379 "" 0 5000 KEYS key1 key2
。
- 新节点加入:
-
槽映射更新 :
- 集群通过Gossip协议广播新映射。
- 每个节点更新
clusterState.slots
(槽到节点的映射)。
-
客户端感知 :
-
RedisTemplate
的JedisClusterConnection
定期执行CLUSTER SLOTS
。 -
返回格式:
bash*3 *3 :0 :5460 *2 $9 192.168.1.1 :6379 *3 :5461 :10922 *2 $9 192.168.1.2 :6379
-
更新本地缓存(如
JedisClusterInfoCache
)。
-
-
读数据流程 :
template.opsForValue().get("key")
。- 计算
CRC16("key") % 16384
(如12345)。 - 查询缓存,若槽12345在新节点(
new_ip:6379
),发送*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n
。 - 若缓存过期,收到
-MOVED
,跳转新节点。
-
从节点读 :
- 新主配从后,
template
连接从节点:
javaJedisConnectionFactory slaveFactory = new JedisConnectionFactory(new RedisStandaloneConfiguration("slave_ip", 6379)); RedisTemplate<String, String> slaveTemplate = new StringRedisTemplate(slaveFactory); slaveTemplate.getConnectionFactory().getConnection().readOnly(); String value = slaveTemplate.opsForValue().get("key");
- 底层同步:从节点通过
PSYNC
从主拉取数据。
- 新主配从后,
-
-
关键点:
- 底层依赖Gossip和
CLUSTER SLOTS
确保一致性。 RedisTemplate
动态适应,无需手动干预。
- 底层依赖Gossip和