redis中的五种设计类型
数据类型有:
String hash list set sorted set
String类型:String类型是redis中最常见的类型,通常对于string类型进行set和get操作
hash类型:value存放的就是这种结构性的对象,可以比较方便的操作其中的某一个字段,
list类型:使用list类型可以做新消息堆料功能,可以很好的完成排队
set类型:set类型中存放的 就是没有重复数值的集合,可以做全局去重
sorted set类型:做排序的集合
数据库中的键值对都是由对象组成的,数据库键是字符串对象,键的值可以是字符串对象,列表对象,哈希对象,集合对象,有序集合对象
详细介绍:
字符串类型redis中没有用自己直接使用C语言传统的字符串表示,而是自己构建了一种名称为简单动态字符串的抽象类型,并将sds用作redis的默认字符串表示
sds是简单动态字符串:用sds来表示字符串的值
set msg “hello world" 在redis数据库中创建了新的键值对,键值对的键是字符串对象sds类型,键值对的值也是字符串对象sds类型
RPUSH fruits ”apple“ ”bana“ “chery” 在数据库中创建了新的键值对,键值对的键是sds,键值对的值是一个列表对象,列表对象中包含了三个字符串对象,sds类型
字符串类型使用sds简单动态字符串进行表示,sds除了用来保存redis中的字符串值之外还用在AOF缓冲区以及客户端状态中的输入缓冲区
redis持久化的方式:
redis持久化中有两种方式一种是AOF:AOF表示所有的命令行记录以redis命令行请求协议的格式保存为aof文件,数据安全,通过append模式写文件,即使中途server宕机,也可以通过redis-check-aof工具解决数据一致性问题,文件大,数据集大,启动效率低
sds.h表示一个sds值,就是一个字符串对象
sds.h中 int len 记录buf数组中已经使用的字节数量,等于sds中字符串的长度
char[] buf 记录着字符串的值,
int free记录着数组中未使用的字节空间的数量
内部小题目,为什么使用sds而不使用c字符串,reids的安全,效率,功能要求
sds结构中free属性中的值表示未使用字节空间的数量
c语言中使用长度为N+1的字符数组表示长度为N的字符串长度,并且字符串最后一个元素总是空字符,c语言这种字符串表示方式不能满足redis对字符串在安全性,效率以及功能方面的要求
1.方便获取字符串的长度,时间复杂度低
c要遍历char数组直到空字符为止,复杂度为O(N),sds直接读取len变量就行,如果是要设置或者更新sds的长度,这个工作sds的API在执行的时候自动完成的,使用sds不用任何修改长度的操作,时间复杂度为O(1),这样保证了获取字符串长度的工作不会称为redis的性能瓶颈,比如一个键值对的键是字符串对象,字符串很长内部使用的是sds实现的,对这个字符串进行反复的STRLEN命令也不会对系统性能造成影响
2.防止出现缓冲区溢出
C字符串不记录自身的长度,执行strcat的时候strcat函数自动假定用户在执行这个函数的时候,已经为一个字符串分配了足够的内存,可以容纳另一个字符串中的全部内容,如果这个假定不成立就会出现数据溢到了其他内存空间中导致其他内存空间中保存的数据被修改,缓冲区溢出。使用sds,sds具有空间分配策略防止出现数据溢出到其他内存空间的情况,缓存溢出,sds中有一个变量是free,使用sds的api对sds进行修改的时候会检查sds的内存空间是否满足修改所需的要求,如果free不满足的话,自动将sds的空间扩展至需要的空间,执行实际的修改操作,因此使用sds不需要手动修改内存看空间的大小,不会出=出现缓存溢出的情况
3.减少修改字符串时候内存重新分配
c字符串底部总是对应一个N+1长度的数组,只要字符串的长度改变了,就会对保存这个字符串的数组进行一次内存重新分配,字符串的长度和数组的长度有关联,如果是append,就会执行一次内存重新分配来扩展底层数组的大小,不然的话数据就会溢出到其他的内存空间中,出现缓存区溢出,如果是减小字符串长度比如trim,就会执行一次内存重新分配来释放不再使用的空间,不然的就会出现,内存泄漏
内存重新分配耗时,reids经常用于速度要求高,数据被频繁修改的场合,如果每次修改都执行一次内存重新分配的话会消耗很多时间,造成性能的影响,sds中使用free变量解除了字符串长度和数组长度的关联。free变量的好处:通过free变量,sds实现了空间预分配和惰性空间释放两种优化策略
空间预分配:用于优化sds字符串增长操作,当对一个字符串增长的时候,sds要进行内存扩展,此时不仅仅是分配必须要的空间,而且会分配额外未使用的空间free,减少字符串连续增长的时候,sds的内存重新分配次数。
惰性空间释放:优化没有使用的空间,当一个字符串缩短的时候,sds不会内存重新分配回收缩短出来的字节,而是使用free进行记录用于将来使用,避免缩短字符字符串时候内存重新分配,也便于将来增长的时候提供内存空间,比如sdstrim(s,“XY”),移除s sds中所有的X,Y,
不用担心惰性空间释放会造成内存浪费,sds中提供方法会释放free记录的内存空间
字符串中的命令:set 键 值
get 键
del 键
链表,高效的节点重排,可以灵活的增删改查,列表键的底层实现就是链表,当列表键包含元素的数量比较多或者列表中包含的元素都是比较长的字符串的时候Redis会使用链表作为列表键的底层实现。列表键,发布订阅,慢查询,监视器也用到了链表
链表list和listNode配合使用,列表节点包含prev和next前置节点和后置节点和值,表头节点的prev和next前置节点后置节点为null,方便查找某一个节点的前置节点和后置节点并且无环,时间复杂度好,链表中包含头节点尾节点指针和len属性可以方便的获取链表的头节点和尾节点,和链表中节点的数量,时间复杂度好,list中包含dup函数复制链表节点所保存的值,free函数释放链表节点所保存的值,match对比链表节点的值与另一个输入值是否相同
字典是reids数据库的底层实现,是hash键的底层实现,用于保存键值对,字典中每一个键都是唯一的,在字典中的根据键查到与之相关的值,通过键更新值或者删除键值对,一个哈希键hash包含的键值对比较多的时候,hash键中键值对中的元素是比较长的字符串的时候使用字典作为底层实现
set msg “hello world” 保存一个键值对,这个键值对保存在字典中
字典使用hash表作为实现,(hash表是字典结构中的一部分)hash表中有多个hash节点,hash节点保存键值对
hash表用dictht结构,链表用list结构,字符串用sdshdr结构
dictht{
dictEntry[] table
unsigned long size
unsigned long sizemask
unsigned long used
}
hash表结构dictht中4个变量,table数组存放键值对,size表示hash表大小table数组长度,used已经存在的节点数量,sizemask计算索引值
table中是hash表节点dictEntry,每一个结构中保存一个键值对
dictEntry{
key
v
}
key存着键,v存着值,next指向下一个键值对,next可以将多个hash值相同的键值对连接在一起,解决hash冲突
字典结构中包含类型特定函数,私有数据,hash表
dict{
type
private
h[2]
}
type属性和private属性为了针对不同类型的键值对,为了多态字典设置,type属性指向一个结构,每个结构中保存了操作特定类型键值对的函数(计算hash,复制键,复制值,对比键,销毁键,销毁值),privatedata保存了传递给操作特定类型键值对的参数
字典存放键值对时候,根据键计算hash值之后利用hash表中的sizemask计算索引,hash表中table数组索引位置有值就是hash冲突,hash表节点中的next形成单向链表,链地址法
hash表中的键值对过多或者过少,通过rehash对hash表进行相应的扩展或者收缩
hash表根据先hash表0中used的值键值对数目和执行的操作是扩展还是缩小为hash表1提供内存,如果是扩展操作内存的大小要大于等于hash表0中键值对的数目*2对应的2^n,然后将hash表0的键值对重新hash,再利用sizemask计算索引存到hash表1中,之后将hash0对应的内存清楚,之后将hash表1编程hash表0,同时为hash表1分配一块空白的内存用于下次rehash
rehash当键值表中键值对过多或者过少的时候
程序会自动对hash表进行扩展,服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且hash表的负载因子大于等于1
服务器正在执行BGSAVE或者BGREWRITEAOF命令,并且hash表的负载因子大于等于5
hash表的负载因子等于hash表中已有节点的数量和hash表的大小的比值,为什么服务器执不执行这个命令会使的进行扩展的负载因子不同
,在执行BGSAVE和BGREWRITEAOF命令的过程中,Redis需要创建当前服务器的子进程,而大多数操作系统都会采用写时复制技术优化子进程的使用效率,所以在子进程存在期间,服务器会提高执行操作所需的负载因子,从而尽可能的避免在子线程存在期间进行hash表的扩展操作,避免不必要的内存写入操作,最大限度的节约内存。
渐进式的rehash,渐进式的rehash并不是一下子就完成的,在数据量少的时候一下子rehash是可以的,但是当数据量很大的时候一次性的rehash将会导致服务器在一段时间内暂停服务,为了避免这种影响,采用渐进式的rehash,每对hash表0进行增删改查的时候就rehash其中的一个键值对,同时字典结构中的rehashidx就会加一,如果是执行set操作的话,键值对直接保存在hash表1中,渐进式的rehash始终操作一张表,hash表0的键值对一直是减少的。
redis中的对象系统有字符串对象,列表对象,hash对象,集合对象,有序集合对象五种不同类型,redis在执行命令的时候根根据对象的类型判断一个对象是否可以执行给定的命令,并且可以针对不同的使用场景为对象设置不同的数据结构,对象系统实现了基于引用计数技术的内存回收机制,当对象不再使用的时候,该对象占用的内存将会自动释放,redis通过引用计数技术实现了对象共享机制,在适当的条件下,可以通过多个数据库键共享一个对象来节约内存,redis对象带有访问时间记录信息,该时间可以用于计算数据库键的空转时长,在服务器启用了maxmemory功能的情况下,空转时间比较长的哪些键将会优先被服务器删除
对象的类与编码
redis用对象表示数据库的键和值,每次在redis数据库中创建一个键值对的时候,我们至少要创建两个对象,一个对象用作键值对的键一个对象用作键值对的值,set命令在数据库中创建了一个新的键值对,其中键值对的键是一个包含字符串值msg的对象,键值对的值是一个包含字符串值helloword的对象
redis中的每个对象都用objectredis结构表示,该结构中与保存数据相关的三个属性是:type属性,encoding ptr
reidsObject{
type 类型
encoding 编码方式
ptr 指向底层实现数据结构的指针
}
type记录了对象的类型,类型为字符串REDIS_STRING 列表REDIS_LIST hashREDIS_HASH 集合REDIS_SET 有序集合REDIS_ZSET类型
redis中的键总是字符串对象,而值可以为字符串对象,列表对象,hash对象,集合对象,有序集合对象,因此当称呼一个数据库的键为字符串键的时候,我们指的是这个数据库键所对应的值为字符串对象,而称一个键为列表键的时候,我们指的是这个数据的键所对应的值为列表对象
对一个数据库的键执行TYPE命令的时候返回的结果为数据库键对应的值对象的类型,而不是键的类型
创建5中不同类型的键值对
set msg sring 字符串
RPUSH numbers “pingguo” "xiiangg"列表
HMSET profiile name tom age 25 career programmer哈希
SADD fruits apple banaa cherry 集合
ZADD price 8.5 apple 5.0 banaa 6.0 cherry有序集合
编码和底层实现
每一个对象由一个redisObject结构表示,其中包含类型,编码,指向底层实现数据结构的指针ptr
redisObject中的ptr指向底层数据结构,这些数据结构由底层的encoding属性决定。encoding属性的值记录了对象使用什么数据结构作为对象的底层实现,long类型的整数,sds简单动态字符串,双端链表,字典,跳跃表,整数集合,压缩列表,以及跳跃表和字典的配合使用,每种对象的类型至少使用了两种不同的编码方式。REDIS_STRING中使用整数值,简单动态字符串,embstr简单动态字符串,REDIS_LIST压缩列表,双端链表,REDIS_HASH压缩列表 字典,REDIS_SET整数集合字典 REDIS_ZSET压缩列表,跳跃表和字典
字符传对象的编码方式int raw embstr,
字符串对象保存的是整数值的时候,字符串对象redisObject结构中type类型表示string encoding表示为int,ptr属性中保存的是整数值。
字符串对象中保存的是字符串并且字符串的长度大于39个字节,type类型为string encoding为raw底层使用sds简单动态字符串ptr中保存着sds结构
如果字符串对象保存的是一个字符串的值,并且这个值小于39那么,encoding为embstr
embstr编码是专门用于保存短字符串的一种优化编码的方式,这种编码和raw一样,redisObject和sds结构,但是raw结构中会调用两次内存分配分别创建redisObject和sdshdr结构,embstr只i进行依次内存分配,分配一块连续的空间,空间中包含redisOject和sdshdr两个结构
embstr和raw相比,内存分配次数不同,只进行一次内存分配,也只进行一次内存释放,embstr编码的字符串对象都保存在一块连续的内存里面,所以这种编码的字符串对象比起raw编码的字符串对象能更好的利用缓存带来好处
最后要说的是:long double类型表示的浮点数在redis中也是作为字符串进行表示的。保存浮点数的值,先转换为字符串的值,之后将字符串值保存到对象中
set pi 3.14
TYPE pi String
OBJECT ENCODING pi embstr
在需要的时程序会将保存在字符串对象里面的字符串值转换浮点数值,执行操作后再将所得的浮点数值转换为字符串值,并继续保存在字符串对象中,如
INCRBYFLOAT pi 2.0 INCRBY增加
5.140000
OBJECT ENCODING pi
embstr
程序会首先取出字符串对象pi中保存的值,“314”然后将值转换为浮点型数值,之后将浮点型值相加,得到的结果之后将浮点型数据转化为字符串
String类型编码int表示字节不长的long类型整数值,编码embstr raw表示字符串值 long double类型表示的浮点数,和长度过大不能用long double类型表示的浮点数
编码之间的转换,int编码的字符串对象和embstr编码的字符串对象在条件满足的情况下会被转换为raw编码的字符串对象
int编码的字符串对象,当我们向对象执行了一些命令之后,使得这个对象存放的不再是整数值而是一个字符串值,那么字符串对象的编码将会从int转变为raw
int编码表示的long类型整数,执行append指令之后number将会成为raw
set number 100
append number “is a good”
embstr编码的字符串对象实际上是只读的,当对embstr编码的字符串对象执行任何修改的命令的时候,程序会将对象的编码从embstr转换为raw,然后再执行修改命令,因此任何embstr编码的字符串对象在执行修改命令之后总会称为raw编码字符串对象
字符串命令
set get
append(将新的字符串添加到末尾) append zz “zhongguozhiiwang”
INCRBYFLOAT(取出字符串值并尝试将字符串的值转换为long double类型的浮点数,对浮点数进行加法计算,然后将得到的浮点数保存起来,如果字符串的值不能被转换为浮点数值,会像客户端返回一个错误)
INCRBYFFLOAT pi 2.0(将pi转换成为浮点类型,之后加上2.0)
INCRBY 对整数值进行加法计算,得出的结果会作为整数保存起来(只有string了类型是int编码的时候进行)
INCRBY pi 2
STRLEN pi
SETRANGE(将字符串特定索引上的值设置为给定的长度),如
SET wangjun wangjun
get wangjun wangjun
SETRANGE wangjun 2 W
get wangjun waWgjun
GETRANGE(取出特定索引上的字符)
GETRANGE wangjun 0 2 waW
字符串String对象encoding对应的三种编码方式:int raw embtr对应的数据结构是:long整数,简单动态字符串,embstr简单动态字符串
列表对象对应的数据结构是压缩列表和双端链表,对应的编码方式为ziplist和linkedlist
ziplist编码方式对应的底层实现是压缩列表,当一个列表键只包含少量列表项(少于512个),并且每个列表项是小整数值,或者是短字符串,(字符串元素的长度小于64个字节)此时同时满足的时候使用压缩列表实现列表键
RPUSH 1st 1 2 3 4 6 7 8
LLEN 1st 7
OBJECT ENCODING 1ST ziplist或者是quiklist
每个键值对的键的对象结构为rediisObject
redisObject{
type redis_list(列表对象)
encoding Redis encoding ziplist(底层编码方式,对应的是压缩列表)
ptr (ptr属性存储对应的值,指向压缩列表结构,zlbytes zltail zllen entry zlend)列表总字节数,列表尾节点到列表起始地址的偏移量,列表中的节点数量链表节点,链表末尾标识
}
linkedlist编码方式对应的底层实现是双端链表,当一个链表键中包含比较多的列表项或者列表中包含的元素都是比较长的字符串的时候,使用双端列表实现列表键。使用双端列表实现的列表结构为
redisObject{
type redis_list
encoding Redis_encoding linkedlist
ptr ptr指向了一个链表结构,链表结构中的listNode节点值可以为字符串对象,相当于列表对象中嵌套了多个字符串对象,字符串对象是五种类型的对象中唯一一种会被其他四种对象嵌套的对象
}
对于使用ziplist编码的列表对象,使用ziplist编码所需的两个条件中任意一个不能满足的时候,就会进行编码转换,从ziplist转换为linkedlist,原本保存在压缩列表中的所有元素将会被转移到双端链表里面,对象的编码方式也会从ziplist转换为linkedlist
列表中的命令实现
RPUSH 将元素压入链表的表尾
LPUSH 将元素推入列表的表头
LPOP删除表头节点
RPOP删除表尾节点
LINDEX 返回节点保存的元素,LLEN返回列表的长度 LINSERT 将新节点插入到指定位置 LREM删除包含给定元素的数目LTRIM 删除不再索引范围的元素 LSET更新指定节点的值
hash对象压缩列表和字典
ziplist编码的hash对象使用压缩列表作为底层实现,每当有新的键值对要加入hash对象的时候程序会将键的hash列表节点推入到压缩列表的表尾然后将保存了值的压缩列表节点推入到压缩列表表尾。
因此,hash对象使用压缩列表作为编码实现使得保存了同一个键值对的两个节点总是挨在一起,保存键的节点放在前面,保存值的节点放在后面,先添加到hash对象的键值对会被放在压缩列表的表头方向,后添加的放在压缩列表的表尾位置
zlbytes zltail zllen
hash对象使用字典作为底层实现,hash对象中的每个键值对都使用字典键值对来保存:字典中的每个键都是一个字符串对象,对象中保存了键值对的键,字典中的每个值都是字符串对象,对象中保存了键值对的值
编码转换:hash对象的保存的键值对的键和值的字符串长度都小于64字节的时候,hash对象保存的键值对的数量小于512个,两个条件同时满足的时候使用hashtable编码
键值对 的键和值的字符串的长度都小于64字节的时候,hash对象保存的键值对的数量小于512个的时候
hash命令的实现
hset编码方式是ziplist的时候首先调用ziplistppush函数将键推到压缩列表的表尾,然后再调用ziplistpush函数将值推入压缩列表的表尾,编码方式是hashtable的时候,将新节点添加到字典里
hget调用ziplistfind函数,找到指定的键之后将指针移动到键指针旁边的值节点,最后返回值节点 ,编码方式是hashtable的时候,使用函数dictfind先找到键,之后之后返回键对应的值
hexists,调用ziplistfind函数查找指定的键对应的节点,如果找到就说明存在没找到的话就不存在 ,hashtable的方式是调用dictfind
hdel 先找到这个键所对应的节点之后将相应的键节点以及键节点旁边的节点都删除,hashtable的编码方式直接使用dictdel函数,将指定的键值对从字典中删除
hlen 查找hash 底层中包含的键的数量,如果是ziplist实现的话就将节点除以2
hgetall 编码方式是ziplist的时候遍历整个列表将键值对全部的返回,编码方式是hashtable用dictgetkey返回字典中的键,用dictgetvalue返回字典中的值
集合对象,集合对象的编码可以是intset或者是hashtable
使用整数集合inset作为底层实现,集合对象保存的所有的值全部都放在整数集合中
sadd numbers 1 2 4
redisObject中的type为集合对象
redisObject{
type redis_set;
encoding redis_encoding_intset ;整数集合作为底层代码的实现
ptr指针指向整数集合结构
}
inset{
encoding:inset_enc_int16 ;元素的保存位
length:3;元素的个数
content:指向一个数组
}
集合对象使用编码方式为字典,encoding:redis_encoding_ht, 字典中的键都是字符串对象,字符串对象中包含了一个集合元素,字典中的值全部设置为null
编码方式的转换:集合对象中保存的所有元素都是整数值,集合对象中保存的元素数量不超过512个,这个时候使用intset作为encoding,整数集合保证不会出现相同的数值