redis原理

redis内存模型

常用数据类型

string 字符串

hash 哈希

list 列表

set 集合

sorted set 有序集合

redis 存储结构

redisobject

redis 内部的存储结构如图示

 了解Redis 的内存模型,对 Redis 的使用有很大帮助:

  • 估算 Redis 内存使用量,选择合理的机器配置
  • 优化内存占用,选择最合适的数据结构和编码
  • 快速定位问题,当 Redis 出现阻塞、内存占用等问题时

redis 数据存储细节

在这里插入图片描述

 

  • dictEntry。Redis 是 Key-Value 数据库,因此对每个键值对都会有一个 dictEntry,里面存储了指向 Key 和 Value 的指针;next 指向下一个 dictEntry,与本 Key-Value 无关。dictEntry是个链表结构。

  • value。无论value是5中类型中的哪一种,都是存储在 RedisObject 中。而 RedisObject 中的 type 字段指明了 value 对象的类型,ptr 字段则指向对象所在的地址。其中字符串对象虽然经过了 RedisObject 的包装,但仍然需要通过 SDS 存储。

RedisObject

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
} robj;
  1. type。表示对象的类型,占 4 个比特;目前包括 REDIS_STRING(字符串)、REDIS_LIST (列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。
  2. encoding。对象的内部编码,占 4 个比特。对于 Redis 支持的每种类型,都有至少两种内部编码,例如对于字符串,有 int、embstr、raw 三种编码。

    Redis 可以根据不同的使用场景来为对象设置不同的编码,大大提高了 Redis 的灵活性和效率。以列表对象为例,有压缩列表和双端链表两种编码方式;如果列表中的元素较少,Redis 倾向于使用压缩列表进行存储,因为压缩列表占用内存更少,而且比双端链表可以更快载入。当列表对象元素较多时,压缩列表就会转化为更适合存储大量元素的双端链表。
  3. lru。记录的是对象最后一次被命令程序访问的时间。通过对比 lru 时间与当前时间,可以计算某个对象的空转时间。

    如果 Redis 打开了 maxmemory 选项,且内存回收算法选择的是 volatile-lru 或 allkeys—lru,那么当 Redis 内存占用超过 maxmemory 指定的值时,Redis 会优先选择空转时间最长的对象进行释放。
  4. refcount。记录的是该对象被引用的次数,类型为整型。refcount 的作用,主要在于对象的引用计数和内存回收。

    当创建新对象时,refcount 初始化为 1;当有新程序使用该对象时,refcount 加 1;当对象不再被一个新程序使用时,refcount 减 1;当 refcount 变为 0 时,对象占用的内存会被释放。Redis 中被多次使用的对象(refcount>1),称为共享对象。这是为了节约内存。目前仅支持整数值的字符串对象。Redis在初始化时,会创建 10000 个字符串对象,值分别是 0~9999 的整数值;当 Redis 需要使用值为 0~9999 的字符串对象时,可以直接使用这些共享对象。
  5. ptr。指向具体的数据。

string

  • redis是使用C语言开发,但C中并没有字符串类型,只能使用指针或字符数组的形式表示一个字符串,所以redis设计了一种简单动态字符串(SDS[Simple Dynamic String])作为底实现:
    定义SDS对象,此对象中包含三个属性:
    1. len buf中已经占有的长度(表示此字符串的实际长度)
    2. free buf中未使用的缓冲区长度
    3. buf[] 实际保存字符串数据的地方
  • 空间分配原则:当len小于IMB(1024*1024)时增加字符串分配空间大小为原来的2倍,当len大于等于1M时每次分配 额外多分配1M的空间。由此可以得出以下特性:
    1. redis为字符分配空间的次数是小于等于字符串的长度N,而原C语言中的分配原则必为N。降低了分配次数提高了追加速度,代价就是多占用一些内存空间,且这些空间不会自动释放。
    2. 高效的计算字符串长度(时间复杂度为O(1))
    3. 高效的追加字符串操作。

list

  • redis对键表的结构支持使得它在键值存储的世界中独树一帜,一个列表结构可以有序地存储多个字符串,拥有例如:lpush lpop rpush rpop等等操作命令。
  • 在3.2版本之前,列表是使用ziplist和linkedlist实现的,在这些老版本中,当列表对象同时满足以下两个条件时,列表对象使用ziplist编码:
    1. 列表对象保存的所有字符串元素的长度都小于64字节
    2. 列表对象保存的元素数量小于512个
  • 当有任一条件 不满足时将会进行一次转码,使用linkedlist。
  • 而在3.2版本之后,重新引入了一个quicklist的数据结构,列表的底层都是由quicklist实现的,它结合了ziplist和linkedlist的优点。
  • ziplist

     

    1. 由表头和N个entry节点和zlend(压缩列表尾部标识符)组成的一个连续的内存块。
    2. 通过一系列的编码规则,提高内存的利用率,主要用于存储整数和比较短的字符串。
    3. 在插入和删除元素的时候,都需要对内存进行一次扩展或缩减,还要进行部分数据的移动操作,这样会造成更新效率低下的情况。
    4. zlbytes:表示压缩列表占总内存的字节数
    5. zltail:表示压缩列表头和尾之间的偏移量,反向遍历ziplist或者pop尾部节点的时候有用
    6. zllen:表示压缩列表中节点的数量
    7. 每个节点由三部分组成:prevlength、encoding、data
    8. prevlengh: 记录上一个节点的长度,为了方便反向遍历ziplist
    9. encoding: 当前节点的编码规则
    10. data: 当前节点的值,可以是数字或字符串 
    11. ziplist是一个经过特殊编码的双向链表,它的设计目标就是为了提高存储效率。ziplist是为节省内存空间而生的。
    12. ziplist是一个为Redis专门提供的底层数据结构之一,本身可以有序也可以无序。当作为listhash的底层实现时,节点之间没有顺序;当作为zset的底层实现时,节点之间会按照大小顺序排列。
  • linkedlist,一个双向链表,和普通的链表定义相同,每个entry包含向前向后的指针,当插入或删除元素的时候,只需要对此元素前后指针操作即可。所以插入和删除效率很高。但查询的效率却是O(n)[n为元素的个数]。
  • quicklist,A doubly linked list of ziplists,意思就是一个由ziplist组成的双向链表。实际上,它整体宏观上就是一个链表结构,只不过每个节点都是以压缩列表ziplist的结构保存着数据,而每个ziplist又可以包含多个entry。也可以说一个quicklist节点保存的是一片数据,而不是一个数据。总结:
    1. 整体上quicklist就是一个双向链表结构,和普通的链表操作一样,插入删除效率很高,但查询的效率却是O(n)。不过,这样的链表访问两端的元素的时间复杂度却是O(1)。所以,对list的操作多数都是poll和push。
    2. 每个quicklist节点就是一个ziplist,具备压缩列表的特性。

set

  • redis的集合和列表都可以存储多个字符串,它们之间的不同在于,列表可以存储多个相同的字符串,而集合则通过使用散列表(hashtable)来保证自已存储的每个字符串都是各不相同的(这些散列表只有键,但没有与键相关联的值)
  • 还可能存在另一种集合,那就是intset,它是用于存储整数的有序集合,里面存放同一类型的整数。
  • 共有三种整数:int16_t、int32_t、int64_t。查找的时间复杂度为O(logN),但是插入的时候,有可能会涉及到升级(比如:原来是int16_t的集合,当插入int32_t的整数的时候就会为每个元素升级为int32_t)这时候会对内存重新分配,所以此时的时间复杂度就是O(N)级别的了。注意:intset只支持升级不支持降级操作。
  • intset在redis.conf中也有一个配置参数set-max-intset-entries默认值为512。表示如果entry的个数小于此值,则可以编码成REDIS_ENCODING_INTSET类型存储,节约内存。否则采用dict的形式存储。

hash

  • hash底层的数据结构实现有两种:
    1. 一种是ziplist,上面已经提到过。当存储的数据超过配置的阀值时就是转用hashtable的结构。这种转换比较消耗性能,所以应该尽量避免这种转换操作。同时满足以下两个条件时才会使用这种结构:
    2. 当键的个数小于hash-max-ziplist-entries(默认512)
    3. 当所有值都小于hash-max-ziplist-value(默认64)
    4. 另一种就是hashtable。这种结构的时间复杂度为O(1),但是会消耗比较多的内存空间。

zset

  • 有序集合和散列一样,都用于存储键值对:有序集合的键被称为成员(member),每个成员都是各不相同的。有序集合的值则被称为分值(score),分值必须为浮点数。有序集合是redis里面唯一一个既可以根据成员访问元素(这一点和散列一样),又可以根据分值以及分值的排列顺序访问元素的结构。它的存储方式也有两种:
  • 是ziplist结构。与上面的hash中的ziplist类似,member和score顺序存放并按score的顺序排列。
  • 另一种是skiplist与dict的结合。
  • skiplist是一种跳跃表结构,用于有序集合中快速查找,大多数情况下它的效率与平衡树差不多,但比平衡树实现简单。
  • skiplist:
    1. 一般跳跃表的实现,主要包含以下几个部分:
    2. 表头(head):指向头节点
    3. 表尾(tail):指向尾节点
    4. 节点(node):实际保存的元素节点,每个节点可以有多层,层数是在创建此节点的时候随机生成的一个数值,而且每一层都是一个指向后面某个节点的指针。
    5. 层(level):目前表内节点的最大层数。
    6. 长度(length):节点的数量。
    7. 跳跃表的遍历总是从高层开始,然后随着元素值范围的缩小,慢慢降低到低层。
    8. https://i-blog.csdnimg.cn/blog_migrate/8ff909ccc0b75a6d758fb91f1936276e.png
    9. 前面也说了,有序列表是使用skiplist和dict结合实现的,skiplist用来保障有序性和访问查找性能,dict就用来存储元素信息,并且dict的访问时间复杂度为O(1)。

     

redis的持久化

rdb持久化

  • RDB持久化即通过创建快照(压缩的二进制文件)的方式进行持久化,保存某个时间点的全量数据。RDB持久化是Redis默认的持久化方式。RDB持久化的触发包括手动触发与自动触发两种方式。
  • save todo...
  • bgsave todo...
  • cow todo...

AOF持久化

  • AOF(Append-Only-File)持久化即记录所有变更数据库状态的指令,以append的形式追加保存到AOF文件中。在服务器下次启动时,就可以通过载入和执行AOF文件中保存的命令,来还原服务器关闭前的数据库状态。
  • 重写aop todo...

redis 内存统计

  • used_memory:Redis 分配器分配的内存总量(单位是字节),包括使用的虚拟内存(即 swap)
  • used_memory_rss:Redis 进程占据操作系统的内存(单位是字节)。包括进程运行本身需要的内存、内存碎片等,但不包括虚拟内存。

  • mem_fragmentation_ratio:内存碎片比率,该值是 used_memory_rss / used_memory 的比值。mem_fragmentation_ratio 一般大于 1,且该值越大,内存碎片比例越大;mem_fragmentation_ratio<1,说明 Redis 使用了虚拟内存,由于虚拟内存的媒介是磁盘,比内存速度要慢很多。当小于1时,应该及时排查,如果内存不足应该及时处理,如增加 Redis 节点、增加 Redis 服务器的内存、优化应用等。一般来说,mem_fragmentation_ratio 在 1.03 左右是比较健康的状态。

  • mem_allocator:Redis 使用的内存分配器,在编译时指定;可以是 libc 、jemalloc 或者 tcmalloc,默认是 jemalloc。

登录到redis中,可以使用info memory命令查看。

redis 内存分布

  1. 数据,作为key-value被存储当数据,这部分占用的内存会统计在 used_memory 中。其中value 包括前文所述的5种类型。Redis 在存储对象时,并不是直接将数据扔进内存,而是会对对象进行各种包装:如 RedisObject、SDS 等。

  2. 进程本身运行需要的内存。这部分内存大约几兆,在生产环境中可忽略。

  3. 缓冲内存。缓冲内存包括客户端缓冲区、复制积压缓冲区、AOF 缓冲区等;其中,客户端缓冲区存储客户端连接的输入输出缓冲;复制积压缓冲区用于部分复制功能;AOF 缓冲区用于在进行 AOF 重写时,保存最近的写入命令。

  4. 内存碎片。内存碎片是 Redis 在分配、回收物理内存过程中产生的。如果对数据的更改频繁,而且数据之间的大小相差很大,可能导致 Redis 释放的空间在物理内存中并没有释放。
    但 Redis 又无法有效利用,这就形成了内存碎片,内存碎片不会统计在 used_memory 中。

redis 为什么那么快

  1. 大部分请求是纯粹的内存操作
  2. 采用单线程,避免了不必要的上下文切换和竞争条件
  3. 非阻塞IO - IO多路复用

Redis的线程模型

todo...

redis文件事件处理器

  • io多路复用器 todo...
  • 消息处理流程 todo...

  • 文件事件的类型 todo...

  • 文件事件处理器 todo...

  • 客户端与服务端连接示例 todo...

redis缓冲区

  • 输入输出缓冲区 todo...
  • cow复制缓冲区 todo...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值