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连接。- 代码:
import 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。
- 代码:
import 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协议)。
- 代码:
clusterConfig.clusterNode("new_ip", 6379); template.opsForValue().set("key", "value");
- 加2节点,用
- redis-cli环境:任意可达节点的机器。
4. 主从模式两个主一定有两个从吗?
- 不固定,灵活配置。
- 代码:
RedisTemplate<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. 主节点能读吗?什么情况下读?
- 可读,适用于从故障或强一致性。
- 代码:
String value = master.opsForValue().get("key");
6. 数据怎么定位到哪个节点?(底层剖析)
Redis的数据定位取决于部署模式,这里剖析Redis Cluster的底层实现,并结合RedisTemplate。
-
表面交互(RedisTemplate):
template.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同步。
- 节点用
- 分片机制:
-
代码验证:
import org.springframework.data.redis.connection.RedisClusterConnection; RedisClusterConnection connection = template.getConnectionFactory().getClusterConnection(); System.out.println(connection.clusterGetNodes()); // 查看节点 -
手动分片:
- 客户端用一致性哈希:
int slot = Math.abs("key".hashCode() % 2); RedisTemplate<String, String> node = (slot == 0) ? node1Template : node2Template; node.opsForValue().set("key", "value");
- 客户端用一致性哈希:
-
关键点:
RedisTemplate屏蔽了底层复杂度,依赖Lettuce/Jedis实现槽定位。- 网络延迟或映射过期可能触发重定向。
7. 扩主节点后怎么知道数据从哪个节点读?(底层剖析)
扩容后数据分布变化,这里剖析底层如何更新并定位。
-
表面交互(RedisTemplate):
clusterConfig.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。- 返回格式:
*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连接从节点:
JedisConnectionFactory 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和