Redis知识点小结——数据结构篇

本文深入探讨Redis的数据结构,包括动态字符串SDS、双向链表、跳跃表、字典、压缩列表和整数集合。重点介绍了它们的特性、执行效率和在不同场景下的适用性,旨在理解Redis内部机制,提升开发效率。
摘要由CSDN通过智能技术生成

本文非教学文,仅做知识点速览之用

前言

Redis的表层类型与底层类型

为了提高读写性能,在不同的数据类型、数据量的情况下,需要使用不同的存储、管理方式。为了提升服务器吞吐率的同时,又能保证redis的易用性。redis 区分了底层数据结构redis对象的关系。例如:HashMap数据量较少时,底层以ziplist结构存储而非dict;set为纯整数集合时,使用intset而非dict,诸如此类。在这里,分开总结底层实现的数据结构和表层对象,最后再梳理总结他们之间的关系。以期理清redis数据类型的实现原理,这对于在日常开发中,保障涉及redis交互的代码的运行效率至关重要。

学习目标

  1. 了解不同对象的数据结构及其使用场景
  2. 了解不同数据结构之间转换的时机及其效率
  3. 了解不同数据结构执行不同
  4. 了解redis数据回收机制及其可能带来的影响

底层数据结构

动态字符串——SDS

特性

  1. 本质是动态字符数组
  2. 空字符 '\0' 不计入已使用字节数,也不计入未使用字节数
  3. 采用空间预分配+惰性释放策略
  4. 修改后 小于 1 MB 时,预分配空间等于已使用空间;大于 1 MB 时,预分配空间为 1 MB
  5. 惰性释放即不主动释放,但有sds有提供释放内存的api
  6. 二进制安全。不以 '\0' 标记字符串结束,而以 len 标记
  7. 在实现中,SDS除了独立存在,在Redis对象中,也会嵌套至其他类型中,以实现特定Redis对象(如dict用sds存具体元素值,实现hash map)
  1. 内存溢出:使用了未被分配的空间
  2. 内存泄漏:已分配空间不再被使用,但未释放
  3. 申请内存空间是慢操作:存在用户态与内核态的切换和外部资源调度(堆栈的换出换入;内存申请等。内存调度速度远慢于cpu计算速度)
  4. 可能是为了进一步提升redis内存利用率。从redis 3.2 开始,将原本的sdshdr结构体进一步拆分为 sdshdr5; sdshdr8; sdshdr16; sdshdr32; sdshdr64 用于存储不同长度的字符串。其中 sdshdr5 在其源码注释说表明永远不会被使用

执行效率

  1. 字符串长度获取是 O(1) —— 直接读取sds结构体成员变量
  2. 清除字符串(sdsclear) 是 O(1) —— 直接执行 len = 0,并未重置各元素值
  3. 释放内存空间 (sdsfree) 是 O(n) —— n 是已申请的内存空间。注意与 sdsclear 区分
  1. redis/src/sds.h
  2. redis/src/sds.c

双向链表——list&listNode

特性

  1. 多态:listNode 使用void *存储值。不同类型的dup; match; free 方法通过具体的宏 listSet<Xxx>Method 设置
  2. list 结构体包含长度和首尾指针

执行效率

  1. 前驱后继相关操作都是O(1)——获取前驱/后继、插入节点、删除给定节点
  2. 需要查找单个节点的操作都是 O(n)——根据值获取节点、根据index获取节点、删除节点等
  1. redis/src/adlist.h
  2. redis/src/adlist.c

跳跃表——zskipList&zskipListNode

特性

  1. 结构体中直接包含首尾指针、长度和最大层数
  2. 每个节点包含 1 个后退指针
  3. 每个节点基于 power law 生成范围 [1, 32] 个前进指针
  4. 节点成员按照分值大小排序
  5. 每个节点成员对象唯一

执行效率

  1. redis/src/server.h
  2. redis/src/t_zset.c

字典——dick&dictht&dictEntry

特性

  1. 哈希算法采用 murmursh2/3。计算hash值后,&sizemask 决定键值对的位置。
  2. 采用单向链表解决冲突
  3. 负载因子大于1或0.5(触发RDB)时,会扩容并rehash,小于0.1时会收缩空间并rehash
  4. dict用数组存了两个hash table 指针 dictht[0]、dictht[1],用于灰度执行rehash
  5. rehash过程中旧数据 dictht[0] 只减不增,具体迁移过程分到各个CRUD过程中。通过rehashidx计数当前rehash进度
  6. rehash期间,RUD操作会先后访问两张表

执行效率

  1. 删除特定键值对(Delete)操作是 O(1),释放所有键值对是 O(n)
  2. CRUD 操作都是 O(1)
  3. 随机返回键值对也是 O(1)
  1. redis/src/dict.h
  2. redis/src/dict.c
  3. redis 使用的 MurmurHash 函数族及测试套件:aappleby/smhasher

压缩列表——zipList

特性

  1. 是一段连续的存储空间
  2. 存储末尾节点的偏移量
  3. 首部4字节记录整个ziplist占用总字节数
  4. 用0xFF标记zipList结束

对于每个节点

  1. 首部 1 或 5 个字节记录了前驱的字节长度
  2. 前驱小于 254 字节时,当前节点首字节存储前驱长度。否则,首字节为 0xFE 并以后续 4 个字节记录前驱长度

执行效率

  1. 前驱/后继相关操作为O(1)
    —— 基于偏移量计算前驱/后继位置
  2. 获取zipList总占用字节数是O(1)
    —— 于zipList首部4字节存储
  3. 获取zipList总节点数最好情况是O(1),最坏情况是O(n)
    —— 节点数量大于65535
  4. 查找的最好情况的O(n) —— 整数,最坏情况是O(n^2) —— 字节数组
  5. 插入、创建、删除的最好情况是O(n),最坏情况是O(n^2)
    —— 可能触发连锁更新

整数集合——intset

特性

  1. 直接用线性表存储,可能为 int16[]、int32[]、int64[]
  2. 新增整数超出当前类型表示范围时,会触发升级操作。新元素只会在头部(过小)或尾部(过大)
  3. 不存在降级操作(注意与dict的实现区分,dict负载因子小于0.1时会触发收缩并rehash)

执行效率

  1. 添加或删除新元素是O(n)
  2. 整数数组的查找过程可以用折半查找,O(log n)
  3. 获取字节数和元素个数都是O(1),这点注意与zipList区分

表层对象

前文介绍了Redis主要的数据结构,而在具体实现中。对于String、List、Set、ZSet和HashMap实际上是Redis对象。Redis定义了RedisObject结构体,其中包含了一个 void *ptr 用于指向具体的数据结构。在不同的条件下,同一的对象也可能用不同的数据结构存储和管理(即,同类的RedisObject,其ptr成员变量指向了不同的数据结构)。接下来,就针对不同的RedisObject 简要概述。
RedisObject 定义:

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS;
    int refcount;
    void *ptr;
} robj;

String

条件存储方式
<= 32ByteembStr
> 32ByteSDS
整数,可以用long表示int(long)
  1. 浮点数也是用embStr或SDS存储
  2. 对int类型的 INCRBYFLOAT 操作会触发类型转换

所谓的 embStr 就是用一段连续的存储空间同时存储redisObject和SDS结构。与SDS关键差别在于:SDS需要执行两次内存申请,而embStr只需要一次

List

条件存储方式
所有元素长度 < list-max-ziplist-value(Byte)
&&
元素数量 < list-max-ziplist-entries(Byte)
zipList
其他linkedList

Set

条件存储方式
所有元素都可以用long int表示
&&
元素数量 < set-max-intset-entries(Byte)
intset
其他dict

ZSet

条件存储方式
所有元素长度 < zset-max-ziplist-value
&&
元素数量 < zset-max-ziplist-entries(Byte)
zipList
其他skipList + dict

数据量多时,使用两种不同的结构 skipList, dict 存储同一份数据。有利于查找效率保持O(1)的同时,排序相关操作(ZRANGE、ZRANK)保持O(n)

HashMap

条件存储方式
所有元素长度 < zset-max-ziplist-value
&&
元素数量 < zset-max-ziplist-entries(Byte)
zipList
其他dict
  1. 在zipList中,key也是zipList的元素,key所在元素与value所在元素总是紧紧挨在一起
  2. key和value都作为StringObject存储,关于StringObject 的存储特性,参考本篇关于 String 对象的描述

多态

Redis的多态基于类型检查、编码方式检查实现。使得同一命令可以对不同的Redis对象、编码方式执行相应操作

  1. Redis通过类型检查,判断某个命令能否对指定键执行
    比如,DEL能对所有类型执行,而GET只能对String执行
  2. Redis通过检查编码方式encoding,判断对某个Redis对象用什么办法执行特定操作(如HashMap对象,其结构体中包含encoding成员变量,指明当前结构为 zipList 还是 dict

内存回收

Redis的GC与 PHP的GC核心思想基本一致:

  1. 在Redis对象中增加 refcount 成员变量作为引用计数
  2. 新对象的引用计数为 1
  3. 新增饮用+1,不再被使用-1
  4. 为0时立即释放

回收函数参考 redis/src/object.cdecrRefCount 方法

但除此之外,Redis还通过LRU管理Redis对象,可以看到前文所属RedisObject 结构体中定义了 unsigned lru:LRU_BITS; 用于记录最后一次访问时间。当Redis已用内存达到 maxmemory 时。lru小的(越长时间未被使用),将被优先释放。

这提醒我们,为了避免数据丢失,除了保证Redis服务器硬件层面正常运行,还需要采取以下措施:

  1. 保证存储空间不被占满——非高峰时段,保持Redis内存占用在30%以下,是比较合适的
  2. 对于需要持久化的数据,应该及时执行持久化操作,并有相应日志、监控、告警机制,避免未被持久化的数据丢失
  3. 负载均衡——可以用Redis-Cluster或其他中间件处理
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值