尹小鱼 2020-06-12
涉及到内存分配器jemalloc, 简单动态字符串(SDS),5种值类型对象的内部编码,redisObject,
redis 在编译时便会指定内存分配器, 内存分配器可以是libc、jemalloc、tcmalloc
jemalloc作为Redis的默认内存分配器,在减小内存碎片方面做的相对比较好。jemalloc在64位系统中,将内存空间划分为小、大、巨大三个范围;每个范围内又划分了许多小的内存块单位;当Redis存储数据时,会选择大小最合适的内存块进行存储。
redis对象的类型,内部编码,内存回收,共享对象等功能都需要RedisObject的支持
typedef struct redisObject{ unsigned type: 4; unsigned encoding: 4; unsigned lru: REDIS_LRU_BITS; /*lru time*/ int refcount; void *ptr; } robj;
type 字段 占4bit 目前有5中类型, REDIS_STRING, REDIS_LIST, REDIS_HASH, REDIS_SET, REDIS_ZSET。 当执行type命令时,便是通过读取redisObject对象的type字段获取对象类型
encoding 占4bit (表示对象的内部编码),对于redis支持的每种类型,都有至少两种内部编码。通过object encoding命令,可以查看对象采用的编码方式
lru 不同版本占用内存大小不一样,4.0版本占用24bit,2.6版本占用22bit
refcount 共享对象 记录对象的引用计数,协助内存回收,引用计数可以通过 object refcount命令查看
ptr 指针指向具体的数据 如 set hello world ptr指向包含字符串world的SDS
RedisObject对象大小16字节 4bit+4bit+24bit+4Byte+8Byte=16Byte
结构
struct sdshdr { int len; // 记录buf数组中已使用字节的数量 等于SDS所保存字符串的长度 int free; // 记录buf数组中未使用的字节数量 char buf[]; };
SDS结构 占据的空间:free+len+buf(表示字符串结尾的空字符串), 其中buf=free+len+1. 则总长度为4+4+free+len+1=free+len+9
与C字符串的比较
在C字符串的基础上加入了free和len字段,优势
buf
数组里的数据, 程序不会对其中的数据做任何限制、过滤、或者假设 —— 数据在写入时是什么样的, 它被读取时就是什么样。总结:
Redis 的字符串表示为 sds ,而不是 C 字符串(以 \0 结尾的 char*)。
对比 C 字符串,sds 有以下特性:
– 可以高效地执行长度计算(strlen);
– 可以高效地执行追加操作(append);
– 二进制安全;
sds 会为追加 操作进行优化:加快追加操作的速度,并降低内存分配的次数,代价是多占用了一些内存,而且这些内存不会被主动释放。
在Redis中的应用:
Redis 的 Hash 类型键使用以下两种数据结构作为底层实现:
实现字典的方法有很多种:
Reids选择了高效且实现简单的哈希表作为字典的底层实现。
/* dict.h/dict * 字典 * * 每个字典使用两个哈希表,用于实现渐进式 rehash */ typedef struct dict { dictType *type; // 特定于类型的处理函数 void *privdata; // 类型处理函数的私有数据 dictht ht[2]; // 2个哈希表 int rehashidx; // 记录rehash 进度的标志, 值为-1 表示rehash未进行 int iterators; // 当前正在运作的安全迭代器数量 } dict;
注: dict类型使用了两个指针分别指向两个哈希表
其中,0号哈希表(ht[0])是字典主要使用的哈希表,而 1号哈希表(ht[1])则只有对0号哈希表进行rehash时才使用。
/*哈希表*/ typedef struct dictht { dictEntry **table; // 哈希表节点指针数组(俗称桶, bucket) unsigned long size; //指针数组的大小 unsigned long sizemask; //指针数组的长度掩码 unsigned long used; // 哈希表现有的节点数量 }dictht;
/*哈希表节点*/ typedef struct dictEntry { void *key; union { void *val; uint64_t u64; int64_t s64; } v; // 链接后继系节点 struct dictEntry *next; } dictEntry;
next 属性指向另一个dictEntry结构, 多个dictEntry 可以通过next指针串连成链表dictht使用链地址法来处理键碰撞;当多个不同键拥有相同的哈希值时,哈希表用一个链表将这些键连接起来。
在哈希表实现中,当两个不同的键拥有相同的哈希值时,我们称这两个键发生碰撞(collision),而哈希表实现必须想办法对碰撞进行处理。字典哈希表所使用的碰撞解决方法被称之为链地址法:这种方法使用链表将多个哈希值相同的节点串连在一起,从而解决冲突问题。
假设现在有一个带有三个节点的哈希表:
对于一个新的键值对 key4 和 value4 ,如果 key4 的哈希值和 key1 的哈希值相同,那么它们将在哈希表的 0 号索引上发生碰撞。
对于使用链地址法来解决碰撞问题的哈希表 dictht 来说,哈希表的性能依赖于它的大小(size属性)和它所保存的节点的数量(used 属性)之间的比率:比率最好在1:1。
跳跃表是一种随机化数据结果,查找、添加、删除操作都可以在对数期望时间下完成
跳跃表目前在Redis的唯一作用就是作为有序集类型的底层数据结构之一
Redis对跳跃表进行了修改包括:
新创建的字符串默认使用 REDIS_ENCODING_RAW 编码,在将字符串作为键或者值保存进数据库时,程序会尝试将字符串转为 REDIS_ENCODING_INT 编码, 字符串的长度不超过512MB
创建新列表时Redis默认使用REDIS_ENCODING_ZIPLIST编码,当一下任意一个条件满足时,列表会被转换成REDIS_ENCODING_LINKEDLIST编码:
且编码只可能由压缩列表转化为双端链表,一个列表可以存储2^32-1个元素
压缩列表是Redis为了节约内存而开发的,由一系列特殊编码的连续内存块(而不是像双端链表每个节点都是指针) 顺序型数据结构;与双端链表相比,压缩列表可以节省内存空间,但是进行修改或增删操作时,复杂度较高;因此当节点数量较少时,可以使用压缩列表;但是节点数量多时,还是使用双端链表划算。因为 ziplist 节约内存的性质,它被哈希键、列表键和有序集合键作为初始化的底层实现来使
用
typedef struct listNode { struct listNode *prev; //前驱节点 struct listNode *next; // 后继节点 void *value; } listNode; typedef struct list { //表头指针 listNode *head; //表尾指针 listNode *tail; unsigned long len; // 节点长度 void *(*dup) (void *ptr); void (*freee)(void *ptr); int (*match) (void *ptr, void *key); }list;
小结:
作为Reids列表的底层实现之一; 作为通用数据结构,被其他功能模块使用。
当哈希表使用字典编码时,程序将哈希表的键(key)保存为字典的键,将哈希表的值(value)保存为字典的值, 字典的键和值都是字符串对象
压缩列表编码的哈希表
编码转换
默认使用ziplist编码,当满足以下条件时,自动切换为字典编码
第一个添加到集合的元素,决定了创建集合时所使用的编码:
当使用 REDIS_ENCODING_HT 编码时,集合将元素保存到字典的键里面,而字典的值则统一设为 NULL
如果一个集合使用 REDIS_ENCODING_INTSET 编码, 当满足以下条件的时候会转成字典编码
整数集合适用于集合所有元素都是整数且集合元素数量较小的时候,与哈希表相比,整数集合的优势在于集中存储,节省空间;同时,虽然对于元素的操作复杂度也由O(1)变为了O(n),但由于集合数量较少,因此操作的时间并没有明显劣势。
有序集合与集合一样,元素都不能重复;但与集合不同的是,有序集合中的元素是有顺序的。与列表使用索引下标作为排序依据不同,有序集合为每个元素设置一个分数(score)作为排序依据
压缩列表
跳跃表(skiplist)
跳跃表是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。除了跳跃表,实现有序数据结构的另一种典型实现是平衡树;大多数情况下,跳跃表的效率可以和平衡树媲美,且跳跃表实现比平衡树简单很多,因此redis中选用跳跃表代替平衡树。跳跃表支持平均O(logN)、最坏O(N)的复杂点进行节点查找,并支持顺序操作。Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成:前者用于保存跳跃表信息(如头结点、尾节点、长度等),后者用于表示跳跃表节点
typedef struct zset { dict *dict; zskiplist *zsl; } zset;
对于一个 REDIS_ENCODING_ZIPLIST 编码的有序集,只要满足以下任一条件,就将它转换为REDIS_ENCODING_SKIPLIST 编码
利用共享对象,可以减少对象的创建(同时减少了redisObject的创建),节省内存空间。目前redis中的共享对象只包括10000个整数(0-9999);可以通过调整REDIS_SHARED_INTEGERS参数提高共享对象的个数;例如将REDIS_SHARED_INTEGERS调整到20000,则0-19999之间的对象都可以共享。
考虑这样一种场景:论坛网站在redis中存储了每个帖子的浏览数,而这些浏览数绝大多数分布在0-20000之间,这时候通过适当增大REDIS_SHARED_INTEGERS参数,便可以利用共享对象节省内存空间
mem_fragmentation_ratio=used_memory_rss (Redis进程占据操作系统的内存(单位是字节))/ used_memory(Redis分配器分配的内存总量(单位是字节)).
如果内存碎片率过高(jemalloc在1.03左右比较正常),说明内存碎片多,内存浪费严重;这时便可以考虑重启redis服务,在内存中对数据进行重排,减少内存碎片。