redis有五种对象类型,分别是字符串、列表、哈希、集合、有序集合,redis的键值对是两个对象,键一般是字符串,值就这五种中选了。
字符串
字符串的编码可以了解下SDS,全称Simple Dynamic String
和C语言中的字面量String相比,SDS优势主要以下几点
- 提供了len属性,O(1)获取长度
- 动态扩展,C语言中的String占多少内存是固定的,进行拼接之类操作如果不提前分配好空间的话就会出错。SDS提供了free属性,表示buf数组中未曾使用的字节数量,这个属性主要是基于空间预分配来的,提前分配好大于当前字符串的字节量给buf,避免了缓冲区溢出。同时,如果字符串变小省下的空间也给free记下来留作后用
字符串是唯一可以嵌套在其他四个对象中的对象类型
redis提前缓存了0-9999这10000个字符串,可以共享使用
列表
列表的底层编码有ziplist和linkedList两种,每个节点表示一个元素
ziplist由压缩列表实现,压缩列表是一系列特殊编码的连续内存块构成的s顺序型存贮结构,一般在数据较小或数据量少的情况下使用;如果数据多的话,底层会切换为LinkeList,即双端链表
ziplist的爆炸问题
ziplist的节点中有一个属性存贮前一个节点的长度,这个属性使得压缩列表可以从表尾向表头节点进行遍历,如果我们拥有指向当前节点的地址的指针,就可以利用这个属性向前回溯。
该属性长度可以是1字节或者5字节(根据前一个节点长度是否<254),连锁更新的问题由此而来
一连串的250-253字节大小的节点,所以这些节点的previous_entry_length大小都是1,若是在表头加一个255字节的,那么后边第一个的previous_entry_length就由1变成5,这第一个变化的节点大于254,后边以此继续下去造成连锁更新。
哈希
哈希编码主要有ziplist和hashtable两种
ziplist在保存的键和值的长度都较小或者保存的键值对数量较少时使用,否则使用hashtable,hashtable的底层使用字典来实现。
如果使用ziplist,键值紧密相连,类比于使用bitmap存贮数字的出现情况。两个紧密相连的位表示一个数字,一个位表示数字本身,另一个位表示数字的出现情况(1次/大于1次)。在ziplist中也是两块紧密相连的位置,一块是key,一块是value。
字典中有hash表,其实就跟hashmap差不多。字典的结构中包括两个hash表,h[0]和h[1],每次只是用h[0]就够了,若是需要扩容,就一部分一部分的迁移到h[1],在此过程中,若是客户端线程来删改查数据,先从h[0]找,没有的话去h[1],若是添加数据,直接添加到h[1],保证H[0]数据不增加。最后迁移完成释放h[0],将h[1]设置为h[0],再在h[1]新建一个空白哈希表
字典中使用MurmurHash算法计算哈希值
集合
集合的编码有intset****和hashtable
当集合保存的对象都是整数或集合内的元素数量少的话可以使用intset,否则使用hashtable编码
hashtable的编码的集合对象底层使用字典实现,键是保存的对象,值是null
有序集合
有序集合的编码主要是ziplist和skiplist
还是元素小或者元素数量少的时候使用ziplis,数据大的话使用zset
ziplist编码的有序集合实现是压缩列表,两个节点保存一个元素,分别保存元素成员和元素分值,集合元素在压缩列表内按分值大小排序
skiplist编码的有序集合底层使用zset实现,zset包含一个跳跃表和字典。跳跃表中可以保存元素成员和分值,有序性也完全是基于跳表实现的。
跳表的查找的效率平均是O(logN),最坏是O(N)。通过跳表可以实现ZRANK、ZRANGE等命令,那么为什么还需要字典呢?主要是为了O(1)通过元素成员得到分数,如果不使用字典,我们最起码需要先O(logN)找到该元素再查分数。相反,如果不适用跳表只使用字典,字典是无序的,那使用ZRANGE等命令时,需要先排序,复杂度至少是O(NlogN).
跳表和字典中的元素成员和分数共用同一个地址,并没有冗余存贮
redis中的内存回收
主要是使用引用计数,熟悉java gc的话这里应该不陌生,我猜主要是因为引用计数不需要STW