关于redis,通常我们的认知是他很快,抛开io设计模式及线程架构,我们单从底层数据结构了解一下redis的trade-off.
先了解一下redis的sds,sds的编码有3种:int、raw或者embstr,当存入的的内容是Long型,redisObject的ptr指针会直接赋值为数据,这样节省了指针空间开销,当存入的内容是字符串,一种情况字符串小于等于44字节,redisObjec的元数据,指针和sds是同一块内存区域,是连续的,可以避免内存碎片,这种编码方式为embstr,当字符串大于44字节,那么sds将会是一块独立空间,ptr指向这块内存。然后关于sds本身的结构,分为两部分sds指针和sdshdr,sds是一个char类型的指针,指向buf数组首元素,sdshdr储存头部信息,例如len长度,作用是避免O(N)的便利而以O(1)直接获得长度,sdshdr的另一个属性bff数组,储存实际数据,根据不同的字符串长度,有5,8,16,32,64五种类型,作用是节省内存。
可以看出,redis关于字符串的设计,存取时间复杂度交给本身字典的O(1),内存方面不采用c的字符串而是使用sds,首先是避免了c的字符串扩缩容的内存溢出和泄露,sds通过预分配机制,就是原本的len+新插入的数据len2,如果原本空间够用,那么不扩容,如果len+len2 < 1Mb,那么分配
2*(len+len2),如果小于1Mb,那么分配len+len2+1M。其次是可以储存各种类型的数据,例如音视频数据,因为c字符串以\0代表结束,而sds是以len长度为结束,这样便能支持更多场景的数据。
接下来结合场景以及另一个数据结构ziplist,对比一下sds,讲述一下关于我们日常开发中系统设计的trade-off,压缩列表表头有3个字段,zlbytes,zltail和zllen,本身的entry有pre-len,len,encoding和content,通过头部属性,我们对表头表尾的操作皆为O(1),而整体操作的时间复杂度便不尽人意,因为我们要遍历。所以可以看出redis在时间空间的trade-off上面,选择了节省内存。接下来通过一个例子,从而了解ziplist的意义
有个场景,本地有1亿张图片,对应云储存上的图片,我们的需求是需要快速通过本地的图片id找到实际云上资源,那么需要把本地的id和云上的id做映射储存起来,假如我们通过string储存 ,根据sds本身的属性空间开销和redisObjec的开销加上id数据空间开销加上全局hash表的dictEntry:key value next三个指针,加上redis内存分配机制,jemalloc会 根据我们申请的字节数去找到最接近的2的幂次数,比如申请6字节实际会分配8字节,笼统下来,存储一张图片的10位id的映射需要64字节,而实际上呢,我们两个10位id,long型实际只需要16字节。因为如果用string的话,一个键值对就需要一个dictEntry,如果我们采用集合,那么一个集合才消耗一个dictEntry,基于ziplist实现的数据类型有 list,hash和sorted set.
hash底层有2中数据接口,ziplist和hash表,如果我们配置好hash-max-ziplist-entries和hash-max-ziplist-value,这样在我们的条件内,就不会转换成hash表从而使用ziplist储存,需要注意的地方是,pre-len是代表前一个entry的长度,如果前一个entry的长度小于255字节,那么pre-len的取值就是1字节反之5字节,所以我们要使用ziplist的时候要注意数据的增删会引起ziplist的连锁更新,比如本来前一个entry是小于255的 插进来一个大于255的,那么我的pre-len得变成5,假如我本身在253,那么prelen的变化会使我的大小超过255,那么从而影响到我的下一个entry.
关于skiplist,通过建立多级索引从而可以实现O(logN)的时间复杂度,思想类似二分法,不多赘述。
所以从数据结构可以看出,redis提供了在时间和空间各有优势的数据结构,得看我们的使用场景的具体选择从而发挥redis在时间和空间上面的优势,举个例子,redis的list,底层实现是双向链表和压缩列表,操作复杂度0(N),但是呢他的头尾操作效率很高,我们就不要用它来做随机读取,而应当用于FIFO的场景,发挥数据结构本身的优势,再比如sortSet,底层是skiplist,我们则应该尽量避免遍历操作,不可避免的话,建议用scan代替