>  기사  >  데이터 베이스  >  Redis 메모리 모델(자세한 설명)

Redis 메모리 모델(자세한 설명)

青灯夜游
青灯夜游앞으로
2019-11-23 15:12:572075검색

Redis는 현재 가장 널리 사용되는 인메모리 데이터베이스 중 하나입니다. 메모리에서 데이터를 읽고 쓰면 읽기 및 쓰기 속도가 크게 향상됩니다. Redis는 높은 동시성을 달성하는 데 없어서는 안 될 부분이라고 할 수 있습니다. 웹사이트. [추천학습: Redis 동영상 튜토리얼]

Redis 메모리 모델(자세한 설명)

Redis를 사용하다 보면 5개의 객체를 접하게 됩니다. Redis 유형(문자열, 해시, 목록, 세트, ​​순서가 지정된 세트), 풍부한 유형은 Memcached에 비해 Redis의 주요 장점입니다. Redis의 5가지 객체 유형의 사용법과 특성을 이해한 것을 바탕으로 Redis의 메모리 모델을 더 깊이 이해하는 것은 Redis 사용에 큰 도움이 될 것입니다. 예:

1. 메모리 사용량. 아직까지 메모리 사용 비용은 여전히 ​​상대적으로 높으며, 필요에 따라 Redis의 메모리 사용량을 합리적으로 평가하고 적절한 머신 구성을 선택하면 필요를 충족하면서 비용을 절약할 수 있습니다.

2. 메모리 사용량을 최적화합니다. Redis 메모리 모델을 이해하면 보다 적절한 데이터 유형과 인코딩을 선택하고 Redis 메모리를 더 잘 활용할 수 있습니다.

3. 문제를 분석하고 해결하세요. Redis에서 Blocking, Memory 사용 등의 문제가 발생하면, 문제의 원인을 최대한 빨리 발견하여 분석 및 해결이 용이해야 합니다.

이 글에서는 Redis가 차지하는 메모리와 쿼리 방법, 다양한 객체 유형이 메모리에 인코딩되는 방식, 메모리 할당자 등 Redis의 메모리 모델(3.0을 예로 들어)을 주로 소개합니다. (jemalloc ), SDS(Simple Dynamic String), RedisObject 등을 기반으로 여러 Redis 메모리 모델의 응용 프로그램을 소개합니다.

1. Redis 메모리 통계

작업을 잘 수행하려면 먼저 도구를 연마해야 합니다. Redis 메모리를 설명하기 전에 먼저 메모리 사용량을 계산하는 방법을 설명하세요. Redis의.

클라이언트가 redis-cli를 통해 서버에 연결한 후(나중에 특별한 지시가 없으면 클라이언트는 항상 redis-cli를 사용합니다) info 명령을 통해 메모리 사용량을 확인할 수 있습니다. # 🎜🎜#

info memory
#🎜 🎜#

그 중 info 명령은 기본 서버 정보, CPU, 메모리, 지속성, 클라이언트 연결 등 Redis 서버에 대한 많은 정보를 표시할 수 있습니다. information 등, memory는 메모리 관련 정보만 표시함을 나타내는 매개변수입니다.

반환된 결과에서 더 중요한 지침 중 일부는 다음과 같습니다.

(1)

used_memory

: #🎜🎜 #Redis 할당 사용된 가상 메모리(예: 스왑)를 포함하여 서버에서 할당한 총 메모리 양(단위는 바이트)입니다. Used_memory_human이 더 친근하게 표시됩니다. (2)used_memory_rss

: Redis 프로세스는 상단 및 상단에서 볼 수 있듯이 운영 체제의 메모리(바이트)를 차지합니다. ps 명령 값은 일관됩니다. 할당자가 할당한 메모리 외에도 Used_memory_rss에는 프로세스 자체를 실행하는 데 필요한 메모리, 메모리 조각 등이 포함되지만 가상 메모리는 포함되지 않습니다. 따라서 Used_memory와 Used_memory_rss는 전자가 Redis 관점에서 얻은 양이고, 후자는 운영체제 관점에서 얻은 양입니다. 둘이 다른 이유는 한편으로는 메모리 단편화와 Redis 프로세스를 실행하는 데 필요한 메모리로 인해 전자가 후자보다 작을 수도 있고, 반면에 가상 메모리의 존재로 인해 전자가 더 작아질 수도 있기 때문입니다. 후자보다 크다.

실제 애플리케이션에서는 Redis 데이터의 양이 상대적으로 크기 때문에 이때 실행되는 프로세스가 차지하는 메모리는 Redis의 데이터 양과 메모리 조각화에 비해 훨씬 작습니다. Used_memory_rss ~ Used_memory 는 Redis 메모리 조각화 비율을 측정하는 매개변수가 됩니다. 이 매개변수는 mem_fragmentation_ratio 입니다.

(3)

mem_fragmentation_ratio

: 메모리 조각화 비율, 이 값은 Used_memory_rss/used_memory의 비율입니다. mem_fragmentation_ratio는 일반적으로 1보다 크며, 값이 클수록 메모리 조각화 비율이 높아집니다. mem_fragmentation_ratio

일반적으로 mem_fragmentation_ratio는 1.03 정도의 비교적 건강한 상태입니다(jemalloc의 경우). 위 스크린샷의 mem_fragmentation_ratio 값은 Redis에 데이터가 저장되지 않았기 때문에 매우 큽니다. 실행되는 메모리는 Used_memory_rss를 Used_memory보다 훨씬 크게 만듭니다.

(4)

mem_allocator

Redis에서 사용하는 메모리 할당자는 컴파일 타임에 지정됩니다. libc, jemalloc 또는 tcmalloc이 될 수 있습니다. default jemalloc 입니다. 스크린샷에서는 기본 jemalloc 을 사용하고 있습니다. 2. Redis 메모리 분할

Redis는 인메모리 데이터베이스이고, 메모리에 저장되는 내용은 아시다시피 주로 데이터(키-값 쌍)입니다. 이전 설명에서 데이터 외에도 Redis의 다른 부분도 메모리를 차지합니다.

Redis의 메모리 사용량은 크게 다음과 같은 부분으로 나눌 수 있습니다.

1, data

데이터베이스로서는 데이터가 가장 중요합니다. part; 이 부분이 차지하는 메모리는 Used_memory에 계산됩니다.

Redis는 키-값 쌍을 사용하여 데이터를 저장하며, 값(객체)에는 문자열, 해시, 목록, 집합, 순서 집합의 5가지 유형이 포함됩니다. 이 5가지 유형은 Redis에서 외부 세계에 제공됩니다. 실제로 Redis 내에서 각 유형에는 2개 이상의 내부 인코딩 구현이 있을 수 있습니다. 또한 Redis가 객체를 저장할 때 데이터를 메모리에 직접 던지지 않습니다. redisObject, SDS 등과 같은 다양한 방식으로 패키징됩니다. 이 기사에서는 나중에 Redis의 데이터 저장에 대한 세부 사항에 중점을 둘 것입니다.

2. 프로세스 자체를 실행하는 데 필요한 메모리

Redis 기본 프로세스 자체는 코드, 상수 풀 등을 실행하는 데 반드시 메모리가 필요합니다. 이 메모리 부분은 약 수 메가바이트에 해당합니다. 대부분의 프로덕션 환경에서 Redis 데이터가 차지하는 비율은 무시할 수 있습니다. 이 메모리 부분은 jemalloc에 ​​의해 할당되지 않으므로 Used_memory에 포함되지 않습니다.

추가 설명: 기본 프로세스 외에도 Redis가 생성한 하위 프로세스(예: Redis가 AOF 및 RDB 재작성을 수행할 때 생성된 하위 프로세스)를 실행하면 메모리를 차지합니다. 물론 이 메모리 부분은 Redis 프로세스에 속하지 않으며 Used_memory 및 Used_memory_rss에 계산되지 않습니다.

3. 버퍼 메모리

버퍼 메모리에는 클라이언트 버퍼, 복사 백로그 버퍼, AOF 버퍼 등이 포함됩니다. 클라이언트 버퍼는 클라이언트 연결의 입력 및 출력 버퍼를 저장하며 부분 복사에 사용됩니다. 기능 AOF 버퍼는 AOF 재작성 중 최신 쓰기 명령을 저장하는 데 사용됩니다. 해당 기능을 이해하기 전에 이러한 버퍼의 세부 사항을 알 필요는 없습니다. 메모리의 이 부분은 jemalloc에 ​​의해 할당되므로 Used_memory에 계산됩니다.

4. 메모리 조각화

Redis는 물리적 메모리를 할당하고 재활용하는 과정에서 메모리 조각화를 생성합니다. 예를 들어, 데이터가 자주 변경되고 데이터 크기가 매우 다른 경우 Redis에서 해제한 공간은 물리적 메모리에서 해제되지 않을 수 있지만 Redis는 이를 효과적으로 사용할 수 없어 메모리 조각화가 발생합니다. 메모리 조각화는 Used_memory에서 계산되지 않습니다.

메모리 조각 생성은 데이터에 대해 수행되는 작업, 데이터의 특성 등과 관련이 있으며 사용된 메모리 할당자와도 관련이 있습니다. 메모리 할당자가 합리적으로 설계된 경우 메모리 조각을 최대한 줄일 수 있습니다. 나중에 논의할 jemalloc은 메모리 조각화를 효과적으로 제어합니다.

Redis 서버의 메모리 조각화가 이미 매우 큰 경우 안전하게 다시 시작하여 메모리 조각화를 줄일 수 있습니다. 다시 시작한 후 Redis는 백업 파일에서 데이터를 다시 읽고 메모리에서 다시 정렬한 후 다시 정렬하기 때문입니다. 각 데이터에 대한 데이터를 적절한 메모리 단위로 선택하여 메모리 조각화를 줄이세요.

3. Redis 데이터 저장소의 세부 사항

1. 개요

메모리 할당자(예: jemalloc), 단순 동적 문자열(SDS), 5가지 객체 유형 및 내부 인코딩, redisObject를 포함한 Redis 데이터 저장소의 세부 사항입니다. 구체적인 내용을 설명하기에 앞서, 먼저 이들 개념 간의 관계에 대해 설명하겠습니다.

다음 그림은 set hello world를 실행할 때 관련된 데이터 모델을 보여줍니다.

이미지 출처: https://searchdatabase.techtarget.com.cn/7-20218/

(1) dictEntry: Redis는 키-값 데이터베이스이므로 각 키-값에 대해 dictEntry가 있습니다. 쌍, 키 및 값에 대한 포인터가 여기에 저장됩니다. 다음은 이 키-값과 관련이 없는 다음 dictEntry를 가리킵니다.

(2) Key: 그림의 오른쪽 상단에서 볼 수 있듯이 Key("hello")는 문자열로 직접 저장되지 않고 SDS 구조로 저장됩니다.

(3) redisObject: Value("world")는 문자열로 직접 저장되거나 Key처럼 SDS에 직접 저장되지 않고 redisObject에 저장됩니다. 실제로 5가지 Value 유형 중 어떤 유형이든 redisObject를 통해 저장됩니다. redisObject의 type 필드는 Value 객체의 유형을 나타내고 ptr 필드는 객체의 주소를 가리킵니다. 그러나 문자열 객체가 redisObject로 패키징되더라도 SDS를 통해 저장해야 함을 알 수 있다.

사실, redisObject에는 type 및 ptr 필드 외에도 다이어그램에 표시되지 않은 다른 필드가 있습니다. 예를 들어 객체의 내부 인코딩을 지정하는 데 사용되는 필드는 나중에 자세히 소개됩니다.

(4) jemalloc: DictEntry 객체, redisObject, SDS 객체이든 저장용 메모리를 할당하려면 메모리 할당자(예: jemalloc)가 필요합니다. DictEntry 객체를 예로 들면, 3개의 포인터로 구성되며 64비트 시스템에서 jemalloc은 32바이트 메모리 단위를 할당합니다.

다음에서는 각각 jemalloc, redisObject, SDS, 객체 유형 및 내부 인코딩을 소개합니다.

2. jemalloc

Redis는 컴파일할 때 메모리 할당자를 지정합니다. 메모리 할당자는 libc, jemalloc 또는 tcmalloc일 수 있으며 기본값은 jemalloc입니다.

jemalloc은 Redis의 기본 메모리 할당자로서 메모리 조각화를 줄이는 데 비교적 좋은 역할을 합니다. 64비트 시스템에서 jemalloc은 메모리 공간을 소형, 대형, 대형의 세 가지 범위로 나누고, 각 범위는 여러 개의 작은 메모리 블록 단위로 나누어지며, Redis는 데이터를 저장할 때 가장 적절한 크기의 메모리 블록을 선택합니다. 저장.

jemalloc으로 나눈 메모리 단위는 아래와 같습니다.

 

图片来源:http://blog.csdn.net/zhengpeitao/article/details/76573053

例如,如果需要存储大小为130字节的对象,jemalloc会将其放入160字节的内存单元中。

3、redisObject

前面说到,Redis对象有5种类型;无论是哪种类型,Redis都不会直接存储,而是通过redisObject对象进行存储。

redisObject对象非常重要,Redis对象的类型、内部编码、内存回收、共享对象等功能,都需要redisObject支持,下面将通过redisObject的结构来说明它是如何起作用的。

redisObject的定义如下(不同版本的Redis可能稍稍有所不同):


typedef struct redisObject {
  unsigned type:4;
  unsigned encoding:4;
  unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
  int refcount;
  void *ptr;
} robj;

redisObject的每个字段的含义和作用如下:

(1)type

type字段表示对象的类型,占4个比特;目前包括REDIS_STRING(字符串)、REDIS_LIST (列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。

当我们执行type命令时,便是通过读取RedisObject的type字段获得对象的类型;如下图所示:

(2)encoding

encoding表示对象的内部编码,占4个比特。

对于Redis支持的每种类型,都有至少两种内部编码,例如对于字符串,有int、embstr、raw三种编码。通过encoding属性,Redis可以根据不同的使用场景来为对象设置不同的编码,大大提高了Redis的灵活性和效率。以列表对象为例,有压缩列表和双端链表两种编码方式;如果列表中的元素较少,Redis倾向于使用压缩列表进行存储,因为压缩列表占用内存更少,而且比双端链表可以更快载入;当列表对象元素较多时,压缩列表就会转化为更适合存储大量元素的双端链表。

通过object encoding命令,可以查看对象采用的编码方式,如下图所示:

5种对象类型对应的编码方式以及使用条件,将在后面介绍。

(3)lru

lru记录的是对象最后一次被命令程序访问的时间,占据的比特数不同的版本有所不同(如4.0版本占24比特,2.6版本占22比特)。

通过对比lru时间与当前时间,可以计算某个对象的空转时间;object idletime命令可以显示该空转时间(单位是秒)。object idletime命令的一个特殊之处在于它不改变对象的lru值。

lru值除了通过object idletime命令打印之外,还与Redis的内存回收有关系:如果Redis打开了maxmemory选项,且内存回收算法选择的是volatile-lru或allkeys—lru,那么当Redis内存占用超过maxmemory指定的值时,Redis会优先选择空转时间最长的对象进行释放。

(4)refcount

refcount与共享对象

refcount记录的是该对象被引用的次数,类型为整型。refcount的作用,主要在于对象的引用计数和内存回收。当创建新对象时,refcount初始化为1;当有新程序使用该对象时,refcount加1;当对象不再被一个新程序使用时,refcount减1;当refcount变为0时,对象占用的内存会被释放。

Redis中被多次使用的对象(refcount>1),称为共享对象。Redis为了节省内存,当有一些对象重复出现时,新的程序不会创建新的对象,而是仍然使用原来的对象。这个被重复使用的对象,就是共享对象。目前共享对象仅支持整数值的字符串对象。

共享对象的具体实现

Redis的共享对象目前只支持整数值的字符串对象。之所以如此,实际上是对内存和CPU(时间)的平衡:共享对象虽然会降低内存消耗,但是判断两个对象是否相等却需要消耗额外的时间。对于整数值,判断操作复杂度为O(1);对于普通字符串,判断复杂度为O(n);而对于哈希、列表、集合和有序集合,判断的复杂度为O(n^2)。

虽然共享对象只能是整数值的字符串对象,但是5种类型都可能使用共享对象(如哈希、列表等的元素可以使用)。

就目前的实现来说,Redis服务器在初始化时,会创建10000个字符串对象,值分别是0~9999的整数值;当Redis需要使用值为0~9999的字符串对象时,可以直接使用这些共享对象。10000这个数字可以通过调整参数REDIS_SHARED_INTEGERS(4.0中是OBJ_SHARED_INTEGERS)的值进行改变。

共享对象的引用次数可以通过object refcount命令查看,如下图所示。命令执行的结果页佐证了只有0~9999之间的整数会作为共享对象。

(5)ptr

ptr指针指向具体的数据,如前面的例子中,set hello world,ptr指向包含字符串world的SDS。

(6)总结

综上所述,redisObject的结构与对象类型、编码、内存回收、共享对象都有关系;一个redisObject对象的大小为16字节:

4bit+4bit+24bit+4Byte+8Byte=16Byte。

4、SDS

Redis没有直接使用C字符串(即以空字符’\0’结尾的字符数组)作为默认的字符串表示,而是使用了SDS。SDS是简单动态字符串(Simple Dynamic String)的缩写。

(1)SDS结构

sds的结构如下:


struct sdshdr { 
    int len;
    int free;
    char buf[];
};

其中,buf表示字节数组,用来存储字符串;len表示buf已使用的长度,free表示buf未使用的长度。下面是两个例子。

图片来源:《Redis设计与实现》

通过SDS的结构可以看出,buf数组的长度=free+len+1(其中1表示字符串结尾的空字符);所以,一个SDS结构占据的空间为:free所占长度+len所占长度+ buf数组的长度=4+4+free+len+1=free+len+9。

(2)SDS与C字符串的比较

SDS在C字符串的基础上加入了free和len字段,带来了很多好处:

  • 获取字符串长度:SDS是O(1),C字符串是O(n)
  • 缓冲区溢出:使用C字符串的API时,如果字符串长度增加(如strcat操作)而忘记重新分配内存,很容易造成缓冲区的溢出;而SDS由于记录了长度,相应的API在可能造成缓冲区溢出时会自动重新分配内存,杜绝了缓冲区溢出。
  • 修改字符串时内存的重分配:对于C字符串,如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减小时会造成内存泄露。而对于SDS,由于可以记录len和free,因此解除了字符串长度和空间数组长度之间的关联,可以在此基础上进行优化:空间预分配策略(即分配内存时比实际需要的多)使得字符串长度增大时重新分配内存的概率大大减小;惰性空间释放策略使得字符串长度减小时重新分配内存的概率大大减小。
  • 存取二进制数据:SDS可以,C字符串不可以。因为C字符串以空字符作为字符串结束的标识,而对于一些二进制文件(如图片等),内容可能包括空字符串,因此C字符串无法正确存取;而SDS以字符串长度len来作为字符串结束标识,因此没有这个问题。

此外,由于SDS中的buf仍然使用了C字符串(即以’\0’结尾),因此SDS可以使用C字符串库中的部分函数;但是需要注意的是,只有当SDS用来存储文本数据时才可以这样使用,在存储二进制数据时则不行(’\0’不一定是结尾)。

(3)SDS与C字符串的应用

Redis在存储对象时,一律使用SDS代替C字符串。例如set hello world命令,hello和world都是以SDS的形式存储的。而sadd myset member1 member2 member3命令,不论是键(”myset”),还是集合中的元素(”member1”、 ”member2”和”member3”),都是以SDS的形式存储。除了存储对象,SDS还用于存储各种缓冲区。

只有在字符串不会改变的情况下,如打印日志时,才会使用C字符串。

四、Redis的对象类型与内部编码

前面已经说过,Redis支持5种对象类型,而每种结构都有至少两种编码;这样做的好处在于:一方面接口与实现分离,当需要增加或改变内部编码时,用户使用不受影响,另一方面可以根据不同的应用场景切换内部编码,提高效率。

Redis各种对象类型支持的内部编码如下图所示(图中版本是Redis3.0,Redis后面版本中又增加了内部编码,略过不提;本章所介绍的内部编码都是基于3.0的):

이미지 출처: "Redis Design and Implement"

Redis의 내부 인코딩 변환은 다음 규칙을 따릅니다. 인코딩 변환은 Redis에서 데이터를 쓸 때 완료되며 변환 과정은 되돌릴 수 없습니다. 메모리 인코딩을 대규모 메모리 인코딩으로 소규모로 변환하는 경우에만 수행할 수 있습니다.

1. 문자열

(1) 개요

문자열은 모든 키가 문자열 유형이고 문자열 이외의 다른 여러 복합 유형의 요소도 문자열이기 때문에 가장 기본적인 유형입니다.

문자열 길이는 512MB를 초과할 수 없습니다.

(2) 내부 인코딩

문자열 유형에는 3가지 내부 인코딩이 있습니다. 해당 적용 시나리오는 다음과 같습니다.

  • int: 8바이트 긴 정수. 문자열 값이 정수인 경우 값은 긴 정수로 표시됩니다.
  • embstr:
  • raw: 39바이트보다 큰 문자열

아래 그림에 예시가 나와 있습니다.

구별해야 할 embstr과 raw의 길이는 39입니다. 이는 redisObject의 길이가 16바이트이고 length of sds 9+string 길이이므로 string 길이가 39일 때 embstr 길이는 정확히 16+9+39=64가 되며 jemalloc은 64byte의 메모리 단위를 할당할 수 있습니다.

(3) 인코딩 변환

int 데이터가 더 이상 정수가 아니거나 크기가 long 범위를 초과하면 자동으로 raw로 변환됩니다.

emstr의 경우 구현이 읽기 전용이므로 embstr 객체가 수정되면 먼저 raw로 변환된 다음 수정됩니다. 따라서 embstr 객체가 수정되는 한 수정된 객체는 raw여야 합니다. 39바이트에 도달하는지 여부에 관계없이. 아래 그림에 예가 나와 있습니다.

2. 목록

(1) 개요

목록(목록)은 여러 개의 정렬된 문자열을 저장하는 데 사용되며, 각 문자열은 2^를 저장할 수 있는 요소라고 합니다. 32-1 요소. Redis의 목록은 양쪽 끝에서 삽입 및 팝핑을 지원하며 지정된 위치(또는 범위)의 요소를 얻을 수 있으며 배열, 큐, 스택 등의 기능을 할 수 있습니다.

(2) 내부 인코딩

목록의 내부 인코딩은 압축 목록(ziplist) 또는 이중 종료 연결 목록(linkedlist)일 수 있습니다.

이중 연결 목록: 목록 구조와 여러 listNode 구조로 구성됩니다. 일반적인 구조는 아래 그림과 같습니다.

사진 출처: "Redis 설계 및 구현"

보실 수 있습니다. 그림에서 보면, 헤드 포인터와 테일 포인터가 동시에 저장되고, 각 노드에는 앞과 뒤를 가리키는 포인터가 있으며, 목록의 길이는 연결 리스트에 저장됩니다. 노드 값에 대한 자유 및 매치 세트 유형별 기능을 제공하므로 연결된 목록을 사용할 수 있습니다. 다양한 유형의 값을 저장하는 데 사용됩니다. 연결된 목록의 각 노드는 유형이 문자열인 redisObject를 가리킵니다.

압축된 목록: 압축된 목록은 메모리를 절약하기 위해 Redis에서 개발되었습니다. 이는 특별히 인코딩된 일련의 연속 메모리 블록으로 구성된 순차적 데이터 구조입니다(각 노드가 양방향 연결 목록과 같은 포인터가 아님). ).;구체적인 구조는 비교적 복잡하므로 생략하겠습니다. 이중 연결 목록과 비교하면 압축 목록은 메모리 공간을 절약할 수 있지만 작업을 수정하거나 추가하거나 삭제할 때 복잡도가 더 높으므로 노드 수가 적을 때는 압축 목록을 사용할 수 있습니다. 규모가 크고, 이중 연결 목록이 여전히 사용되고 있습니다.

압축 목록은 목록을 구현하는 데 사용될 뿐만 아니라 해시 및 순서가 지정된 목록을 구현하는 데에도 매우 널리 사용됩니다.

(3) 인코딩 변환

압축 목록은 다음 두 조건이 충족되는 경우에만 사용됩니다. 목록의 요소 수가 512개 미만이고 목록의 모든 문자열 개체가 64바이트 미만입니다. 한 가지 조건이 충족되지 않으면 이중 끝 목록이 사용되며 인코딩은 압축 목록에서 이중 끝 연결 목록으로만 변환될 수 있으며 반대 방향은 불가능합니다.

다음 그림은 목록 인코딩 변환의 특징을 보여줍니다.

其中,单个字符串不能超过64字节,是为了便于统一分配每个节点的长度;这里的64字节是指字符串的长度,不包括SDS结构,因为压缩列表使用连续、定长内存块存储字符串,不需要SDS结构指明长度。后面提到压缩列表,也会强调长度不超过64字节,原理与这里类似。

3、哈希

(1)概况

哈希(作为一种数据结构),不仅是redis对外提供的5种对象类型的一种(与字符串、列表、集合、有序结合并列),也是Redis作为Key-Value数据库所使用的数据结构。为了说明的方便,在本文后面当使用“内层的哈希”时,代表的是redis对外提供的5种对象类型的一种;使用“外层的哈希”代指Redis作为Key-Value数据库所使用的数据结构。

(2)内部编码

内层的哈希使用的内部编码可以是压缩列表(ziplist)和哈希表(hashtable)两种;Redis的外层的哈希则只使用了hashtable。

压缩列表前面已介绍。与哈希表相比,压缩列表用于元素个数少、元素长度小的场景;其优势在于集中存储,节省空间;同时,虽然对于元素的操作复杂度也由O(1)变为了O(n),但由于哈希中元素数量较少,因此操作的时间并没有明显劣势。

 

hashtable:一个hashtable由1个dict结构、2个dictht结构、1个dictEntry指针数组(称为bucket)和多个dictEntry结构组成。

正常情况下(即hashtable没有进行rehash时)各部分关系如下图所示:

 

图片改编自:《Redis设计与实现》

下面从底层向上依次介绍各个部分:

dictEntry

dictEntry结构用于保存键值对,结构定义如下:


typedef struct dictEntry{
    void *key;
    union{
        void *val;
        uint64_tu64;
        int64_ts64;
    }v;
    struct dictEntry *next;
}dictEntry;

其中,各个属性的功能如下:

  • key:键值对中的键;
  • val:键值对中的值,使用union(即共用体)实现,存储的内容既可能是一个指向值的指针,也可能是64位整型,或无符号64位整型;
  • next:指向下一个dictEntry,用于解决哈希冲突问题

在64位系统中,一个dictEntry对象占24字节(key/val/next各占8字节)。

bucket

bucket是一个数组,数组的每个元素都是指向dictEntry结构的指针。redis中bucket数组的大小计算规则如下:大于dictEntry的、最小的2^n;例如,如果有1000个dictEntry,那么bucket大小为1024;如果有1500个dictEntry,则bucket大小为2048。

dictht

dictht结构如下:


typedef struct dictht{
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
}dictht;

其中,各个属性的功能说明如下:

  • table属性是一个指针,指向bucket;
  • size属性记录了哈希表的大小,即bucket的大小;
  • used记录了已使用的dictEntry的数量;
  • sizemask属性的值总是为size-1,这个属性和哈希值一起决定一个键在table中存储的位置。

dict

一般来说,通过使用dictht和dictEntry结构,便可以实现普通哈希表的功能;但是Redis的实现中,在dictht结构的上层,还有一个dict结构。下面说明dict结构的定义及作用。

dict结构如下:

typedef struct dict{
    dictType *type;
    void *privdata;
    dictht ht[2];
    int trehashidx;
} dict;

其中,type属性和privdata属性是为了适应不同类型的键值对,用于创建多态字典。

ht属性和trehashidx属性则用于rehash,即当哈希表需要扩展或收缩时使用。ht是一个包含两个项的数组,每项都指向一个dictht结构,这也是Redis的哈希会有1个dict、2个dictht结构的原因。通常情况下,所有的数据都是存在放dict的ht[0]中,ht[1]只在rehash的时候使用。dict进行rehash操作的时候,将ht[0]中的所有数据rehash到ht[1]中。然后将ht[1]赋值给ht[0],并清空ht[1]。

因此,Redis中的哈希之所以在dictht和dictEntry结构之外还有一个dict结构,一方面是为了适应不同类型的键值对,另一方面是为了rehash。

(3)编码转换

如前所述,Redis中内层的哈希既可能使用哈希表,也可能使用压缩列表。

只有同时满足下面两个条件时,才会使用压缩列表:哈希中元素数量小于512个;哈希中所有键值对的键和值字符串长度都小于64字节。如果有一个条件不满足,则使用哈希表;且编码只可能由压缩列表转化为哈希表,反方向则不可能。

下图展示了Redis内层的哈希编码转换的特点:

4、集合

(1)概况

集合(set)与列表类似,都是用来保存多个字符串,但集合与列表有两点不同:集合中的元素是无序的,因此不能通过索引来操作元素;集合中的元素不能有重复。

一个集合中最多可以存储2^32-1个元素;除了支持常规的增删改查,Redis还支持多个集合取交集、并集、差集。

(2)内部编码

集合的内部编码可以是整数集合(intset)或哈希表(hashtable)。

哈希表前面已经讲过,这里略过不提;需要注意的是,集合在使用哈希表时,值全部被置为null。

整数集合的结构定义如下:

typedef struct intset{
    uint32_t encoding;
    uint32_t length;
    int8_t contents[];
} intset;

其中,encoding代表contents中存储内容的类型,虽然contents(存储集合中的元素)是int8_t类型,但实际上其存储的值是int16_t、int32_t或int64_t,具体的类型便是由encoding决定的;length表示元素个数。

整数集合适用于集合所有元素都是整数且集合元素数量较小的时候,与哈希表相比,整数集合的优势在于集中存储,节省空间;同时,虽然对于元素的操作复杂度也由O(1)变为了O(n),但由于集合数量较少,因此操作的时间并没有明显劣势。

(3)编码转换

只有同时满足下面两个条件时,集合才会使用整数集合:集合中元素数量小于512个;集合中所有元素都是整数值。如果有一个条件不满足,则使用哈希表;且编码只可能由整数集合转化为哈希表,反方向则不可能。

下图展示了集合编码转换的特点:

5、有序集合

(1)概况

有序集合与集合一样,元素都不能重复;但与集合不同的是,有序集合中的元素是有顺序的。与列表使用索引下标作为排序依据不同,有序集合为每个元素设置一个分数(score)作为排序依据。

(2)内部编码

有序集合的内部编码可以是压缩列表(ziplist)或跳跃表(skiplist)。ziplist在列表和哈希中都有使用,前面已经讲过,这里略过不提。

跳跃表是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。除了跳跃表,实现有序数据结构的另一种典型实现是平衡树;大多数情况下,跳跃表的效率可以和平衡树媲美,且跳跃表实现比平衡树简单很多,因此redis中选用跳跃表代替平衡树。跳跃表支持平均O(logN)、最坏O(N)的复杂点进行节点查找,并支持顺序操作。Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成:前者用于保存跳跃表信息(如头结点、尾节点、长度等),后者用于表示跳跃表节点。具体结构相对比较复杂,略。

(3)编码转换

只有同时满足下面两个条件时,才会使用压缩列表:有序集合中元素数量小于128个;有序集合中所有成员长度都不足64字节。如果有一个条件不满足,则使用跳跃表;且编码只可能由压缩列表转化为跳跃表,反方向则不可能。

下图展示了有序集合编码转换的特点:

五、应用举例

了解Redis的内存模型之后,下面通过几个例子说明其应用。

1、估算Redis内存使用量

要估算redis中的数据占据的内存大小,需要对redis的内存模型有比较全面的了解,包括前面介绍的hashtable、sds、redisobject、各种对象类型的编码方式等。

下面以最简单的字符串类型来进行说明。

假设有90000个键值对,每个key的长度是7个字节,每个value的长度也是7个字节(且key和value都不是整数);下面来估算这90000个键值对所占用的空间。在估算占据空间之前,首先可以判定字符串类型使用的编码方式:embstr。

90000个键值对占据的内存空间主要可以分为两部分:一部分是90000个dictEntry占据的空间;一部分是键值对所需要的bucket空间。

每个dictEntry占据的空间包括:

1)一个dictEntry,24字节,jemalloc会分配32字节的内存块

2)一个key,7字节,所以SDS(key)需要7+9=16个字节,jemalloc会分配16字节的内存块

3)一个redisObject,16字节,jemalloc会分配16字节的内存块

4)一个value,7字节,所以SDS(value)需要7+9=16个字节,jemalloc会分配16字节的内存块

5)综上,一个dictEntry需要32+16+16+16=80个字节。

bucket空间:bucket数组的大小为大于90000的最小的2^n,是131072;每个bucket元素为8字节(因为64位系统中指针大小为8字节)。

因此,可以估算出这90000个键值对占据的内存大小为:90000*80 + 131072*8 = 8248576。

下面写个程序在redis中验证一下:

public class RedisTest {

  public static Jedis jedis = new Jedis("localhost", 6379);

  public static void main(String[] args) throws Exception{
    Long m1 = Long.valueOf(getMemory());
    insertData();
    Long m2 = Long.valueOf(getMemory());
    System.out.println(m2 - m1);
  }

  public static void insertData(){
    for(int i = 10000; i < 100000; i++){
      jedis.set("aa" + i, "aa" + i); //key和value长度都是7字节,且不是整数
    }
  }

  public static String getMemory(){
    String memoryAllLine = jedis.info("memory");
    String usedMemoryLine = memoryAllLine.split("\r\n")[1];
    String memory = usedMemoryLine.substring(usedMemoryLine.indexOf(&#39;:&#39;) + 1);
    return memory;
  }
}

运行结果:8247552

理论值与结果值误差在万分之1.2,对于计算需要多少内存来说,这个精度已经足够了。之所以会存在误差,是因为在我们插入90000条数据之前redis已分配了一定的bucket空间,而这些bucket空间尚未使用。

作为对比将key和value的长度由7字节增加到8字节,则对应的SDS变为17个字节,jemalloc会分配32个字节,因此每个dictEntry占用的字节数也由80字节变为112字节。此时估算这90000个键值对占据内存大小为:90000*112 + 131072*8 = 11128576。

在redis中验证代码如下(只修改插入数据的代码):

public static void insertData(){
  for(int i = 10000; i < 100000; i++){
    jedis.set("aaa" + i, "aaa" + i); //key和value长度都是8字节,且不是整数
  }
}

运行结果:11128576;估算准确。

对于字符串类型之外的其他类型,对内存占用的估算方法是类似的,需要结合具体类型的编码方式来确定。

2、优化内存占用

了解redis的内存模型,对优化redis内存占用有很大帮助。下面介绍几种优化场景。

(1)利用jemalloc特性进行优化

上一小节所讲述的90000个键值便是一个例子。由于jemalloc分配内存时数值是不连续的,因此key/value字符串变化一个字节,可能会引起占用内存很大的变动;在设计时可以利用这一点。

例如,如果key的长度如果是8个字节,则SDS为17字节,jemalloc分配32字节;此时将key长度缩减为7个字节,则SDS为16字节,jemalloc分配16字节;则每个key所占用的空间都可以缩小一半。

(2)使用整型/长整型

如果是整型/长整型,Redis会使用int类型(8字节)存储来代替字符串,可以节省更多空间。因此在可以使用长整型/整型代替字符串的场景下,尽量使用长整型/整型。

(3)共享对象

利用共享对象,可以减少对象的创建(同时减少了redisObject的创建),节省内存空间。目前redis中的共享对象只包括10000个整数(0-9999);可以通过调整REDIS_SHARED_INTEGERS参数提高共享对象的个数;例如将REDIS_SHARED_INTEGERS调整到20000,则0-19999之间的对象都可以共享。

考虑这样一种场景:论坛网站在redis中存储了每个帖子的浏览数,而这些浏览数绝大多数分布在0-20000之间,这时候通过适当增大REDIS_SHARED_INTEGERS参数,便可以利用共享对象节省内存空间。

(4)避免过度设计

然而需要注意的是,不论是哪种优化场景,都要考虑内存空间与设计复杂度的权衡;而设计复杂度会影响到代码的复杂度、可维护性。

如果数据量较小,那么为了节省内存而使得代码的开发、维护变得更加困难并不划算;还是以前面讲到的90000个键值对为例,实际上节省的内存空间只有几MB。但是如果数据量有几千万甚至上亿,考虑内存的优化就比较必要了。

3、关注内存碎片率

内存碎片率是一个重要的参数,对redis 内存的优化有重要意义。

如果内存碎片率过高(jemalloc在1.03左右比较正常),说明内存碎片多,内存浪费严重;这时便可以考虑重启redis服务,在内存中对数据进行重排,减少内存碎片。

如果内存碎片率小于1,说明redis内存不足,部分数据使用了虚拟内存(即swap);由于虚拟内存的存取速度比物理内存差很多(2-3个数量级),此时redis的访问速度可能会变得很慢。因此必须设法增大物理内存(可以增加服务器节点数量,或提高单机内存),或减少redis中的数据。

要减少redis中的数据,除了选用合适的数据类型、利用共享对象等,还有一点是要设置合理的数据回收策略(maxmemory-policy),当内存达到一定量后,根据不同的优先级对内存进行回收。

위 내용은 Redis 메모리 모델(자세한 설명)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 cnblogs.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제