redis的五种数据类型string、list、hash、set、zset用起来很好用,用法也很简单,但是对底层的具体原理并没有了解过,不懂底层的程序员不是一个好程序员,所以我打算以博客的形式做成笔记来学习redis的底层和进阶知识。
一、用法
字符串string是redis最简单的数据结构。以key/value的形式在redis中存在,redis所有的数据结构都是以唯一的key字符串作为名称,然后通过这个唯一key值来获取相应的value数据。

可以对String类型进行操作的命令:
基础命令
| 命令 | 行为 | 格式 | 时间复杂度 |
| get | 获取存储在给定键中的值 | get key | O(1) |
| set | 设置存储在给定键中的值 | set key value | O(1) |
| del | 删除存储在给定键中的值 | del key | O(1) |
| mget | 批量获取执行key的value | mget key1 key2 key // 返回一个列表 | O(n), n为指定key个数 |
| mset | 批量设置字符串键值对 | mset key1 value1 key2 value2 | O(n), n为指定key个数 |
| msetnx | 效果如同执行多个setnx,但是该操作只要有一个key已存在就都不会再执行了,也就是只有在所有给定的键值对不存在时给所有键值赋值,有一个已存在就都不执行。 | msetnx key1 value1 key2 value2.. | O(n), n为指定key个数 |
| getset | 获取old value 并设置 new value | getset key1 new_value | O(1) |
| expire | 给指定的key设置过期时间 | expire key 5 // 5s 后过期 | O(1) |
| setnx | 如果key值不存在就执行set操作,存在不执行 | setnx key value | O(n), n为指定key个数 |
| incr | 如果value是一个整数,可以进行自增操作因为redis是单线程的,所以该操作没有并发隐患,可以放心使用 | incr key 1 // 自增1 | O(1) |
| append | 将value添加到指定key的字符串结尾 | append key value | O(n),n为被推入值的长度 |
| strlen | 获取指定key的value长度 | strlen key | O(1) |
数字操作
| 命令 | 行为 | 格式 | 时间复杂度 |
| incrby | 将key所存储的值加上指定增量 | incrby key increment // 增加increment | O(1) |
| incr | 相当于incrby key 1 | incr key //指定key的值增加1 | O(1) |
| decrby | 将key所存储的值减少指定减量 | decrby key decrement // 减少decrement | O(1) |
| decr | 相当于decrby key 1 | decr key //执行key的值减少1 | O(1) |
| incrbyfloat | 将key所存储的值加上浮点型增量,没有相对应的decrbyfloat命令,但是可以通过该命令的负数实现 | incrbyfloat key increment | O(1) |
字符串拓展操作
| 命令 | 行为 | 格式 | 时间复杂度 |
| setrange | 覆盖指定key的指定位置开始的字符串 | setrange key index value //将指定key的字符串从index位置开始覆盖为value | O(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冗余空间。
本文深入解析Redis中的字符串类型,包括其基本用法、底层实现原理及扩容策略。介绍Redis字符串的动态特性,如何通过预分配冗余空间提高性能,以及在不同存储形式之间的转换条件。
1810

被折叠的 条评论
为什么被折叠?



