[Redis]1-高效的数据结构P1-String与Hash

书接上回Redis学习开篇概述,本篇开始理解Redis数据结构的设计。

Redis作为简单高效的数据库,其数据结构是使得其高效的关键。

理解Redis数据类型

Redis数据库里面的每个键值对(key-value pair)都是由对象(object)组成的,其中key总是一个字符串对象(string object),而value的值则可以是字符串对象、列表对象(list object)、哈希对象(hash object)、集合对象(set object)、有序集合对象(sorted set object)等……
目前官方文档显示支持的数据类型已经有13种。

字符串 String

Redis追求字符串的安全性、效率以及功能方面的要求。
Redis的字符串可以是各种类型字符串(包括二进制数据、序列化对象)。值的大小不能超过512MB。

Redis自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。
官网文档➡️sds

sds结构 sds.c 如下:

struct sdshdr {
    long len;
    long free;
    char buf[];
};

buf 字符数组存储实际的字符串。
len 字段存储 buf 的长度。这使得获取 Redis 字符串的长度是一个 O(1)操作。
free 字段存储可用于使用的额外字节数。
len 和 free 字段可以看作是存储 buf 字符数组的元数据。

SDS和传统C语言定义的字符串(以空字符结尾的字符数组)有哪些不同呢?
多了长度信息len与未使用空间与其说明属性free。

定义长度信息

  • 获取长度信息的时间复杂度降低
    根据传统,C语言使用长度为N+1的字符数组来表示长度为N的字符串,并且字符数组的最后一个元素总是空字符’\0’。
    C字符串并不记录自身的长度信息,获取长度时需要遍历整个字符数组至空字符以获取长度。时间复杂度为O(n)。
  • 长度信息解决缓冲区溢出问题
    已知长度则可以在操作字符串前检查是否有足够的内存。即可以依据长度分配内存。

定义未使用空间

SDS的buf[]字符串数组存在未使用的空间,buf[]的长度不一定就是字符数量加一,数组里面可以包含未使用的字节。属性free值说明未使用空间长度。

Redis需求频繁修改数据,每次修改字符串都需要重新分配内存,频繁内存重分配带来时间占用影响性能。
SDS通过未使用空间解除了字符串长度和底层数组长度之间的关联。(这也能解耦的啊
具体实现有"空间预分配"和"惰性空间释放"两种优化策略。用于解决“频繁内存重分配”,对应分配内存或释放情况。

  • 空间预分配

空间预分配用于优化SDS的字符串增长操作:当SDS的API对一个SDS进行修改,并且需要对SDS进行空间扩展的时候,程序不仅会为SDS分配修改所必须要的空间,还会为SDS分配额外的未使用空间。

通过空间预分配策略,Redis可以减少连续执行字符串增长操作所需的内存重分配次数。

  • 惰性空间释放
    惰性空间释放用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。

总结

定义长度是现在的数据类型其都会做的事情,空间预分配在Java也有基础的HashMap扩容。

惰性释放则提供了一种基于频繁分配空间的设计思路。它延迟释放操作,以适应可能出现的扩容需求。

链表 List

虽然在各个网站都翻译是列表,但是从其官网介绍来看,和《设计与实现》一书中一样翻译为链表更符合Redis本意。
Redis 列表是字符串值的链表。Redis 列表常用于:

  • 实现栈和队列
  • 为后台工作系统构建队列管理。

Redis链表是一个双端链表,和我们学习数据结构了解到的链表差不多。

字典 Hash

字典,又称为符号表(symbol table)、关联数组(associativearray)或映射(map),是一种用于保存键值对(key-value pair)的抽象数据结构。
字典经常作为一种数据结构内置在很多高级编程语言里面,但Redis所使用的C语言并没有内置这种数据结构,因此Redis构建了自己的字典实现。

Redis使用Hash Table来作为字典的底层实现。可以使用散列来表示基本对象,以及存储计数器等分组。

  • Redis的Hash冲突
    和其他的Hash作为底层的数据结构一样,Redis的Hash也需要解决Hash冲突。
    Redis的哈希表使用链地址法(separate chaining)来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,这就解决了键冲突的问题。

  • 扩展与收缩-rehash
    Redis的扩展是条件为:
    1)服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1。
    2)服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5。

与Redis String类似的思路,Redis Hash也提供了未使用空间。不同于String是在字符串数组中预留,Redis Hash在哈希表的槽位数组中预留空间(ht[0]、ht[1]),并通过渐进式 rehash 动态调整大小。

执行rehash操作时,流程为确定新大小、分配新表、逐步迁移键(ht[0]迁移到ht[1])、完成替换(释放ht[0],并将ht[1]设置为ht[0],然后为ht[1]分配一个空白哈希表)。是一个渐进的过程。

附书上图例以理解ht[0] ht[1]。

![[Pasted image 20250409160946.png]]

总结

Redis中的字典使用哈希表作为底层实现,每个字典带有两个哈希表,一个平时使用,另一个仅在进行rehash时使用。

Redis Hash较Java的Hash Map,其大小可以做到扩展与收缩,Java Hash只实现了扩展。

Java HashMap没有主动收缩机制,避免了频繁调整带来的性能开销,虽然会导致内存浪费。
Redis设计了预留槽位则适合Redis频繁修改数据场景,可以减少内存分配开销。且Redis Hash采用渐进式的rehash,将调整过程分摊到多次操作中,使单次操作的时间开销更低、更稳定。

这两者设计的差异来源于设计目标和使用场景与运行环境的差异。
Redis Hash追求高并发(实时性)、低延迟和内存效率。其读写频繁,需要避免阻塞,因此使用预留空间、槽位的方式减少频繁扩容的概率。但内存资源宝贵,尽管使用了“空间换时间”的理念,还是要尽可能的节省内存,做动态调整。因此有这样的Hash设计。
而Java HashMap追求单机环境下的简单性和性能。内存回收又GC进行统一管理,无需过于复杂的动态调整。更关注插入和查询的平均性能,而非实时性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值