深入了解Redis中String底层结构

318 阅读6分钟
  • 存储类型与操作命令
类型(Type key)存储编码(OBJECT encoding key)实例命令
stringint存储8个字节的长整型 (存储范围long,2^63-1)set num 6379、 incr num、 incrby num 100 、decr num、 decrby num 100
stringembstr格式的SDS,存储小于44个字节的字符串set name tom、strlen name、append name ok、set ff 2.2、 incrbyfloat ff 4.4
stringraw格式的SDS,存储大于44个字节的字符串或起始类型为int,通过append命令追加字符串set song haskhsdhehhdshnnxcdshhhqqhohdjahlsdahdqohjkljklj
set song 1
append song a

问题:存储编码什么时候转化?会还原吗?

答:存储编码转换在Redis写入数据时完成,且转换过程不可逆,只能从小内存编码向大内存编码转换(不包括重新set)

问题:为什么采取多种存储编码

答:首先是节省内存空间,另外还有内存回收与内存再分配策略相关

存储原理

  • SDS(Simple Dynamic String 简单动态字符串)

SDS是Redis开发设计的数据结构,其本质就是扩展了C语言用于存储字符串的字符串数组**(char [] buf)**

struct sdshdr{
  uint32_t len;// 记录buf数组内已使用的字节的数量等于SDS保存字符串的长度
  uint32_t alloc;// 记录buf数组内未使用的字符数量
  unsigned char flag;//低3位用于存储,高5位用于预留
  char buf[];// 字节数组,用于保存字符串
}
  • SDS与C字符串的区别
  1. 常数复杂度获取字符串长度
C字符串数组SDS
C字符串不会记录自身的长度信息,所以若要获取字符串长度,必须必须遍历整个字符串,这个操作复杂度为O(n)SDS中的len属性记录了字符串长度,所以通过SDS的len属性立刻知道字符串长度,这个操作复杂度O(1)
  1. 杜绝缓冲区溢出
C字符串数组SDS
因为C字符串不记录自身的长度,所以在使用strcat(char *dest ,char *src)这个函数时可能造成缓冲区溢出,举例:内存紧邻的s1与s2,其中s1保存了'Redis',s2保存了'MongoDB',若s1使用strcat(s1,'cluster');由于执行函数前未对s1分足够的空间导致数据溢出到了s2所在的空间中,导致s2保存的内容被意外地修改了SDS的空间分配策略完全杜绝了发生缓冲区溢出的情况,当SDS API需要对SDS进行修改时,API会先检查SDS的空间是否满足所需要求**(alloc属性)**,若不满足的话API会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作
  1. 减少修改字符串带来的的内存重分配次数
C字符串数组SDS
因为C字符串不记录自身的长度,所以对于一个包含N个字符串的C字符串来说,这个字符串的底层实现总是一个N+1个字符长的数组**(额外1个字符空间用于保存空字符)**,所以每次增长或者缩短字符串长度需要对字符串数组内存重新分配:
1. 增长字符串操作:拼接(append),执行这个操作之前若未分配空间,造成缓冲区溢出
2.截断字符串操作:截断(trim),执行这个操作若未释放不再使用的空间,造成内存溢出
SDS通过未使用空间解除了字符串长度和底层数组长度之间的关联:在SDS中,buf数组的长度不一定就是字符串数量加一,数组里面可以包含未使用的字节,而这些字节的数量由SDS的free属性记录
  1. SDS 实现了空间预分配和惰性空间释放两种优化策略
空间预分配(SDS字符串增长操作)惰性空间释放(SDS字符串缩短操作)
1.若SDS的长度**(即len属性的值)**将小于1MB,那么程序分配和len属性同样大小相同的未使用空间,这个时候SDS len属性与free属性值相同,举例:若对s1进行了修改,其len为13字节,那么程序会分配13字节未使用空间,即 len与free相等,此时s1实际占用空间大小为:13(len)+13(free)+1byte(额外的一个字节保存空字符) = 27字节
2.若对SDS进行了修改其长度大于等于1MB,那么程序会分配1MB未使用空间,举例:若对s1进行了修改,其len为30MB那么程序会分配1MB的未使用空间(free),此时s1实际占用空间大小为:30MB(len)+1MB(free)+1byte(额外的一个字节保存空字符)
当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用,举例:对SDS所存字符串执行sdstrim操作,对于释放的空间仍然保存在SDS内(free),若SDS需要增加内容即不需要内存重新分配,SDS也提供了相应的API,让我们可以在有需要的时,真正地释放SDS的未使用空间,所以不用担心惰性空间释放策略会造成内存浪费
  1. 二进制安全
C字符串数组SDS
C语言判断字符串结束通过空字符串('\0'),若所存字符串包含多个'\0'以至于无法完整读取所存内容SDS通过使用len属性的值而不是空字符('\0')来判断字符串是否结束
  1. 兼容部分C字符串函数
  • C字符串和SDS之间区别总结
C字符串数组SDS
获取字符串长度时间复杂度O(n)获取字符串长度时间复杂度O(1)
API是不安全的,可能会造成缓冲区溢出API是安全的,不会造成缓冲区溢出
修改字符串N次必然执行N次内存分配修改字符串N次最多执行N次内存分配
只能保存文本数据可以保存文本或者二进制数据
可以使用所有<string.h>库中函数可以使用一部分<string.h>库中函数

应用场景

  • 缓存
  • 分布式Session
  • 分布式锁 set NX EX
  • 分布式全局ID incr
  • 计数器 incr
  • 限流 incr
  • 位操作