Redis内存模型与对象系统:深入理解redisObject

引言 在使用Redis时,我们操作的是String、Hash、List这些高层数据类型。但你是否想过:Redis是如何在内存中存储和管理这些对象的?为什么同样是存储字符串,Redis能做到如此高的性能和内存利用率? 今天我们深入Redis底层,揭开对象系统的神秘面纱。理解对象系统,是理解Redis高性能和内存优化的基础。 一、为什么需要对象系统? 1.1 直接存储的问题 假设我们不用对象系统,直接在内存中存储数据,会遇到什么问题? // 简单粗暴的方式 char* key = "user:1001"; char* value = "张三"; // 问题1:如何知道value的类型?是字符串?整数?还是列表? // 问题2:如何实现引用计数?避免重复拷贝 // 问题3:如何记录对象的访问时间?用于LRU淘汰 // 问题4:如何选择最优的底层编码?节省内存 1.2 对象系统的价值 Redis设计了一个统一的对象系统redisObject,解决了以下问题: 类型识别:明确知道对象是什么类型(String/Hash/List/Set/ZSet) 编码优化:根据数据特点自动选择最优编码方式(节省内存) 内存管理:引用计数、LRU/LFU淘汰、内存回收 命令多态:同一个命令可以作用于不同编码的对象 类型安全:避免类型错误操作(如对字符串执行LPUSH) 二、redisObject核心结构 2.1 源码定义 typedef struct redisObject { unsigned type:4; // 类型(4 bits):String、Hash、List、Set、ZSet unsigned encoding:4; // 编码(4 bits):底层实现方式 unsigned lru:24; // LRU时间戳(24 bits)或LFU数据 int refcount; // 引用计数(32 bits) void *ptr; // 指向实际数据的指针(64 bits) } robj; 结构说明: 总大小:16字节(紧凑设计,节省内存) 4 bits + 4 bits + 24 bits = 32 bits = 4字节 refcount:4字节 ptr:8字节(64位系统) 2.2 字段详解 2.2.1 type:对象类型(4 bits) #define OBJ_STRING 0 // 字符串 #define OBJ_LIST 1 // 列表 #define OBJ_SET 2 // 集合 #define OBJ_ZSET 3 // 有序集合 #define OBJ_HASH 4 // 哈希表 示例: ...

2025-01-21 · maneng

Pipeline批量操作:性能优化的利器

引言 恭喜你!这是第一阶段的最后一篇文章。 前面我们学习了Redis的所有基础知识: ✅ 5大数据类型 ✅ 过期和淘汰 ✅ 持久化 ✅ 事务 今天我们学习一个性能优化神器:Pipeline(管道)。 一个真实的案例: 需求:批量获取10000个用户信息 方式1:循环GET,耗时10秒 方式2:MGET,耗时0.5秒 方式3:Pipeline,耗时0.3秒 性能提升:30倍! 一、RTT延迟问题 1.1 什么是RTT? RTT(Round-Trip Time):往返时间,从客户端发送请求到收到响应的时间。 客户端 Redis服务器 | | | ----发送命令----> | 1ms | | | <----返回结果---- | 1ms | | RTT = 2ms 实际测量: # 本地Redis $ redis-cli --latency min: 0.05, max: 2, avg: 0.12 (ms) # 同机房Redis RTT ≈ 0.5-1ms # 跨机房Redis RTT ≈ 5-10ms # 跨地域Redis RTT ≈ 50-100ms 1.2 RTT的影响 单条命令的性能: ...

2025-01-21 · maneng

事务与原子性:MULTI/EXEC命令详解

引言 在关系型数据库中,事务是保证数据一致性的重要手段(ACID)。那么Redis有事务吗? 答案是:有,但不完全是你理解的那种事务。 Redis的事务更像是批量命令,提供的是有限的原子性,而不是ACID中的那种强事务。 一、Redis事务的本质 1.1 什么是Redis事务? Redis事务是一组命令的集合,这些命令会: 顺序执行:按队列顺序依次执行 不被打断:执行期间不会插入其他客户端的命令 要么全执行,要么全不执行(有限制) 示例: 127.0.0.1:6379> MULTI # 开始事务 OK 127.0.0.1:6379> SET account:1 100 QUEUED 127.0.0.1:6379> SET account:2 200 QUEUED 127.0.0.1:6379> EXEC # 执行事务 1) OK 2) OK 1.2 Redis事务 vs 关系型数据库事务 特性 MySQL事务 Redis事务 原子性(A) ✅ 全部成功或回滚 ⚠️ 部分支持 一致性(C) ✅ 约束检查 ⚠️ 无约束 隔离性(I) ✅ 多种隔离级别 ⚠️ 无隔离级别 持久性(D) ✅ 提交后持久化 ⚠️ 取决于配置 回滚 ✅ 支持 ❌ 不支持 关键区别: MySQL: BEGIN; UPDATE account SET balance = balance - 100 WHERE id = 1; UPDATE account SET balance = balance + 100 WHERE id = 2; COMMIT; -- 要么都成功,要么都回滚 Redis: MULTI DECR account:1:balance 100 INCR account:2:balance 100 EXEC -- 命令出错也不回滚! 二、事务命令详解 2.1 MULTI - 开始事务 127.0.0.1:6379> MULTI OK # 此后的命令不会立即执行,而是进入队列 127.0.0.1:6379> SET key1 "value1" QUEUED # 入队 127.0.0.1:6379> SET key2 "value2" QUEUED 2.2 EXEC - 执行事务 127.0.0.1:6379> EXEC 1) OK # SET key1的返回值 2) OK # SET key2的返回值 # 所有命令顺序执行,一次性返回结果 2.3 DISCARD - 取消事务 127.0.0.1:6379> MULTI OK 127.0.0.1:6379> SET key1 "value1" QUEUED 127.0.0.1:6379> DISCARD # 取消事务 OK # 队列中的命令全部丢弃,不执行 127.0.0.1:6379> GET key1 (nil) # 没有执行 2.4 WATCH - 乐观锁 问题场景: ...

2025-01-21 · maneng

持久化进阶:AOF日志详解

引言 上一篇我们学习了RDB快照,但它有一个缺点:两次快照之间的数据可能丢失。 如果你的业务对数据安全性要求很高(比如交易系统、订单系统),RDB就不够了。这时候需要AOF(Append Only File)。 AOF的核心思想:记录每一条写命令,恢复时重新执行。 RDB:拍照存档(快但可能丢数据) AOF:录像回放(慢但更安全) 一、AOF的本质 1.1 什么是AOF? AOF是命令日志(Command Log): Redis执行写命令 ↓ 记录命令到AOF文件(appendonly.aof) ↓ 服务器重启时 ↓ 重新执行AOF文件中的所有命令 ↓ 恢复数据 示例: # Redis执行的命令 127.0.0.1:6379> SET name "张三" OK 127.0.0.1:6379> SADD tags "Java" "Redis" (integer) 2 127.0.0.1:6379> ZADD rank 100 "user1" (integer) 1 # AOF文件内容(RESP协议格式) *2 $6 SELECT $1 0 *3 $3 SET $4 name $6 张三 *4 $4 SADD $4 tags $4 Java $5 Redis *4 $4 ZADD $4 rank $3 100 $5 user1 1.2 AOF配置 redis.conf: ...

2025-01-21 · maneng

持久化入门:RDB快照详解

引言 前面我们学习了Redis的数据类型和过期机制,但有一个关键问题: Redis是内存数据库,服务器重启或宕机后,数据会丢失吗? 答案是:如果不配置持久化,数据会丢失。 Redis提供两种持久化方式: RDB(Redis Database):定时快照,保存某个时间点的数据副本 AOF(Append Only File):记录写命令日志,恢复时重放 今天我们先学习RDB,下一篇学习AOF。 一、RDB的本质 1.1 什么是RDB? RDB就是内存快照(Snapshot): Redis内存数据 ↓ 某个时间点 ↓ 完整复制到磁盘 ↓ 生成dump.rdb文件 类似于: 给电脑硬盘做Ghost镜像 虚拟机快照 游戏存档 特点: ✅ 全量备份:保存完整数据 ✅ 体积小:二进制压缩格式 ✅ 恢复快:直接加载到内存 ❌ 丢失风险:两次快照之间的数据会丢失 1.2 RDB文件在哪里? 查看配置: 127.0.0.1:6379> CONFIG GET dir 1) "dir" 2) "/var/lib/redis" # RDB文件目录 127.0.0.1:6379> CONFIG GET dbfilename 1) "dbfilename" 2) "dump.rdb" # RDB文件名 # 完整路径:/var/lib/redis/dump.rdb 文件示例: $ ls -lh /var/lib/redis/ -rw-r--r-- 1 redis redis 128M Jan 21 14:00 dump.rdb 1.3 RDB的工作流程 1. Redis接收到SAVE/BGSAVE命令或满足自动触发条件 ↓ 2. Fork子进程(写时复制) ↓ 3. 子进程将内存数据写入临时RDB文件 ↓ 4. 替换旧的RDB文件 ↓ 5. 完成,父进程继续处理请求 二、触发RDB的三种方式 2.1 手动触发:SAVE命令 阻塞式保存: ...

2025-01-21 · maneng

键空间管理:过期策略与淘汰机制

引言 前面我们学习了Redis的5大数据类型,知道如何存储数据。但有一个关键问题: Redis的内存是有限的,数据会一直增长,内存满了怎么办? 这就涉及到两个核心机制: 过期策略:如何删除过期的键? 淘汰策略:内存不足时,删除哪些键? 理解这两个机制,是使用Redis的必修课。 一、为什么需要过期机制? 1.1 内存是有限的 假设你有一台8GB内存的Redis服务器: 缓存用户Session:每个100KB,1万在线用户 = 1GB 缓存商品详情:每个50KB,10万商品 = 5GB 缓存热点数据:2GB 总需求:1GB + 5GB + 2GB = 8GB(刚刚好) 但是: Session如果不过期,用户越来越多,内存会爆 过期商品不删除,占用空间 临时缓存不清理,内存泄漏 结论:必须有过期机制自动清理。 1.2 过期时间的设置 EXPIRE命令族: # EXPIRE:设置秒级过期时间 127.0.0.1:6379> SET session:abc123 "user:1001" OK 127.0.0.1:6379> EXPIRE session:abc123 3600 (integer) 1 # 1小时后过期 # EXPIREAT:设置到某个时间戳过期 127.0.0.1:6379> EXPIREAT session:abc123 1735689600 (integer) 1 # PEXPIRE:设置毫秒级过期时间 127.0.0.1:6379> PEXPIRE cache:hot 5000 (integer) 1 # 5秒后过期 # PEXPIREAT:毫秒级时间戳 127.0.0.1:6379> PEXPIREAT cache:hot 1735689600000 (integer) 1 # SET命令直接设置过期时间(推荐) 127.0.0.1:6379> SET cache:data "value" EX 60 OK 127.0.0.1:6379> SETEX cache:data 60 "value" OK 查看过期时间: ...

2025-01-21 · maneng

ZSet有序集合:排行榜的终极方案

引言 前面我们学习了Set(无序、唯一),今天要学习有序且唯一的ZSet(Sorted Set)。 想象一下这些场景: 🏆 游戏排行榜:按分数从高到低排序,实时更新 🔥 热搜榜:按热度值排序,展示TOP 10 ⏰ 延迟队列:按时间戳排序,到期自动执行 💰 按价格筛选:查询1000-5000元的商品 这些场景的共同特点是:需要排序、需要快速查询范围。ZSet正是为此而生,它是Redis中最强大、最复杂的数据类型。 一、ZSet的本质 1.1 什么是ZSet? ZSet(Sorted Set)是一个按分数排序的有序集合: ZSet: { (member1, score1), (member2, score2), (member3, score3), ... } 特点: - 有序:按score从小到大排序 - 唯一:member不能重复 - 分数可重复:多个member可以有相同score - 支持范围查询:按score或按排名查询 示例: # 添加元素(member:score) 127.0.0.1:6379> ZADD leaderboard 100 "张三" 95 "李四" 92 "王五" (integer) 3 # 按分数从低到高查询 127.0.0.1:6379> ZRANGE leaderboard 0 -1 WITHSCORES 1) "王五" 2) "92" 3) "李四" 4) "95" 5) "张三" 6) "100" # 按分数从高到低查询 127.0.0.1:6379> ZREVRANGE leaderboard 0 -1 WITHSCORES 1) "张三" 2) "100" 3) "李四" 4) "95" 5) "王五" 6) "92" 1.2 ZSet vs Set 特性 Set ZSet 有序性 无序 有序(按score) 唯一性 元素唯一 元素唯一 分数 无 有 范围查询 不支持 支持 排名查询 不支持 支持 时间复杂度 O(1) O(log n) 适用场景 去重、标签 排行榜、范围查询 1.3 底层实现:跳表(Skiplist) ZSet底层使用**跳表(Skiplist)+ 哈希表(Hashtable)**实现: ...

2025-01-21 · maneng

Set类型应用:去重与集合运算

引言 前面我们学习了有序的List,今天要学习无序但唯一的Set。 想象一下这些场景: 🏷️ 文章标签:“Java”、“Redis”、“数据库”,每个标签只能添加一次 👥 共同好友:你和张三有哪些共同好友? 🎲 抽奖去重:从1000个用户中随机抽10个中奖者,不能重复 📊 UV统计:今天有多少独立访客? 这些场景的共同特点是:需要去重、需要集合运算。Set正是为此而生。 一、Set的本质 1.1 什么是Set? Set是一个无序的、不重复的字符串集合: Set: {element1, element2, element3, ...} 特点: - 无序:元素没有固定顺序 - 唯一:元素不会重复 - 支持集合运算:交集、并集、差集 示例: # 添加元素 127.0.0.1:6379> SADD myset "apple" "banana" "orange" (integer) 3 # 重复添加无效 127.0.0.1:6379> SADD myset "apple" (integer) 0 # 0表示未添加(已存在) # 查看所有元素(无序) 127.0.0.1:6379> SMEMBERS myset 1) "banana" 2) "orange" 3) "apple" # 顺序可能每次都不同 1.2 Set vs List 特性 Set List 有序性 无序 有序 唯一性 元素唯一 可重复 查询元素是否存在 O(1) O(n) 按索引访问 不支持 支持 集合运算 支持 不支持 适用场景 去重、标签、关系 队列、时间线 1.3 底层实现 Set有两种底层编码: ...

2025-01-21 · maneng

List类型详解:从队列到时间线

引言 在前面我们学习了String和Hash,它们都是"单值"类型。今天我们要学习第一个"集合"类型:List。 List最大的特点是:有序、可重复、双端操作。 想象一下这些场景: 📝 微信朋友圈:按时间倒序展示,最新的在最前面 📮 消息队列:先进先出(FIFO),生产者推送,消费者弹出 📰 最新文章列表:保留最新的100篇,自动淘汰旧的 ⏱️ 操作日志:记录用户最近的操作历史 这些场景的共同特点是:需要保持顺序,支持两端操作。List正是为此而生。 一、List的本质 1.1 什么是List? List是一个双向链表(Doubly Linked List): head tail ↓ ↓ [A] ⇄ [B] ⇄ [C] ⇄ [D] ⇄ [E] ↑ ↑ 左端(head) 右端(tail) LPUSH/LPOP RPUSH/RPOP 特点: ✅ 有序:元素按插入顺序排列 ✅ 可重复:允许重复元素 ✅ 双端操作:可以从左端或右端插入/弹出 ✅ 索引访问:支持按索引读取(但不推荐频繁使用) 1.2 List vs 数组 特性 List(链表) 数组 按索引访问 O(n) O(1) 头尾插入/删除 O(1) O(n) 中间插入/删除 O(n) O(n) 内存占用 较高(指针) 较低 适用场景 队列、栈 随机访问 结论:List适合队列操作,不适合频繁随机访问。 1.3 底层实现 Redis 3.2之前: ...

2025-01-21 · maneng

Hash类型实战:对象存储的最佳选择

引言 上一篇我们学习了String类型,可以用JSON序列化存储对象。但是有一个问题: 如果只想修改用户的一个字段(比如年龄),需要怎么做? // String方式:读取→反序列化→修改→序列化→写入 String json = redis.get("user:1001"); User user = JSON.parseObject(json, User.class); user.setAge(26); // 只改了年龄 redis.set("user:1001", JSON.toJSONString(user)); 这样做有几个问题: ❌ 需要整个对象序列化/反序列化 ❌ 网络传输整个对象(浪费带宽) ❌ 并发修改容易出现覆盖问题 Hash类型就是为了解决这个问题而生的。 一、Hash的本质 1.1 什么是Hash? Hash就是一个键值对集合(类似Java的HashMap、Python的dict): Hash对象 ├── field1: value1 ├── field2: value2 └── field3: value3 在Redis中: 外层键(Key):对象的唯一标识 内层字段(Field):对象的属性名 值(Value):属性值 示例: # 用户对象 user:1001 ├── name: "张三" ├── age: "25" ├── email: "zhangsan@example.com" └── city: "北京" 1.2 Hash vs String 维度 String (JSON) Hash 存储方式 整个对象序列化为JSON 每个字段单独存储 修改单个字段 需要整体读写 直接修改该字段 内存占用 略高(JSON格式化) 略低(原生存储) 可读性 好(JSON可读) 一般(需遍历) 性能 读写整个对象 按需读写字段 适用场景 整体读写 频繁修改部分字段 选择原则: ...

2025-01-21 · maneng

如约数科科技工作室

浙ICP备2025203501号

👀 本站总访问量 ...| 👤 访客数 ...| 📅 今日访问 ...