视频看了几百小时还迷糊?关注我,几分钟让你秒懂!
你是否曾好奇:当你在 Java 代码中调用redisTemplate.opsForValue().set("user:1001", "张三")时,Redis 内部到底发生了什么?
一条数据是如何从你的 Spring Boot 应用,穿越网络,最终安全地存入 Redis 内存中的?
本文将带你逐层拆解这个过程,涵盖客户端、网络协议、服务端处理、内存结构、持久化等关键环节,并结合反例与注意事项,让你真正理解 Redis 的写入机制。
一、整体流程概览
graph LR A[Spring Boot 应用] -->|1. 构造命令| B(Lettuce/Jedis 客户端) B -->|2. RESP 协议编码| C[网络传输 TCP] C -->|3. Redis 服务端接收| D[事件循环 Event Loop] D -->|4. 命令解析| E[执行 SET 命令] E -->|5. 内存分配| F[SDS + Redis Object] F -->|6. 可选持久化| G[RDB/AOF]下面,我们一步步深入。
二、Step 1:客户端构造命令(Spring Boot 层)
✅ 正确写法:
@Autowired private StringRedisTemplate redisTemplate; public void saveUser(Long id, String name) { // key = "user:1001", value = "张三" redisTemplate.opsForValue().set("user:" + id, name, 30, TimeUnit.MINUTES); }🔍 背后发生了什么?
StringRedisTemplate使用Lettuce(默认)或 Jedis 客户端- 将 Java 对象转换为 Redis 命令:
SET user:1001 张三 EX 1800 - 自动序列化:String 类型无需额外序列化器
❌ 反例:存储大对象不设 TTL
// 错误:未设置过期时间,导致内存泄漏! redisTemplate.opsForValue().set("user:full:info:1001", hugeJsonString);⚠️ 后果:key 永久存在,内存持续增长,最终 OOM。
三、Step 2:RESP 协议编码(客户端 → 网络)
Redis 使用RESP(Redis Serialization Protocol)作为通信协议,简单高效。
示例:SET user:1001 张三 EX 1800的 RESP 编码
*5 $3 SET $10 user:1001 $6 张三 $2 EX $4 1800*5:表示有 5 个参数$3:下一个字符串长度为 3(即 "SET")- 所有数据以
\r\n结尾
✅优势:文本协议,易解析;二进制安全(支持任意字节)
四、Step 3:网络传输(TCP 连接)
- 客户端通过TCP 连接将 RESP 数据包发送到 Redis 服务端(默认端口 6379)
- Redis 使用I/O 多路复用(epoll/kqueue)监听连接,非阻塞接收数据
- 数据进入 Redis 的输入缓冲区(querybuf)
📌 注意:若网络延迟高或带宽不足,会影响写入性能。建议 Redis 与应用部署在同一内网。
五、Step 4:服务端处理(Event Loop + 命令分发)
Redis 是单线程事件驱动模型:
- 事件循环检测到 socket 可读
- 读取数据到
client->querybuf - 解析命令:按 RESP 格式拆分为
argv[]数组argv[0] = "SET" argv[1] = "user:1001" argv[2] = "张三" argv[3] = "EX" argv[4] = "1800" - 查找命令表,找到
setCommand函数指针 - 调用
setCommand(client)执行
✅关键点:整个过程在主线程完成,无锁,无上下文切换。
六、Step 5:内存存储(核心!)
这是最核心的一步:如何把 key-value 存入内存?
6.1 创建 Redis 对象(redisObject)
Redis 为每个 value 包装一个redisObject,包含类型、编码、引用计数等:
typedef struct redisObject { unsigned type:4; // OBJ_STRING unsigned encoding:4; // OBJ_ENCODING_EMBSTR 或 RAW int refcount; // 引用计数 void *ptr; // 指向实际数据 } robj;6.2 选择底层编码(Encoding)
根据 value 大小,自动选择最优结构:
| value 特征 | 底层编码 | 说明 |
|---|---|---|
| 是整数(如 "123") | int | 直接用 long 存储 |
| 字符串 ≤ 44 字节 | embstr | 一次性分配 redisObject + SDS |
| 字符串 > 44 字节 | raw | redisObject 和 SDS 分开分配 |
💡为什么是 44 字节?
RedisObject (16B) + SDS header (8B) + 字符串 + '\0' ≤ 64B(内存分配器最小单元),避免内存碎片。
6.3 使用 SDS 存储字符串
Redis 不用 C 原生字符串,而是SDS(Simple Dynamic String):
struct sdshdr8 { uint8_t len; // 已用长度 uint8_t alloc; // 总分配长度 unsigned char flags; // 类型标识 char buf[]; // 实际字符数组 };✅优势:
- O(1) 获取长度
- 杜绝缓冲区溢出
- 二进制安全(可存图片、序列化对象)
6.4 插入全局哈希表
Redis 将 key-value 存入全局字典(dict),本质是哈希表 + 链地址法:
dictEntry *entry = dictAddRaw(db->dict, key, NULL); entry->v.val = val; // val 是 redisObject 指针📌注意:Redis 会在哈希冲突严重时自动 rehash(渐进式,不阻塞)。
七、Step 6:持久化(可选,异步)
如果开启了持久化,Redis 会异步记录写操作:
方式 1:RDB(快照)
- 不立即触发,由配置的
save规则决定(如 900 秒 1 次修改) - 主进程 fork 子进程,子进程将内存数据写入
.rdb文件
方式 2:AOF(追加日志)
- 将命令追加到
aof_buf缓冲区 - 根据
appendfsync策略(always/everysec/no)刷盘 - 不影响主线程性能(刷盘由后台线程或 OS 负责)
✅重要:持久化不会阻塞 SET 命令返回!客户端在步骤 5 完成就收到 OK。
八、完整 Spring Boot 实战示例
@Service public class UserService { @Autowired private StringRedisTemplate redisTemplate; // 保存用户信息(带过期时间) public void saveUser(User user) { String key = "user:" + user.getId(); String value = JSON.toJSONString(user); // 转为 JSON // 自动选择 embstr 或 raw 编码 redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES); // 验证存储编码(开发环境可用) // redis-cli> OBJECT ENCODING user:1001 } }如何验证底层编码?
# 连接 Redis redis-cli # 查看 key 的编码 127.0.0.1:6379> SET small "hello" OK 127.0.0.1:6379> OBJECT ENCODING small "embstr" 127.0.0.1:6379> SET large "这是一个超过44字节的字符串,用于测试raw编码" OK 127.0.0.1:6379> OBJECT ENCODING large "raw"九、常见误区与注意事项
| 误区 | 正确认知 |
|---|---|
| “SET 命令会立刻写磁盘” | ❌ 持久化是异步的,SET 返回时只保证内存写入 |
| “Redis 用 HashMap 存数据” | ❌ 用的是自研 dict(哈希表 + 渐进 rehash) |
| “字符串都用 embstr” | ❌ >44 字节会转为 raw,内存开销更大 |
| “不设 TTL 没关系” | ❌ 会导致内存泄漏,务必设置合理过期时间 |
十、总结:一条数据的旅程
- 客户端:构造 SET 命令,编码为 RESP
- 网络:TCP 传输到 Redis 服务端
- 服务端:事件循环接收 → 解析命令 → 执行 setCommand
- 内存:
- 创建 redisObject
- 根据大小选择 int/embstr/raw 编码
- 用 SDS 存储字符串
- 插入全局哈希表
- 持久化(异步):记录到 AOF 或等待 RDB 快照
💡核心思想:Redis 的快,源于内存操作 + 单线程无锁 + 精细内存管理。
结语
下次当你调用redisTemplate.set()时,不妨想象一下:此刻,Redis 正在用 SDS、redisObject、哈希表为你精心安放这条数据,而这一切,都在微秒级完成!
理解这个过程,你不仅能写出更高效的代码,还能在面试中惊艳面试官:“我知道 Redis 为什么用 embstr……”
视频看了几百小时还迷糊?关注我,几分钟让你秒懂!