当前位置: 首页 > news >正文

Redis对象机制详解

1. 引言

作为一款高性能的键值存储系统,Redis在互联网应用中扮演着举足轻重的作用。其卓越的性能不仅得益于内存存储的特性,更离不开其精巧的底层设计。本文将从redisObject这一核心数据结构入手,阐述Redis如何通过对象机制实现数据类型的封装、类型检查、多态操作、对象共享以及内存回收。

2. 概述

Redis 并没有直接使用传统的数据结构(如链表、哈希表等)来存储键值对,而是在这些数据结构之上封装了一层统一的抽象——redisObjectredisObject 是 Redis 内部定义的一个核心数据结构,它使得 Redis 能够以统一的方式管理不同类型的数据,并实现类型检查、多态性以及内存管理等高级功能。理解 redisObject 的结构是理解 Redis 对象机制的关键。

2.1 redisObject 结构详解

redisObject 结构体定义如下(简化版):

typedef struct redisObject {// 类型unsigned type:4;// 编码方式unsigned encoding:4;// LRU - 24位, 记录最末一次访问时间(相对于lru_clock); 或者 LFU(最少使用的数据:8位频率,16位访问时间)unsigned lru:LRU_BITS; // LRU_BITS: 24// 引用计数int refcount;// 指向底层数据结构实例void *ptr;} robj;

该结构体包含以下几个重要字段:

  • type (类型):这是一个4位的无符号整数,用于标识 redisObject 所存储的数据类型。Redis 支持五种主要的数据类型,它们在 redisObject 中对应的值如下:

    • OBJ_STRING:字符串对象
    • OBJ_LIST:列表对象
    • OBJ_SET:集合对象
    • OBJ_ZSET:有序集合对象
    • OBJ_HASH:哈希对象

    通过 type 字段,Redis 可以在执行命令时对键值对的类型进行检查,确保操作的合法性。例如,对一个字符串对象执行列表操作命令将会返回类型错误。

  • encoding (编码方式):这也是一个4位的无符号整数,用于标识 redisObject 所指向的底层数据结构的编码方式。Redis 为每种数据类型提供了多种编码方式,以在不同场景下优化内存使用和操作效率。例如,字符串对象可能采用 OBJ_ENCODING_RAW (原始字符串)、OBJ_ENCODING_INT (整数) 或 OBJ_ENCODING_EMBSTR (嵌入式字符串) 等编码;列表对象可能采用 OBJ_ENCODING_QUICKLIST (快速列表) 或 OBJ_ENCODING_ZIPLIST (压缩列表) 等编码。具体的编码方式取决于存储的数据量、数据特性以及Redis的版本和配置。

  • lru (最近最少使用):这是一个24位的无符号整数,用于记录对象的最近访问时间(或访问频率,如果启用了LFU策略)。这个字段主要用于实现Redis的键淘汰策略,当内存不足时,Redis会根据LRU(Least Recently Used)或LFU(Least Frequently Used)算法淘汰掉一部分键,以释放内存空间。

  • refcount (引用计数):这是一个整数,表示当前 redisObject 被引用的次数。Redis 通过引用计数来实现自动内存回收。当一个 redisObject 被创建时,其 refcount 初始化为1;当有其他地方引用该对象时,refcount 增加;当引用被移除时,refcount 减少。当 refcount 降为0时,表示该对象不再被任何地方引用,Redis 会自动释放该对象及其底层数据结构所占用的内存。

  • ptr (指针):这是一个 void* 类型的指针,指向实际存储数据的底层数据结构。ptr 所指向的具体数据结构类型由 typeencoding 字段共同决定。例如,如果 typeOBJ_STRINGencodingOBJ_ENCODING_RAW,那么 ptr 可能指向一个 sds (简单动态字符串) 结构体;如果 typeOBJ_LISTencodingOBJ_ENCODING_QUICKLIST,那么 ptr 将指向一个 quicklist 结构体。

2.2 类型与编码

Redis 的灵活性和高效性很大程度上来源于其“类型与编码”分离的设计。type 决定了对象的逻辑数据类型,而 encoding 则决定了该逻辑类型在内存中的具体物理存储方式。这种设计允许 Redis 根据实际存储的数据特点,动态选择最合适的底层数据结构,从而在内存效率和操作性能之间取得平衡。例如,一个只包含少量整数的列表,可能会被编码为 OBJ_ENCODING_ZIPLIST 以节省内存;而当列表元素数量增加或包含复杂字符串时,可能会自动转换为 OBJ_ENCODING_QUICKLIST 以提高操作效率。

3. 实现原理

3.1 类型检查与多态

在Redis中,每个键值对都由一个redisObject来表示。当客户端发送一个命令到Redis服务器时,服务器首先会根据命令所操作的键,从数据库中查找对应的redisObject。在执行具体操作之前,Redis会进行严格的类型检查。这一过程主要依赖于redisObject中的type字段。如果命令所要求的操作类型与redisObject的实际type不匹配,Redis会立即返回一个类型错误(WRONGTYPE)给客户端,从而保证了数据操作的安全性与一致性。例如,尝试对一个字符串类型的键执行LPUSH(列表操作)命令,Redis会拒绝该操作并报错。

除了类型检查,Redis还通过encoding字段实现了命令的多态性。这意味着对于同一种逻辑数据类型(type),由于其底层可能采用了不同的编码方式(encoding),Redis会根据当前的编码选择最适合的底层数据结构操作函数。这种设计使得Redis能够根据数据的实际存储情况,动态地选择最高效的算法。例如:

  • 字符串对象:当字符串内容是纯数字且长度较短时,可能以OBJ_ENCODING_INT编码存储为长整型,此时对字符串的加减操作可以直接进行整数运算,效率极高。当字符串较长或包含非数字字符时,则可能以OBJ_ENCODING_RAWOBJ_ENCODING_EMBSTR编码存储为sds(简单动态字符串),此时字符串操作会调用sds相关的函数。
  • 列表对象:当列表元素数量较少且元素值较小时,可能以OBJ_ENCODING_ZIPLIST(压缩列表)编码存储,以节省内存。当列表元素数量或单个元素大小超过一定阈值时,Redis会自动将编码转换为OBJ_ENCODING_QUICKLIST(快速列表),以提高随机访问和插入删除的效率。

这种类型检查和多态机制,使得Redis在保证数据完整性的同时,能够根据数据的特点和使用场景,灵活地选择底层实现,从而在内存占用和执行效率之间取得最佳平衡。对于开发者而言,这意味着无需关心底层数据结构的具体实现,只需关注Redis提供的抽象数据类型即可,大大简化了开发复杂度。

3.2 对象共享

为了进一步优化内存使用,Redis 实现了一种对象共享机制。对于一些常用且不变的对象,Redis 会预先创建并缓存它们,当多个键需要存储相同的值时,它们会共享同一个 redisObject,而不是为每个键都创建一个新的对象。这在以下场景中尤为常见:

  • 整数对象:Redis 会缓存0到REDIS_SHARED_INTEGERS(默认10000)之间的所有整数对象。当一个键的值是这个范围内的整数时,Redis 会直接引用这些共享对象,而不是创建新的redisObject。这对于计数器、版本号等场景非常有效,显著减少了内存开销。
  • 常用字符串对象:例如,命令的返回值(如OKERRORQUEUED等)以及一些短小的、频繁使用的字符串,Redis 也会进行共享。这些字符串对象在内部被创建一次后,可以被多个地方引用,避免了重复创建和销毁的开销。

对象共享的实现依赖于redisObject中的refcount字段。当一个对象被共享时,其refcount会增加。这种机制在读多写少的场景下,能够极大地节省内存空间,并减少CPU创建对象的开销。然而,需要注意的是,只有当redisObject作为数据库的键或值,并且其底层数据结构支持指针引用时(如字典和双端链表),才能进行对象共享。像整数集合(intset)和压缩列表(ziplist)这类只保存字面值的内存数据结构,是无法共享对象的。

3.3 内存回收机制:引用计数

C语言本身不提供自动垃圾回收机制,为了有效管理内存,Redis 的对象系统采用了基于引用计数(Reference Counting)的内存回收机制。redisObject 结构中的 refcount 字段正是为此目的而设计。

其工作原理如下:

  1. 对象创建:当一个新的 redisObject 被创建时,其 refcount 属性被初始化为1。
  2. 引用增加:当有新的地方引用该对象(例如,一个键指向该对象,或者该对象被共享给另一个数据结构)时,refcount 会增加1。
  3. 引用减少:当一个引用被移除(例如,一个键被删除,或者一个数据结构不再引用该对象)时,refcount 会减少1。
  4. 内存释放:当 refcount 降至0时,表示该 redisObject 不再被任何地方引用。此时,Redis 会自动释放该 redisObject 结构本身以及其 ptr 指向的底层数据结构所占用的内存。

引用计数机制确保了不再使用的内存能够被及时回收,避免了内存泄漏。同时,它也支持了对象共享,使得多个键可以安全地共享同一个值对象,而无需担心内存管理问题。这种机制简单高效,非常适合Redis这种对性能和内存占用有严格要求的场景。

4. 对性能的影响

Redis 的对象机制在很大程度上影响了其性能表现,主要体现在以下几个方面:

  1. 内存效率

    • 多态编码:Redis 根据数据量和数据特性选择不同的底层编码,例如,对于小整数或短字符串,使用更紧凑的编码(如OBJ_ENCODING_INTOBJ_ENCODING_EMBSTROBJ_ENCODING_ZIPLIST),这显著减少了内存占用。当数据增长到一定阈值时,Redis 会自动进行编码转换,虽然会带来一定的CPU开销,但保证了在不同规模下的内存效率。
    • 对象共享:通过共享常用整数和字符串对象,Redis 避免了重复创建大量相同对象的内存开销,尤其是在存储大量重复小数据时,内存节省效果显著。
  2. CPU开销

    • 编码转换:当底层数据结构需要从一种编码转换为另一种编码时(例如,ziplist转换为quicklist),会涉及数据的重新分配和复制,这会带来一定的CPU开销。然而,这种转换通常发生在数据量达到一定规模时,且是Redis为了优化后续操作性能而进行的权衡。
    • 引用计数:引用计数的增减操作本身开销很小,但当refcount降为0时,需要释放内存,这会涉及到内存分配器的操作,可能带来一定的CPU开销。但在大多数情况下,这种开销是可接受的,并且是实现自动内存管理所必需的。
    • 类型检查与多态:每次操作前进行类型检查和根据编码选择操作函数,虽然会增加少量的CPU指令,但这种开销相对于操作底层数据结构本身的开销来说微乎其微,且保证了操作的正确性和高效性。
  3. 数据访问速度

    • ptr指针redisObject中的ptr指针直接指向底层数据结构,使得数据访问路径短,提高了访问速度。
    • 高效底层数据结构:Redis 针对不同数据类型和编码选择了高度优化的底层数据结构(如sdsquicklistdictintsetskiplist等),这些数据结构都经过精心设计,以提供高效的增删改查操作。

总而言之,Redis 的对象机制通过精妙的内存管理(多态编码、对象共享、引用计数)和高效的底层数据结构选择,在内存占用和CPU效率之间取得了良好的平衡。虽然在某些情况下会引入少量的CPU开销(如编码转换),但这些开销是为了换取整体性能的提升和内存的有效利用,使得Redis能够在大规模数据场景下依然保持卓越的性能。

5. 总结

Redis 的对象机制是其高性能和灵活性的基石。通过引入 redisObject 这一统一的抽象层,Redis 巧妙地实现了数据类型的封装、动态编码切换、对象共享以及基于引用计数的内存管理。这种设计不仅使得 Redis 能够以极高的效率存储和操作不同类型的数据,还在内存占用和CPU开销之间取得了完美的平衡。

http://www.dtcms.com/a/300406.html

相关文章:

  • vue3.6更新哪些内容
  • 如何在 InsCodeAI 上搭建并使用 Jupyter Notebook 环境?
  • spring gateway 配置http和websocket路由转发规则
  • 零基础学习性能测试第五章:JVM性能分析与调优-GC垃圾分代回收机制与优化
  • JVM terminated. Exit code=1
  • vmware虚拟机中显示“网络电缆被拔出“的解决方法
  • MySQL存储过程与触发器
  • systemtick使用详解章
  • 计数dp(基础)
  • 【AI】联网模式
  • 【micro:bit】从入门到放弃(六):示例蜂鸣器音乐、摇色子、光照强度、串口调试、麦克风
  • vulhub Earth靶场攻略
  • Scrapy分布式爬虫数据统计全栈方案:构建企业级监控分析系统
  • 慧星云新增大模型服务:多款大模型轻松调用
  • 【leetGPU】1. Vector Addition
  • LChot100--128. 最长连续序列
  • 7月26日京东秋招第一场第一题
  • 资产负债表及其数据获取
  • earth靶场
  • 【408二轮强化】数据结构——线性表
  • Pspice仿真电路:(三十四)如何使用Pspcie进行仿真
  • mount: /mnt/sd: wrong fs type, bad option, bad superblock on /dev/mmcblk1
  • 两个USB-CAN-A收发测试
  • Item14:在资源管理类中小心拷贝行为
  • 小白成长之路-部署Zabbix7(二)
  • 每日一题【删除有序数组中的重复项 II】
  • linux shell从入门到精通(二)——变量操作
  • 深度学习损失函数的设计哲学:从交叉熵到Huber损失的深入探索
  • java--JDBC
  • OSPF路由协议之多区域划分