redis深入学习笔记(一) --- String

本文深入解析Redis中的字符串类型,包括其基本用法、底层实现原理及扩容策略。介绍Redis字符串的动态特性,如何通过预分配冗余空间提高性能,以及在不同存储形式之间的转换条件。

redis的五种数据类型string、list、hash、set、zset用起来很好用,用法也很简单,但是对底层的具体原理并没有了解过,不懂底层的程序员不是一个好程序员,所以我打算以博客的形式做成笔记来学习redis的底层和进阶知识。

一、用法

字符串string是redis最简单的数据结构。以key/value的形式在redis中存在,redis所有的数据结构都是以唯一的key字符串作为名称,然后通过这个唯一key值来获取相应的value数据。

可以对String类型进行操作的命令:

基础命令

命令行为格式时间复杂度
get获取存储在给定键中的值get  keyO(1)
set设置存储在给定键中的值set key valueO(1)
del删除存储在给定键中的值del keyO(1)
mget批量获取执行key的valuemget key1 key2 key // 返回一个列表O(n), n为指定key个数
mset批量设置字符串键值对mset key1 value1 key2 value2O(n), n为指定key个数
msetnx效果如同执行多个setnx,但是该操作只要有一个key已存在就都不会再执行了,也就是只有在所有给定的键值对不存在时给所有键值赋值,有一个已存在就都不执行。msetnx key1 value1 key2 value2..O(n), n为指定key个数
getset获取old value 并设置 new valuegetset key1 new_valueO(1)
expire给指定的key设置过期时间expire key 5  // 5s 后过期O(1)
setnx如果key值不存在就执行set操作,存在不执行setnx key valueO(n), n为指定key个数
incr如果value是一个整数,可以进行自增操作因为redis是单线程的,所以该操作没有并发隐患,可以放心使用incr key 1  // 自增1O(1)
append将value添加到指定key的字符串结尾append key valueO(n),n为被推入值的长度
strlen获取指定key的value长度strlen key  O(1)

数字操作

命令行为格式时间复杂度
incrby将key所存储的值加上指定增量incrby key increment // 增加incrementO(1)
incr相当于incrby key 1incr key //指定key的值增加1O(1)
decrby将key所存储的值减少指定减量decrby key decrement // 减少decrementO(1)
decr相当于decrby key 1decr key //执行key的值减少1O(1)
incrbyfloat将key所存储的值加上浮点型增量,没有相对应的decrbyfloat命令,但是可以通过该命令的负数实现incrbyfloat key increment  O(1)

字符串拓展操作

命令行为格式时间复杂度
setrange覆盖指定key的指定位置开始的字符串setrange key index value //将指定key的字符串从index位置开始覆盖为valueO(n)
getrange获取指定key指定位置的值getrange key start end // 获取指定key的从start到end位置的字符串O(n)

 

二、底层实现

redis的字符串是动态字符串,是可以修改的字符串,内部结构的实现类似于java中的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配,如上图所示,其中capacity为字符串所占的内存空间,而len是字符串所占用的实际长度,之所以设计为冗余空间是因为append命令,如果没有冗余空间,那么追加操作一定会涉及到分配新数组,然后将旧内容复制过来,再append,这样内存的开销就会特别大。同时扩容时当字符串长度小于1M时,每次扩容都是加倍现有空间,即扩容后空间为现在的2倍,但是如果超过1M,每次扩容时只会最多增加1M,同时需要注意字符串的最大长度为512M。

内部结构:

struct SDS<T>{
    T capacity; // 数组容量
    T len; // 数组长度    
    byte flags; // 特殊标识位
    byte[] content; // 数组内容
}

 如上面代码中的结构体SDS,redis就是用这个数据结构来存储字符串的,细心的程序员一定发现了这个SDS数据结构还用了泛型T,为什么不直接用int呢,这是因为当字符串比较短的时候,len 和 capacity 可以使用byte 和 short来表示,这样更加的节省内存空间,从这就可以看到redis对内存优化做到了极致。

redis的字符串有两种存储方式,在字符串特别短的时候,使用emb形式存储(embeded),当长度超过44时,使用raw形式存储,那问题来了,问什么要用两种形式来存储字符串呢?

需要解释这个现象就需要先了解一下所有redis对象都有的结构头:

struct RedisObject{
    int4 type; // 4 bits
    int4 encoding; // 4 bits
    int24 lru; // 24 bits
    int32 refcount; // 4 bytes
    void *ptr; // 8 bytes, 64-bit system
}

如上代码所示,不同对象具有不同的类型type(4bit),同一个类型的type会有不用的存储形式encoding(4bit),为了记录对象的LRU信息,使用了24个bit来记录LRU信息。每个对象都有一个引用计数器,当引用计数器为零时,对象被销毁,内存被回收。ptr是指向对象内容的指针。从上可以计算出一个RedisObject对象头需要占用内存16字节(一个字节8bit)。

然后看SDS结构体大小,在字符串较小的时候SDS对象头的大小最小是3字节,其中capacity、len、flags各占一个字节。

struct SDS{
    int8 capacity; // 1 byte
    int8 len; // 1 byte
    byte flags; // 1 byte
    byte[] content; // 内联数组,长度capacity
}

如图所示,emb 存储形式它将RedisObject 对象头和 SDS对象连续存储在一起,也就是使用malloc方法一个分配来存储。而raw 却不同,RedisObject 和 SDS分开存储,在内存中并不连续,需要两次malloc方法分配内存。

而内存分配器malloc 内存分配的单位都是2的次方,为了能容纳一个完整的emb 对象,至少要分配32 字节的空间,如果字符串稍微长一点就需要64 字节的空间。 如果总体超出了64字节,redis就会认为它是一个大字符串,就不再使用emb形式存储,改为raw形式存储,因为继续使用emb形式,会分配128字节空间,而大字符串不一定占用这么大空间,会浪费空间。

同时内存分配器分配64空间时,字符串长度最多达到44,因为SDS中的字符串是以 \0 结束的占用一个字节。所以留给content来存储字符串的大小就是 64-19-1 = 44.

三、扩容策略

字符串在长度小于1M之前,采用加倍策略扩容,保留100%冗余空间。

当长度超过1M之后,为了避免加倍后空余空间过大而浪费空间,每次最多分配1M冗余空间。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值