18

蜻蜓点水说说Redis的String的奥秘

 3 years ago
source link: http://www.cnblogs.com/CodeBear/p/13385727.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本篇博客参考: 掘金Redis小册 敖丙

如果面试官问你,单线程的Redis为什么那么快,你可能脱口而出,因为单线程,避免上下文切换;因为基于内存,比硬盘读写快很多;因为采用的是多路复用网络模型。不管你是否真的理解了,这个回答足以应付一半以上的面试官了,但是如果可以再进行补充就更好了:因为Redis对各种数据结构进行了精心的设计,比如String采用的是SDS,比如list采用的是ziplist,quicklist等等,可能这样的回答就比较出彩了,至少可以说出部分面试者不太清楚的事情。今天我们就来看看Redis中最常用的String数据结构的奥秘。

从位操作说起

bitmap的应用场景很多,比如大名鼎鼎的布隆过滤器(之前的博客有介绍过:《大白话布隆过滤器》),比如统计指定用户在一年内任意日期内的登录情况,统计任意日期内,所有用户的登录情况等等,都可以用bitmap来实现(之前的博客也有介绍过:《有点长的博客:Redis不是只有get set那么简单》),所以好好看看bitmap还是很有必要的,不过本篇博客不打算详细介绍bitmap,只是通过bitmap引出我们今天的话题,而bitmap的核心就是位操作。

如果我们要往Redis塞入一个value为“hello”的key,这个所有人都会:

test:1>set key hello
"OK"
test:1>get key
"hello"

如果我们要利用位操作实现这个需求呢?什么,我没听错把,位操作也可以实现这个需求吗?当然可以,因为在Redis中,String就是用byte数组来存储的。

什么,你不信?那请继续看下去。

要用位操作实现这个需求,我们要获得“hello”的ascii码,接着计算出二进制:

比如,“h”的ascii码是104,二进制是1101000:

rQn6va7.png!web

"e"的ascii码是65,二进制是101,二进制是1100101:

MnUFR3Z.png!web

然后形成如下的位图:

aaUfiyB.png!web

下面就需要利用位操作来进行设置:

test:1>setbit s 1 1
"0"
test:1>setbit s 2 1
"0"
test:1>setbit s 4 1
"0"
test:1>setbit s 10 1
"0"
test:1>setbit s 13 1
"0"
test:1>setbit s 9 1
"0"
test:1>setbit s 15 1
"0"

setbit的顺序可以随意调整,只要最终得到的位图是如上形式的就OK了。(我这里就调整了下seitbit的顺序,好吧,我承认其实我是打错了,又懒得再去打一遍,反正最终形成的位图是一样的)。

然后我们get一下:

test:1>get s
"he"

很神奇,有木有,这也说明了在Redis的底层,String就是一个数组,而且还是一个byte[]。

SDS

不管在什么编程语言、存储引擎中,String都是应用最广泛的,而在不同的编程语言、存储引擎中,String可能有不同的实现,在Redis中,String的底层就是SDS,它的全称是Simple Dynamic String。

Redis是C语言开发的,Redis为什么不直接利用C语言的字符串,而要“别出心裁”的自己构建SDS数据结构来实现字符串呢?

我们先来这个SDS是个什么鬼:

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

SDS的定义比较简单,只有3个字段,而且从字面上就可以看出是什么意思:

  • len:存储字符串的实际长度
  • free:存储剩余(空闲)的空间
  • buf[]:存储实际数据

下面我们就来看下SDS和C语言的字符串有什么区别:

  • 求字符串长度
    在C语言中,求字符串的长度只能遍历,时间复杂度是o(n),单线程的Redis表示鸭梨山大,但是现在引入了一个字段来存储字符串的实际长度,时间复杂度瞬间降低成了o(1)。
  • 二进制安全
    在C语言中,读取字符串遵循的是“遇零则止”,即,读取字符串,当读取到“\0”,就认为已经读到了结尾,哪怕后面还有字符串也不会读取了,像图片、音频等二进制数据,经常会穿插“\0”在其中,好端端的图片、音频就毁了...但是现在有了一个字段来存储字符串的实际长度,读取字符串的时候,先看下这个字符串的长度是多少,然后往后读多少位就可以了。
  • 缓冲区溢出
    字符串拼接是开发中常见的操作,C语言的字符串是不记录字符串长度的,一旦我们调用了拼接函数,而没有提前计算好内存,就会产生缓冲区溢出的情况,但是现在引入了free字段,来记录剩余的空间,做拼接操作之前,先去看下还有多少剩余空间,如果够,那就放心的做拼接操作,不够,就进行扩容。
  • 减少内存重分配次数
  1. 空间预分配:当对字符串进行拼接操作的时候,Redis会很贴心的分配一定的剩余空间,这块剩余空间现在看起来是有点浪费,但是我们如果继续拼接,这块剩余空间的作用就出来了。
  2. 惰性空间释放:当我们做了字符串缩减的操作,Redis并不会马上回收空间,因为你可能即将又要做字符串的拼接操作,如果你再次操作,还是没有用到这部分空间,Redis也会去回收这部分空间。

扩容策略

字符串小于1M,采用的是加倍扩容的策略,也就是多分配100%的剩余空间,当大于1M,每次扩容,只会多分配1M的剩余空间。

最大长度

Redis 规定字符串的长度不得超过 512M 字节。

embstr raw

Redis的字符串有两种存储方式,一种是embstr,一种是raw,当长度<=44,采用embstr 来存储:

set codebear abcdefghijklmnopqrstuvwxyz012345678912345678
"OK"
debug object codebear
"Value at:0x7f4050476880 refcount:1 encoding:embstr serializedlength:45 lru:1999016 lru_seconds_idle:36"

当长度>44,改用raw来存储:

set codebear abcdefghijklmnopqrstuvwxyz0123456789123456781
"OK"
debug object codebear
"Value at:0x7f404ac30100 refcount:1 encoding:raw serializedlength:46 lru:1999188 lru_seconds_idle:3"

网上也有一些博客说是以39为分界线,为什么会有两种答案呢?继续看下去就明白了。

我们先来看看Redis的对象头, 查看

#define LRU_BITS 24
typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
} robj;

Redis对象头占 4bit+4bit+24bit+4byte+8byte(指针,在64 bit system下,占8byte)=32bit+12byte=4byte+12byte=16byte。

再来看看这两种存储形式有什么区别:

RviMBr.png!web

embstr的存储形式比较紧凑,Redis的对象头和SDS对象存在一起(连续)。

RRvuYjV.png!web 一般来说,在raw的存储形式下,Redis的对象头和SDS对象不存在一起(不连续)。

我们可以简单的理解为,一块内存的大小为64byte。

好了,前置内容介绍完毕了,我们来看看Redis3.0版本的SDS的定义, 查看

struct sdshdr {
    unsigned int len;
    unsigned int free;
    char buf[];
};

Redis对象头占了16byte,SDS对象的len和free又占了8byte,64-16-8=40,同时保存的字符串会以\0结尾,又占用了1byte,所以实际存储的字符串只能<=39位,所以在低版本的Redis下,embstr、raw的分界线为39。

再来看看Redis5.0版本的SDS的定义, 查看

ZjyMvqr.png!web

可以看到变化很大,为什么要做那么大的改变?更节省内存,当字符串长度比较小的时候,会用

sdshdr8来存储,len和alloc共占用2byte,flags占用1byte,\0结尾占用1byte,一共是4byte,64byte-16byte(对象头)-4byte=44byte,所以在高版本的Redis下,embstr、raw的分界线为44。

怎么样,没想到吧,我们Redis经常使用的String竟然牵扯到那么多东西,而这些东西就可以区分平庸开发和优秀开发,成为一个优秀的开发,要学习的东西还有很多很多。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK