学习Redis(一)——数据结构和对象

如今redis的使用已经普遍很广泛了,99.9%的互联网公司都在使用redis,就像spring一样普遍。之前虽然用过reedis,但是也仅仅是限制在使用的层面。所以最近有时间在学习redis,
学习资料有两个:
1.redis官方网站
2.《Redis设计与实现》
使用手册:
1.命令大全
2.黄健宏的个人网站

一、 Redis的基本数据结构(底层):

Redis常用的数据结构有5种,strings, hashes, lists, sets, sorted sets。这些数据结构其实是在Redis的基本数据结构上构建而来的。

1.SDS
定义:

simple dynamic string,简单动态字符串。
SDS的结构
SDS保留的C的空字符 ‘\0’ 结尾,这是因为可以直接重用部分C字符串函数库里的函数,如printf.

与C的区别:

1.获取长度:
C遍历获取,O(n);Redis简单加法,O(1)
2.杜绝缓冲区溢出:
字符串的存储都是分配连续的字节数组 buf[],以strcat() (拼接函数)为例,程序的内存中有两个紧邻的字符串s1和s2,
s1和s2
当对s1进行拼接时,若没有对s1分配足够空间时,就会造成缓冲区溢出;Redis会根据free的大小自动进行扩容。
3.内存充分配次数(负载因子)
内存分配涉及复杂算法,可能会执行系统调用,所以比较耗时,而Redis的使用就是为了节约时间。为了避免每次都重新分配空间(用多少分配多少不可取),redis有两种策略空间预分配和惰性空间释放:
空间预分配:
f=1M,当len<f 时,assign space = (modified len)*2。当len>f时,assign = f。
惰性空间释放:
如截串后不会立即释放空间。为了避免内存浪费,有API可释放未使用内存。(疑问?:没有策略实现吗,应该不会只能手动释放。这处可能只是作者没有研究)
4.二进制安全:
C字符串中的字符必须符合某种编码格式,且字符串中间不能含有空字符。这些限制了C的字符串只能保存文本格式数据(使用char[]);char[] + '\0’
而redis不是使用空字符判断结束而是len值并且使用字节数组(byte[])而不是字符数组(char[])所以可以保存任意的二进制文件(图像视频等)。byte[] + len
5.兼容部分C字符串函数
原因是保留了使用’\0’作为字符串的结尾
C和Redis

2.链表
定义:

链表的结构
Redis的双端链表可以保存不同类型的value。

3.字典
定义:

使用哈希表作为底层实现
字典的结构
hash算法:
对键计算出hash值,根据hash值和掩码计算出索引值,最后将值放入到对应的索引上(具体怎么计算打大同小异)。Redis使用的hash算法为:MurmurHash2。
hash冲突:
链地址法
rehash:
随着操作的不断执行, 哈希表保存的键值对会逐渐地增多或者减少, 为了让哈希表的负载因子(load factor = ht[0].used / ht[0].size)维持在一个合理的范围之内, 需要对哈希表的大小进行相应的扩展或者收缩。通过rehash完成。
步骤:
1).为字典的 ht[1] 哈希表分配空间, 这个哈希表的空间大小取决于要执行的操作, 以及 ht[0] 当前包含的键值对数量 (也即是 ht[0].used 属性的值):
如果执行的是扩展操作, 那么 ht[1] 的大小为第一个大于等于 ht[0].used * 2 的 2^n;
如:used = 3; 3*2=6,2^2 < 6 < 2^3, 所以ht[1]的大小就是2^3=8.
如果执行的是收缩操作, 那么 ht[1] 的大小为第一个大于等于 ht[0].used 的 2^n 。
2).将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 上面: rehash 指的是重新计算键的哈希值和索引值, 然后将键值对放置到 ht[1] 哈希表的指定位置上。
3).当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后 (ht[0] 变为空表), 释放 ht[0] , 将 ht[1] 设置为 ht[0] , 并在 ht[1] 新创建一个空白哈希表, 为下一次 rehash 做准备。
渐进式rehash:
当数据量很大时,一次性对所有数据机型rehash,可能会导致服务器在一段时间内停止服务,所以分多次、渐进式的rehash
步骤:
1).为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
2).在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增1。在rehash过程中,rehashidx = ht[1].used-1
3).随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。
若在rehash的过程中有CRUD操作,那么会同时在两个表中进行(一个没有就找另外一个)。

4.跳跃表
定义:

跳跃表的结构
跳跃表是一个有序数据结构,通过在每个节点中维持多个指向其他节点的指针达到快速访问节点的目的
平均O(logN),最坏O(N)复杂度查找节点。Redis中实现有序集合和在集群节点中用作内部数据结构。

5.整数集合
定义:

整数集合的编码方式
集合的底层实现之一

升级:

1).根据新元素的类型, 扩展整数集合底层数组的空间大小, 并为新元素分配空间。
2).将底层数组现有的所有元素都转换成与新元素相同的类型, 并将类型转换后的元素放置到正确的位上, 而且在放置元素的过程中, 需要继续维持底层数组的有序性质不变。
3).将新元素添加到底层数组里面。
这个由于新元素引发了升级,所以要不比所有元素都大排至新数组末尾,要不比所有元素都小排至新数组开头。不支持降级
升级的好处:
1.灵活,C是静态类型的语言,为避免犯错,不会将不同类型的值放在同一数组中。而整数集合会自动升级,提高了灵活性
2.尽可能的节约内存,在必须升级的时候进行升级。

6.压缩列表
定义:

是列表和hash的的底层实现之一。是Redis为了节约内存而开发,由一系列特殊编码的连续内存块组成的顺序型数据结构。
压缩内标的结构
重点说一下previous_entry_length:
1).如果前一节点的长度小于 254 字节, 那么 previous_entry_length 属性的长度为 1 字节: 前一节点的长度就保存在这一个字节里面。
2).如果前一节点的长度大于等于 254 字节, 那么 previous_entry_length 属性的长度为 5 字节: 其中属性的第一字节会被设置为 0xFE (十进制值 254), 而之后的四个字节则用于保存前一节点的长度。
连锁更新:
例:在一个压缩列表中, 有多个连续的、长度介于 250 字节到 253 字节之间的节点 e1 至 eN ,当我们在表头添加一个大于254字节的entry时,这个它的下一个节点的p_e_l不能存储下新结点的长度,所以压缩列表就需要进行空间重分配。这样下下一个节点也如此,这就是连锁更新(时间复杂度最坏O(N2))。但是实际中的这种情况并不多见,对性能不会有太大影响。

二、 Redis的数据类型(应用):

redis数据库以键值对的方式保存数据,键为一个字符串对象,而值就是某个对象的一种,redis中的每个对象都由一个redisObject结构表示:

typedef struct redisObject {
    // 类型,主要有String,List,Hash,Set,ZSet等.命令type key
    unsigned type:4;
    /* 编码。
    1.通过属性来设置对象的编码,而不是对特定的对象关联一种固定的编码,这样redis就可以在不同场景
    使用不同的编码,从而优化对象在此场景下的效率。极大的提高的redis的灵活性和效率。
    2.编码有int(long类型的整数),embstr(embstr编码的SDS),raw(SDS),hashtable(字典),
    linkedlist(双端链表),ziplist(压缩列表),intset(整数集合),skiplist(T跳跃表和字典).
   	3.命令object encoding key */
    unsigned encoding:4;
    // 指向底层实现数据结构的指针
    void *ptr;
    // ...
} robj;

1.String

a.编码有三种:

int:可以用long类型表示的整数值
raw:字符串的长度大于39字节
embstr:字符串长度小于等于39字节
embstr的好处:
1.一次分配连续空间,空间中包括redisObject和sdshdr。并且一次释放;而raw需要两次分配(先分配redisObject,然后再分配sdshdr),同样也需要两次释放。
2.embstr的字符串对象保存在一块连续内存中,能更好的利用缓存带来的优势,方便查找?

b.注意:

对于浮点型数据,以字符串存储,需要进行计算时,先转化为浮点型,计算完成后,再转为字符串保存;embstr为只读,对其使用函数时,会将其转化为raw类型,再进行后续操作。

2.List

a.编码有两种:
ziplist:

  • 列表对象保存的所有字符串元素的长度都小于 64 字节;
  • 列表对象保存的元素数量小于 512 个;

linkedList:不满足以上条件就是用linkedList,上限值可在配置文件中修改

3.Hash

a.编码有两种:
ziplist:

  • 哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节;
  • 哈希对象保存的键值对数量小于 512 个;

hashtable:不满足以上条件使用,同样上限值可在配置文件中修改

4.Set

编码有两种:
inset:

  • 集合对象保存的所有元素都是整数值;
  • 集合对象保存的元素数量不超过 512 个;

hashtable:不满足以上条件使用,同样上限值可在配置文件中修改

5.Sorted Set

编码有两种:
ziplist:

  • 有序集合保存的元素数量小于 128 个;
  • 有序集合保存的所有元素成员的长度都小于 64 字节;

zipList实现有序集合

skiplist:不满足以上条件使用,同样上限值可在配置文件中修改skiplist的实现有序集合

内存回收:

C中没有自动回收内存的功能,Redis在对象系统中构建了一个引用计数技术实现内存回收,redisObject对象中有一个refcount属性:

  1. 在创建一个新对象时, 引用计数的值会被初始化为 1 ;
  2. 当对象被一个新程序使用时, 它的引用计数值会被增一;
  3. 当对象不再被一个程序使用时, 它的引用计数值会被减一;
  4. 当对象的引用计数值变为 0 时, 对象所占用的内存会被释放。

整个对象的生命周期可分为:创建,使用,回收。

对象共享:

当有相同的值对象时,redis不会新建一个对象,而是进行对象共享:

  1. 将数据库键的值指针指向一个现有的值对象;
  2. 将被共享的值对象的引用计数增一。

数据库中保存相同的对象越多,就能节约越多的内存。验证对象是否完全相同所耗费的CPU时间随着对象的复杂度而提升,所以redis中只对包含整数值的字符串对象进行共享。

对象空转时长:

  • redisObject对象中的 lru 属性,记录对象最后一次被命令访问的时间,可通过命令 object idletime
    打印。空转时长=sysdate-lru
  • 如果服务器打开了 maxmemory 选项, 并且服务器用于回收内存的算法为 volatile-lru 或者 allkeys-lru ,
    那么当服务器占用的内存数超过了 maxmemory 选项所设置的上限值时, 空转时长较高的那部分键会优先被服务器释放, 从而回收内存。

总结:

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值