【redis数据结构 – strings】
类似arrylist
字符串 string 是 Redis 最简单的数据结构。Redis 所有的数据结构都是以唯一的 key 字符串作为名称,然后通过这个唯一 key 值来获取相应的 value 数据。不同类型的数据结 构的差异就在于 value 的结构不一样。
有人说,如果只使用redis中的字符串类型,且不使用redis的持久化功能,那么,redis就和memcache非常非常的像了。这说明strings类型是一个很基础的数据类型,也是任何存储系统都必备的数据类型。
字符串类型的用法就是这么简单,因为是二进制安全的,所以你完全可以把一个图片文件的内容作为字符串来存储。
类似于arrylist的add
append key value //将给定的value添加在末尾
Redis 的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于 Java 的 ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配,如图中所示,内部为当前字 符串实际分配的空间 capacity 一般要高于实际字符串长度 len。当字符串长度小于 1M 时, 扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。需要注意的是 字符串最大长度为 512M。
查询与设置
set 【key】 【value】//创建,可覆盖,setnx不能覆盖
get key//查询对应的
strlen key //获得值得长度
setnx key value //只有当key不存在时才能被设置成功
mset k1 v1 k2 v2 k3 v3//设置多个值
mget k1 k2//得到多个值
msetnx k1 v1 k2 v2//设置多个值,但是如果有一个k是存在的就都不成功
getrange key <起始位置><结束位置>//得到key的value的对应值
setrange key <起始位置><结束位置>//有覆盖的作用
setex key <过期时间><value>//设置的同时设置过期时间,单位秒
getset key value//设置新值的同时获得旧值
添加、删除、更新
append key value //将给定的value添加在末尾
incr key //将key存储得数字的value值加1
decr key//减一,为空也会执行
//在遇到数值操作时,redis会将字符串类型转换成数值
incrby/decrby key <长度> //将key存储得数字的value值加任意值
string的数据结构
除了记录实际数据,String类型还需要额外的内存空间记录数据长度、空间使用等信息,这些信息也叫作元数据。
string中SDS起到这个作用:
SDS:
struct SDS {
int8 capacity; // 1byte
int8 len; // 1byte
int8 flags; // 1byte
byte[] content; // 内联数组,长度为 capacity
}
另外,对于String类型来说,除了SDS的额外开销,还有一个来自于RedisObject结构体的开销。
RedisObject结构体会随着存储的string的大小而进行改变,接下来实验一下:
> set codehole abcdefghijklmnopqrstuvwxyz012345678912345678
OK
> debug object codehole
Value at:0x7fec2de00370 refcount:1 encoding:embstr serializedlength:45 lru:5958906 lru_seconds_idle:1
> set codehole abcdefghijklmnopqrstuvwxyz0123456789123456789
OK
> debug object codehole
Value at:0x7fec2dd0b750 refcount:1 encoding:raw serializedlength:46 lru:5958911 lru_seconds_idle:1
注意上面 debug object 输出中有个 encoding 字段,一个字符的差别,存储形式就发生 了变化。这是为什么呢? 为了解释这种现象,我们首先来了解一下 Redis 对象头结构体,因为Redis的数据类型有很多,而且,不同数据类型都有些相同的元数据要记录(比如最后一次访问的时间、被引用的次数等),所以,Redis会用一个RedisObject结构体来统一记录这些元数据,同时指向实际数据。
所有的 Redis 对象都有 下面的这个结构头:
struct RedisObject {
int4 type; // 4bits
int4 encoding; // 4bits
int24 lru; // 24bits
//4+4+24=32bits ==4bytes
int32 refcount; // 4bytes
void *ptr; // 8bytes,64-bit system
} robj
//4+4+8=16bytes
一个RedisObject包含了8字节的元数据和一个8字节指针,这个指针再进一步指向具体数据类型的实际数据所在,例如指向String类型的SDS结构所在的内存地址。
每个对象都有个引用计数,当引用计数为零时,对象就会被销毁,内存被回收。ptr 指针将指向对 象内容 (body) 的具体存储位置。这样一个 RedisObject 对象头需要占据 16 字节的存储空间。
为了节省内存空间,Redis还对Long类型整数和SDS的内存布局做了专门的设计。
int 、embstr、raw
接着我们再看 SDS 结构体的大小,在字符串比较小时,SDS 对象头的大小是 capacity+3,至少是 3( 每个SDS的三个信息就占3个字节)。意味着分配一个字符串的最小空间占用为 19 字节 (16为redisobject的大小+每个SDS的三个信息大小)。
另外由于content是由/0 结尾的也会占一个字节
所以每个字符串至少占20字节
struct SDS {
int8 capacity; // 1byte
int8 len; // 1byte
int8 flags; // 1byte
byte[] content; // 内联数组,长度为 capacity
}
一方面,当保存的是Long类型整数时,RedisObject中的指针就直接赋值为整数数据了(int编码),这样就不用额外的指针再指向整数了,节省了指针的空间开销。
另一方面,当保存的是字符串数据,并且字符串小于等于44字节时,RedisObject中的元数据、指针和SDS是一块连续的内存区域,这样就可以避免内存碎片。这种布局方式也被称为embstr编码方式。
当然,当字符串大于44字节时,SDS的数据量就开始变多了,Redis就不再把SDS和RedisObject布局在一起了,而是会给SDS分配独立的空间,并用指针指向SDS结构。这种布局方式被称为raw编码模式。
如图所示,embstr 存储形式是这样一种存储形式,它将 RedisObject 对象头和 SDS 对 象连续存在一起,使用 malloc 方法一次分配。而 raw 存储形式不一样,它需要两次 malloc,两个对象头在内存地址上一般是不连续的。
而内存分配器 jemalloc/tcmalloc 等分配内存大小的单位都是 2、4、8、16、32、64 等 等,为了能容纳一个完整的 embstr 对象,jemalloc 最少会分配 32 字节的空间,如果字符 串再稍微长一点,那就是 64 字节的空间。如果总体超出了 64 字节,Redis 认为它是一个 大字符串,不再使用 emdstr 形式存储,而该用 raw 形式。
为什么是44呢?
由于每个字符串至少占20字节(RedisObject(16)+SDS内部信息(3)+/0的结尾(1)),64-20=44,留给字符内容的大小为44字节。因此超过44,embstr就变化为raw。
扩容策略
字符串在长度小于 1M 之前,扩容空间采用加倍策略,也就是保留 100% 的冗余空 间。当长度超过 1M 之后,为了避免加倍后的冗余空间过大而导致浪费,每次扩容只会多分 配 1M 大小的冗余空间。