Redis数据结构与对象(下)

  1. 对象的类型与编码
    1. Redis使用对象来表示数据库中的键和值,新创建一个键值对时,至少会创建两个对象(键对象,值对象)
    2. Redis中每个对象都由一个RedisObject结构表示,五个属性(type(类型)、encoding(编码)、ptr(指向底层实现数据结构的指针)、refcount(引用计数值)、lru(记录对象最后一次被访问时间))
    3. 类型(type属性):记录了对象的类型

对象的名称

对象type属性的值(类型常量)

TYPE命令的输出

字符串对象

REDIS_STRING

“string”

列表对象

REDIS_LIST

“list”

哈希对象

REDIS_HASH

“hash”

集合对象

REDIS_SET

“set”

有序集合对象

REDIS_ZSET

“zset”

 

      • 当称呼一个数据库键为“字符串键”/“列表键”时,指的是“这个数据库键所对应的值为字符串对象/列表对象”
    • 编码和底层实现
      • 对象的ptr指针指向对象的底层实现数据结构,而这些数据结构由对象的encoding属性决定
      • encoding属性记录了对象所使用的编码,也就是说这个对象底层实现使用了什么数据结构;每种对象至少使用了两种不同的编码
      • 通过encoding属性来设定编码,而不是关联一种固定的编码,极大地提升了Redis的灵活性和效率,因为Redis可以根据不同的使用场景来为一个对象设置不同的编码,从而优化对象在这一场景下的效率

 

 

  1. 字符串对象
    1. 字符串对象的编码可以是int、raw或者embstr
    2. 字符串对象保存各类型值的编码方式:

编码

long类型保存的整数

Int

long类型保存的整数

embstr或者raw

字符串值,或者长度太大无法用(long/double)表示的(整数/浮点数)

embstr或者raw

 

    1. 如果字符串对象保存的是字符串值,由SDS保存字符串值
      • 长度大于32字节,字符串对象编码设为raw
      • 长度小于等于32字节,字符串编码设为embstr
    2. embstr编码是用来保存短字符串的一种优化编码方式
      • 与raw编码一样,使用redisObject结构和sdshdr结构来表示字符串对象
      • embstr编码将创建字符串对象所需的内存分配次数从raw编码的两次降低为一次
      • 释放embstr编码的字符串对象只需要调用一次内存释放函数,而raw编码需要两次
      • 因为embstr编码的字符串对象的所有数据都保存在一块连续的内存中,能够更好的利用缓存带来的优势
    3. 浮点数在Redis中也是作为字符串保存的,在有需要的时候,会转换回浮点数,执行完成后在转换为字符串值
    4. int编码和embstr编码的字符串对象在一些情况下,会被转换为raw编码的字符串对象
      • int:因某些命令,使得对象保存的不再是整数值,而是个字符串值,那么字符串对象的编码将从int变为raw
      • embstr:因为Redis没有为embstr编码的字符串对象编写相应的修改程序,所以会将embstr转换为raw,然后再执行修改

 

 

    1. 字符串命令:

命令

int编码的实现方法

embstr编码的实现方法

raw编码的实现方法

SET

使用int编码保存值

使用embstr编码保存值

使用raw编码保存值

GET

复制对象所保存的整数值,将这个复制值转换成字符串值,然后向客户端返回这个字符串值

直接向客户端返回字符串值

直接向客户端返回字符串值

APPEND

转换成raw编码,按raw编码的方式执行此操作

转换成raw编码,按raw编码的方式执行此操作

调用sdscatlen函数,将给定字符串追加到现有字符串的末尾

 

  1. 列表对象
    1. 列表对象的编码可以是ziplist或者linkedlist
    2. 两种底层实现:
      • ziplist编码的列表对象使用压缩列表作为底层实现,每个压缩列表节点(entry)都存了一个列表元素
      • linkedlist编码的列表对象使用双向链表作为底层实现,每个链表节点(node)都保存了一个字符串对象,而每个字符串对象都存了一个列表元素
    3. 编码转换:当列表同时满足以下两个条件时,使用ziplist
      • 列表对象中的所有字符串长度都小于64字节
      • 列表对象中的元素数量小于512个

不能同时满足时使用linkedlist编码;即使已经使用ziplist编码后,任意一点不被满足仍然会转换为linkedlist编码

以上两个条件的上限值可以修改(配置文件中的list-max-ziplist-value/list-max-ziplist-entries选项说明)

 

    1. 列表命令的实现

命令

ziplist编码的实现方法

linkedlist编码的实现方法

LPUSH

调用ziplistpush函数,将新元素推入到压缩列表的表头

调用listAddNodeHead函数,将新元素推入到双向链表的表头

RPUSH

调用ziplistpush函数,将新元素推入到压缩列表的表尾

调用listAddNodeTail函数,将新元素推入到双向链表的表尾

LLEN

调用ziplistLen函数返回压缩列表的长度

调用listLength函数返回双向链表的长度

 

  1. 哈希对象
    1. 哈希对象的编码可以是ziplist或者hashtable
    2. 两种底层实现:
      • ziplist编码的哈希对象由压缩列表作为底层实现,每当新加入键值对时,程序会依次将保存了键的压缩列表节点和保存了值的节点先后推入到压缩列表结尾。
        1. 保存了同一键值对的两个节点总是紧挨在一起,键在前,值在后
        2. 先进来的键值对离表头近,后进来的离表尾近
      • hashtable编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对来保存
        1. 字典的每个键/值都是一个字符串对象,对象中保存了键值对的键/值
    3. 编码转换:同时满足以下条件时,哈希对象使用ziplist编码:
      • 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节
      • 哈希对象保存的键值对数量小于512个

不能满足这两个条件的哈希对象使用hashtable编码;即使已经使用ziplist之后,不满足条件也会被转换成hashtable编码

以上两个条件的上限值可以修改(配置文件中的hash-max-ziplist-value/hash-max-ziplist-entries选项说明)

    1. 哈希命令的实现

命令

ziplist编码实现方法

hashtable编码的实现方法

HSET

先后调用ziplistPush函数,依次将键/值推入到压缩列表表尾

调用dictAdd函数,将新节点添加到字典

HGET

首先调用ziplistFind函数,在压缩列表中查找键所对应的节点,然后调用ziplistNext函数,将指针移动到键节点旁的值节点,最后返回值节点

调用dictFind函数,在字典中查找给定键,然后调用dictGetVal函数,返回该键所对应的值

 

 

 

  1. 集合对象
    1. 集合对象的编码可以是intset/hashtable
    2. 底层实现:
      • intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合中
      • hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,字典的值全部设置为null
    3. 编码的转换:当集合对象同时满足以下两个条件时,使用intset编码:
      • 集合对象保存的所有元素都是整数值;
      • 集合对象保存的元素数量不超过512个;
      • 不满足时 使用hashtable编码;即使集合对象使用intset编码,当不满足任一条件时仍然会转换成hashtable
    4. 集合命令的实现

命令

intset编码的实现方式

hashtable编码的实现方式

SADD

调用intsetAdd函数,将所有新元素添加到整数集合

调用dictAdd,以新元素为键,null为值,将键值对添加到字典

SCARD

调用intsetLen函数,返回整数集合所包含的元素数量(集合对象的元素数量)

调用dictSize函数,返回字典的键值对个数(集合对象的元素数量)

 

 

  1. 有序集合对象
    1. 有序集合的编码可以是ziplist/skiplist
    2. 底层实现:
      • ziplist编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,而第二个节点则保存元素的分值;按分值从小到大排序,小的在前(靠近表头),大的在后(靠近表尾)
      • skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳表:
        1. zset结构中的zsl跳表按分值从小到大保存了所有集合元素,每个跳表节点都存了一个集合元素:跳表节点的object属性保存了元素的成员,score属性保存了元素的分值。通过当前跳表,程序可以对有序集合进行范围型操作,比如ZRANK、ZRANGE命令就是基于跳表API实现的
        2. zset结构中的dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素:键存了元素的成员,值存了元素的分值。通过当前字典,程序可以用O(1)复杂度查找给定成员的分值
      • 有序集合每个元素的成员都是一个字符串对象,分值都是一个double浮点数。虽然zset同时使用字典和跳表来保存元素,但是他们之间保存元素不会产生重复成员或者分值,他们会通过指针来共享相同元素的成员和分值,不会因此浪费额外内存
    3. 重点:为什么有序集合需要同时使用跳表和字典来实现?
      • 理论上,可以单独使用其中一种数据结构来实现,但性能上比同时使用会有所降低
      • 比如只用字典,但字典以无序方式保存元素。每次执行范围性操作(ZRANK、ZRANGE),都需要先排序(需要O(nlogn)时间复杂度以及O(n)的空间复杂度(因为要创建一个数组来保存排序后的元素))
      • 再比如只用跳表,没了字典,根据成员查找分值的复杂度会由O(1)上升为O(logn)
    4. 个人理解重点:使用字典是为了弥补跳表查找分值的O(logN)的效率;跳表是为了弥补字典的无序、创建数组排序的额外O(N)内存空间;同时使用是为了让他们之间取长补短,形成更健壮的结构
    5. 编码的转换:同时满足两种条件时,使用ziplist编码
      • 有序集合的所有元素成员的长度都小于64字节
      • 有序集合的元素数量小于128个

不能同时满足时使用skiplist;即使已经使用ziplist,只要任一不满足都会转换为skiplist

    1. 有序集合命令的实现:因为有序集合对象的值为哈希对象,所以命令都是由哈希对象来构建的

命令

ziplist编码的实现方式

zset编码的实现方式

ZADD

调用ziplistInsert函数,将成员和分值作为两个节点分别插入到压缩列表

先调用zslInsert函数,将新元素添加到跳表,然后调用dictAdd函数,将新元素关联到字典

ZCARD

调用ziplistLen函数,获得压缩列表包含的节点的数量,将这个数量除以2得出集合元素的数量

访问跳表的length属性,直接返回集合元素的数量

ZCOUNT

遍历压缩列表,统计分值在给定范围内的节点的数量

遍历跳表,统计分值在给定范围内的节点数量

ZRANGE

从表头向表尾遍历压缩列表,返回给定索引范围内的所有元素

从表头向表尾遍历跳表,返回给定索引范围内的所有元素

ZREVRANGE

从表尾向表头遍历压缩列表,返回给定索引范围内的所有元素

从表尾向表头遍历跳表,返回给定索引范围内的所有元素

 

  1. 类型检查与命令多态
    1. 类型检查的实现:在执行一个类型特定的命令前,Redis会先检查输入键的类型是否正确,然后再决定是否执行给定的命令(SET、GET只能给字符串对象使用)
    2. 多态命令的实现:
      • 比如DEL、RENAME等命令是基于类型的多态:一个命令可以同时处理多种不同类型的键
      • 比如LLEN等命令是基于编码的多态:一个命令可以同时用于处理多种不同编码

 

  1. 内存回收
    1. 概述:采用引用计数(reference counting)法实现内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象并内存回收
    2. 对象的引用计数会随着使用状态而不断变化:
      • 对象被创建时,引用计数值初始化为1
      • 当对象被一个新程序使用时,引用计数值+1
      • 当对象不在被一个程序使用时,引用计数值-1
      • 引用计数为0时,所占用的内存会被释放
    3. 对象的整个生命周期:创建对象、操作对象、释放对象三阶段

函数

作用

incrRefCount

将对象的引用计数值+1

decrRefCount

将对象的引用计数值-1,当引用计数值为0时,释放对象

resetRefCount

将对象的引用计数值设为0,但不释放对象,用于重置引用计数值

 

  1. 对象共享
    1. 对象的引用计数属性还带有对象共享的作用
    2. Redis中,让多个对象共享同一个值对象需要执行两步骤:
      • 将数据库对象的值指针指向一个现有的值对象
      • 将被共享的值对象的引用计数+1
    3. 优势:数据库中保存的相同值对象越多,对象共享机制就能节约越多的内存
    4. Redis会在初始化服务器时,创建一万个(0-9999)整数值作为共享对象使用
    5. 为什么Redis不共享包含字符串的对象:
      • 因为一个共享对象保存的值越复杂,验证共享对象和目标对象是否相同所需的复杂度就会更高,消耗的CPU时间也会越多
      • 共享对象为整数值/字符串时,复杂度为O(1)/O(n)
      • 共享对象为复杂对象(哈希/列表等对象),复杂度为O(n2)

 

  1. 对象的空转时长
    1. 用途:
      • RedisObject结构包含的最后一个属性lru,该属性记录了对象最后一次被访问的时间
        1. OBJECT IDLETIME命令可以打印出给定对象的空转时长,这一空转时长就是通过将当前时间减去键的值对象的lru时间得出
      • 如果服务器打开了maxmemory选项,并且服务器用于回收内存的算法为volatile-lru/allkeys-lru,那么当服务器占用的内存数超过了maxmemory选项所设置的上限值时,空转时长较高的那部分对象会优先被服务器释放,从而回收内存
  2. Maxmemory***
    1. volatile-lru(least recently used):最近最少使用算法,从设置了过期时间的键中选择空转时间最长的键值对清除掉;
    2. volatile-lfu(least frequently used):最近最不经常使用算法,从设置了过期时间的键中选择某段时间之内使用频次最小的键值对清除掉;
    3. volatile-ttl:从设置了过期时间的键中选择过期时间最早的键值对清除;
    4. volatile-random:从设置了过期时间的键中,随机选择键进行清除;
    5. allkeys-lru:最近最少使用算法,从所有的键中选择空转时间最长的键值对清除;
    6. allkeys-lfu:最近最不经常使用算法,从所有的键中选择某段时间之内使用频次最少的键值对清除;
    7. allkeys-random:所有的键中,随机选择键进行删除;
    8. noeviction:不做任何的清理工作,在redis的内存超过限制之后,所有的写入操作都会返回错误;但是读操作都能正常的进行;

前缀为volatile-和allkeys-的区别在于二者选择要清除的键时的字典不同,volatile-前缀的策略代表从redisDb中的expire字典中选择键进行清除;allkeys-开头的策略代表从dict字典中选择键进行清除。

 

 

Redis在你们工作中设置的maxmemorey是多少

前提:redis作为缓存使用。
如果机器不是主要作为缓存用,只是想内存的部分作为缓存(例如服务器托管的网站也在这台机器上),那么需要设置maxmemory,不过设置maxmemory后可能会发生写入失败的情况,这就要选择一个好的淘汰策略,例如LRU。下面是一个例子。
`maxmemory 2mb
 maxmemory-policy allkeys-lru `
设置一方面多大内存看需求了。也要考虑是否达到了缓存的效果,例如设置后只能放入两条记录,还不如不做缓存。

 

往 redis 写入的数据怎么没了

依据Redis的内存淘汰机制策略,可能内存已达到峰值,被Redis给清除了。也许是maxmemory选项设置为allkeys-random/volatile-random算法,随机删除了这个数据

你可能感兴趣的:(redis,redis)