In-depth study of Redis: Redis memory model

1. Redis memory statistics

If you want to do a good job, you must first sharpen your tools. Before explaining Redis memory, first explain how to count the memory usage of Redis.

After the client connects to the server through redis-cli (if there is no special instruction in the following, the client will use redis-cli), you can check the memory usage through the info command:

1

info memory

Among them, the info command can display a lot of information about the redis server, including basic server information, CPU, memory, persistence, client connection information, etc.; memory is a parameter, indicating that only memory-related information is displayed.

Several important instructions in the returned results are as follows:

(1) used_memory : The total amount of memory allocated by the Redis allocator (in bytes), including the virtual memory used (ie swap); the Redis allocator will be introduced later. used_memory_human is just displayed more friendly.

(2) used_memory_rss : The Redis process occupies the memory of the operating system (in bytes), which is consistent with the values ​​seen by the top and ps commands; in addition to the memory allocated by the allocator, used_memory_rss also includes the memory required for the process to run itself , memory fragmentation, etc., but does not include virtual memory.

Therefore, used_memory and used_memory_rss, the former is the amount obtained from the perspective of Redis, and the latter is the amount obtained from the perspective of the operating system. The reason for the difference between the two is that on the one hand, memory fragmentation and the memory occupied by the Redis process may make the former smaller than the latter, and on the other hand, the existence of virtual memory may make the former larger than the latter.

Since the amount of data in Redis will be relatively large in practical applications, the memory occupied by the process running at this time will be much smaller than the amount of Redis data and memory fragmentation; therefore, the ratio of used_memory_rss and used_memory becomes a measure of Redis memory fragmentation The parameter of the rate; this parameter is mem_fragmentation_ratio.

(3) mem_fragmentation_ratio : memory fragmentation ratio, this value is the ratio of used_memory_rss / used_memory.

mem_fragmentation_ratio is generally greater than 1, and the larger the value, the larger the memory fragmentation ratio. mem_fragmentation_ratio<1, indicating that Redis uses virtual memory. Since the medium of virtual memory is disk, it is much slower than memory. When this happens, it should be checked in time. If the memory is insufficient, it should be dealt with in time, such as adding Redis nodes, Redis server memory, optimized applications, etc.

Generally speaking, mem_fragmentation_ratio is in a relatively healthy state around 1.03 (for jemalloc); the value of mem_fragmentation_ratio in the above screenshot is very large, because the data has not been stored in Redis, and the memory of the Redis process itself makes used_memory_rss than used_memory Much bigger.

(4) mem_allocator : The memory allocator used by Redis, which is specified at compile time; it can be libc, jemalloc or tcmalloc, and the default is jemalloc; the default jemalloc is used in the screenshot.

2. Redis memory division

As an in-memory database, Redis mainly stores data (key-value pairs); from the previous description, we can know that in addition to data, other parts of Redis also occupy memory.

The memory usage of Redis can be divided into the following parts:

1. Data

As a database, data is the most important part; the memory occupied by this part will be counted in used_memory.

Redis uses key-value pairs to store data, and the values ​​(objects) include five types, namely strings, hashes, lists, sets, and ordered sets. These 5 types are provided by Redis. In fact, within Redis, each type may have 2 or more internal encoding implementations; in addition, when Redis stores objects, it does not directly throw data into memory, but Objects will be packaged in various ways: such as redisObject, SDS, etc.; later in this article, we will focus on the details of data storage in Redis.

2. The memory required by the process itself to run

The operation of the Redis main process itself must occupy memory, such as code, constant pool, etc.; this part of memory is about a few megabytes, which can be ignored compared with the memory occupied by Redis data in most production environments. This part of memory is not allocated by jemalloc, so it will not be counted in used_memory.

Supplementary note: In addition to the main process, the sub-process created by Redis will also occupy memory, such as the sub-process created when Redis executes AOF and RDB rewriting. Of course, this part of memory does not belong to the Redis process, nor will it be counted in used_memory and used_memory_rss.

3. Buffer memory

Buffer memory includes client buffer, copy backlog buffer, AOF buffer, etc. Among them, the client buffer stores the input and output buffers of client connections; the copy backlog buffer is used for partial copy functions; the AOF buffer is used for AOF replay When writing, save the most recent write command. You don't need to know the details of these buffers before understanding the corresponding functions; this part of memory is allocated by jemalloc, so it will be counted in used_memory.

4. Memory Fragmentation

Memory fragmentation is generated when Redis allocates and reclaims physical memory. For example, if the data is frequently changed and the size of the data differs greatly, the space released by redis may not be released in the physical memory, but redis cannot be effectively used, which forms memory fragmentation. Memory fragmentation will not be counted in used_memory.

The generation of memory fragmentation is related to the operation of the data, the characteristics of the data, etc.; in addition, it is also related to the memory allocator used: if the memory allocator is designed properly, the generation of memory fragmentation can be reduced as much as possible. Jemalloc, which will be mentioned later, does a good job in controlling memory fragmentation.

If the memory fragmentation in the Redis server is already large, you can reduce the memory fragmentation by safely restarting: because after restarting, Redis reads the data from the backup file again, rearranges it in memory, and reselects the appropriate data for each data. memory unit to reduce memory fragmentation.

3. Details of Redis data storage

1 Overview

The details about Redis data storage involve memory allocator (such as jemalloc), simple dynamic string (SDS), 5 object types and internal encoding, redisObject. Before describing the specific content, first explain the relationship between these concepts.

The figure below is the data model involved when executing set hello world.

Image source: https://searchdatabase.techtarget.com.cn/7-20218/

(1) dictEntry: Redis is a Key-Value database, so there will be a dictEntry for each key-value pair, which stores pointers to Key and Value; next points to the next dictEntry, which has nothing to do with this Key-Value.

(2) Key: It can be seen in the upper right corner of the figure that Key ("hello") is not directly stored as a string, but is stored in the SDS structure.

(3) redisObject: Value("world") is neither directly stored as a string, nor directly stored in SDS like Key, but stored in redisObject. In fact, no matter which of the five types Value is, it is stored through redisObject; and the type field in redisObject indicates the type of Value object, and the ptr field points to the address where the object is located. However, it can be seen that although the string object has been packaged by redisObject, it still needs to be stored by SDS.

In fact, besides the type and ptr fields, redisObject has other fields not shown in the figure, such as the field used to specify the internal encoding of the object; it will be described in detail later.

(4) jemalloc: Whether it is a DictEntry object, or a redisObject or SDS object, a memory allocator (such as jemalloc) is required to allocate memory for storage. Taking the DictEntry object as an example, it consists of 3 pointers, occupying 24 bytes on a 64-bit machine, and jemalloc will allocate a 32-byte memory unit for it.

Let's introduce jemalloc, redisObject, SDS, object type and internal encoding respectively.

2、jemalloc

Redis will specify the memory allocator when compiling; the memory allocator can be libc, jemalloc or tcmalloc, and the default is jemalloc.

As the default memory allocator of Redis, jemalloc does a relatively good job in reducing memory fragmentation. In a 64-bit system, jemalloc divides the memory space into three ranges: small, large, and huge; each range is divided into many small memory block units; when Redis stores data, it will select the memory block with the most appropriate size for processing. storage.

The memory unit divided by jemalloc is shown in the following figure:

For example, if an object of size 130 bytes needs to be stored, jemalloc will place it in a memory unit of 160 bytes.

3、redisObject

As mentioned earlier, there are 5 types of Redis objects; no matter which type, Redis will not store them directly, but store them through the redisObject object.

The redisObject object is very important. The type of Redis object, internal coding, memory recovery, shared objects and other functions all need the support of redisObject. The following will explain how it works through the structure of redisObject.

The definition of redisObject is as follows (different versions of Redis may be slightly different):

1

2

3

4

5

6

7

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;

The meaning and function of each field of redisObject are as follows:

(1)type

The type field indicates the type of the object, accounting for 4 bits; currently includes REDIS_STRING (string), REDIS_LIST (list), REDIS_HASH (hash), REDIS_SET (set), REDIS_ZSET (ordered set).

When we execute the type command, we obtain the type of the object by reading the type field of RedisObject; as shown in the following figure:

(2)encoding

encoding represents the internal encoding of the object, accounting for 4 bits.

For each type supported by Redis, there are at least two internal encodings. For example, for strings, there are three encodings: int, embstr, and raw. Through the encoding attribute, Redis can set different encodings for objects according to different usage scenarios, which greatly improves the flexibility and efficiency of Redis. Taking the list object as an example, there are two encoding methods: compressed list and double-ended linked list; if there are fewer elements in the list, Redis tends to use compressed list for storage, because compressed list occupies less memory and can be more efficient than double-ended linked list. Fast loading; when the list object has many elements, the compressed list will be transformed into a double-ended linked list that is more suitable for storing a large number of elements.

Through the object encoding command, you can view the encoding method adopted by the object, as shown in the following figure:

The encoding methods and usage conditions corresponding to the five object types will be introduced later.

(3)lru

lru records the time when the object was last accessed by the command program, and the number of bits occupied by different versions is different (for example, version 4.0 occupies 24 bits, and version 2.6 occupies 22 bits).

By comparing the lru time with the current time, you can calculate the idle time of an object; the object idletime command can display the idle time (in seconds). A special feature of the object idletime command is that it does not change the lru value of the object.

In addition to printing the lru value through the object idletime command, it is also related to the memory recovery of Redis: if the maxmemory option is enabled in Redis, and the memory recovery algorithm is volatile-lru or allkeys—lru, then when the memory usage of Redis exceeds the value specified by maxmemory When the value is set to , Redis will give priority to the object with the longest idling time for release.

(4)refcount

refcount and shared objects

refcount records the number of times the object is referenced, the type is integer, and occupies 4 bytes. The role of refcount is mainly in object reference counting and memory recovery. When a new object is created, refcount is initialized to 1; when a new program uses the object, refcount is incremented by 1; when the object is no longer used by a new program, refcount is decremented by 1; when refcount becomes 0, the memory occupied by the object will be released.

Objects (refcount>1) that are used multiple times in Redis are called shared objects. In order to save memory, when some objects appear repeatedly, the new program will not create new objects, but still use the original objects. This reused object is a shared object. Shared objects currently only support string objects with integer values.

Concrete implementation of shared objects

Redis shared objects currently only support integer-valued string objects. The reason for this is actually a balance between memory and CPU (time): although sharing objects will reduce memory consumption, it takes extra time to determine whether two objects are equal. For integer values, the judgment operation complexity is O(1); for ordinary strings, the judgment complexity is O(n); and for hashes, lists, sets, and ordered sets, the judgment complexity is O(n^ 2).

Although shared objects can only be integer-valued string objects, all 5 types may use shared objects (elements such as hashes, lists, etc. can be used).

As far as the current implementation is concerned, when the Redis server is initialized, it will create 10,000 string objects whose values ​​are integer values ​​from 0 to 9999; when Redis needs to use string objects with values ​​from 0 to 9999, you can use these directly shared with. The number 10000 can be changed by adjusting the value of the parameter REDIS_SHARED_INTEGERS (OBJ_SHARED_INTEGERS in 4.0).

The number of references to shared objects can be viewed through the object refcount command, as shown in the figure below. The result page of command execution proves that only integers between 0 and 9999 will be used as shared objects.

(5)ptr

The ptr pointer points to specific data, such as in the previous example, set hello world, ptr points to the SDS containing the string world. The number of bytes occupied by the ptr pointer is related to the system, for example, it occupies 8 bytes in a 64-bit system.

(6) Summary

To sum up, the structure of redisObject is related to object type, encoding, memory recovery, and shared objects; in a 64-bit system, the size of a redisObject object is 16 bytes:

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

4、SDS

Redis does not directly use C strings (that is, character arrays terminated by the null character '\0') as the default string representation, but uses SDS. SDS is the abbreviation of Simple Dynamic String.

(1) SDS structure

The structure of sds is as follows:

1

2

3

4

5

struct sdshdr {

    int len;

    int free;

    char buf[];

};

Among them, buf represents a byte array used to store strings; len represents the length of buf used, and free represents the length of buf that is not used. Below are two examples.

Image source: "Redis Design and Implementation"

It can be seen from the structure of SDS that the length of the buf array = free + len + 1 (where 1 represents the null character at the end of the string); therefore, the space occupied by an SDS structure is: the length occupied by free + the length occupied by len + The length of the buf array=4+4+free+len+1=free+len+9.

(2) Comparison of SDS and C strings

SDS adds free and len fields on the basis of C strings, which brings many benefits:

  • Get string length: SDS is O(1), C string is O(n)
  • Buffer overflow: When using the C string API, if the string length increases (such as strcat operation) and forget to reallocate memory, it is easy to cause buffer overflow; and SDS records the length, the corresponding API may cause buffering When the area overflows, it will automatically reallocate the memory to prevent buffer overflow.
  • Memory reallocation when modifying strings: For C strings, if you want to modify the string, you must reallocate the memory (release first and then apply), because if there is no reallocation, the memory buffer will overflow when the string length increases , when the length of the string is reduced, it will cause a memory leak. For SDS, since len and free can be recorded, the association between the length of the string and the length of the space array is released, and optimization can be performed on this basis: space pre-allocation strategy (that is, more memory is allocated than actually needed) makes The probability of reallocating memory is greatly reduced when the string length increases; the lazy space release strategy greatly reduces the probability of reallocating memory when the string length decreases.
  • Access to binary data: SDS can, C strings can't. Because the C string uses a null character as the end of the string, and for some binary files (such as pictures, etc.), the content may include an empty string, so the C string cannot be accessed correctly; and the SDS uses the string length len as the The end-of-string flag, so there is no such problem.

In addition, since buf in SDS still uses C strings (that is, ends with '\0'), SDS can use some functions in the C string library; but it should be noted that only when SDS is used to store text data This can only be used when storing binary data ('\0' is not necessarily the end).

(3) Application of SDS and C string

When Redis stores objects, it always uses SDS instead of C strings. For example, the set hello world command, hello and world are stored in the form of SDS. The sadd myset member1 member2 member3 command, whether it is the key ("myset") or the elements in the set ("member1", "member2" and "member3"), is stored in the form of SDS. In addition to storing objects, SDS is also used to store various buffers.

C strings are only used in cases where the string will not change, such as when printing logs.

4. Object type and internal encoding of Redis

As mentioned earlier, Redis supports 5 object types, and each structure has at least two encodings; the advantage of this is that on the one hand, the interface is separated from the implementation, and when the internal encoding needs to be added or changed, the user's use will not be affected , on the other hand, the internal encoding can be switched according to different application scenarios to improve efficiency.

The internal encoding supported by various object types of Redis is shown in the following figure (the version in the figure is Redis3.0, and the internal encoding is added in the later version of Redis, which is omitted; the internal encoding introduced in this chapter is based on 3.0):

Image source: "Redis Design and Implementation"

Regarding the conversion of Redis's internal encoding, it all conforms to the following rules: the encoding conversion is completed when Redis writes data, and the conversion process is irreversible, and can only be converted from small memory encoding to large memory encoding.

1. String

(1) Overview

String is the most basic type, because all keys are of type String, and elements of several other complex types besides String are also String.

String length cannot exceed 512MB.

(2) Internal encoding

There are three internal encodings of the string type, and their application scenarios are as follows:

  • int: 8-byte long integer. When the string value is an integer, this value is represented by a long integer.
  • embstr: <=39 byte string. Both embstr and raw use redisObject and sds to save data. The difference is that the use of embstr only allocates memory space once (so redisObject and sds are continuous), while raw needs to allocate memory space twice (allocate space for redisObject and sds respectively). Therefore, compared with raw, the advantage of embstr is that it allocates less space when creating, releases less space when deleting, and connects all the data of the object together, making it easy to find. The disadvantages of embstr are also obvious. If the length of the string increases and the memory needs to be reallocated, the entire redisObject and sds need to reallocate space, so the embstr in redis is implemented as read-only.
  • raw: strings larger than 39 bytes

An example is shown in the figure below:

The length of the distinction between embstr and raw is 39; because the length of redisObject is 16 bytes, and the length of sds is 9+string length; therefore, when the string length is 39, the length of embstr is exactly 16+9+39 =64, jemalloc can just allocate a 64-byte memory unit.

(3) Code conversion

When the int data is no longer an integer, or the size exceeds the range of long, it is automatically converted to raw.

For embstr, because its implementation is read-only, when modifying the embstr object, it will be converted to raw first and then modified. Therefore, as long as the embstr object is modified, the modified object must be raw, no matter whether it reaches took 39 bytes. An example is shown in the figure below:

2. List

(1) Overview

A list (list) is used to store multiple ordered strings, and each string is called an element; a list can store 2^32-1 elements. Lists in Redis support insertion and pop-up at both ends, and can obtain elements at specified positions (or ranges), which can act as arrays, queues, stacks, etc.

(2) Internal encoding

The internal encoding of the list can be a compressed list (ziplist) or a double-ended linked list (linkedlist).

Double-ended linked list: It consists of a list structure and multiple listNode structures; a typical structure is shown in the figure below:

Image source: "Redis Design and Implementation"

It can be seen from the figure that the double-ended linked list saves the head pointer and the tail pointer at the same time, and each node has pointers pointing to the front and pointing to the back; the length of the list is saved in the linked list; dup, free and match are nodes Values ​​set type-specific functions, so linked lists can be used to hold values ​​of various different types. Each node in the linked list points to a redisObject whose type is a string.

Compressed list: Compressed list is developed by Redis to save memory. It is a sequential data structure composed of a series of specially encoded continuous memory blocks (instead of each node being a pointer like a double-ended linked list); the specific structure is relatively complicated. ,slightly. Compared with the double-ended linked list, the compressed list can save memory space, but the complexity is higher when modifying or adding or deleting operations; therefore, when the number of nodes is small, the compressed list can be used; but when the number of nodes is large, the double-ended linked list is still used cost-effective.

Compressed lists are not only used to implement lists, but also hashes, ordered lists; very widely used.

(3) Code conversion

The compressed list will be used only when the following two conditions are met at the same time: the number of elements in the list is less than 512; all string objects in the list are less than 64 bytes. If one of the conditions is not met, a double-ended list is used; and the encoding can only be converted from a compressed list to a double-ended linked list, and the reverse direction is not possible.

The following figure shows the characteristics of list encoding conversion:

Among them, a single string cannot exceed 64 bytes, which is to facilitate the uniform allocation of the length of each node; 64 bytes here refers to the length of the string, excluding the SDS structure, because the compressed list is stored in continuous, fixed-length memory blocks String, no need for SDS structure to indicate length. When it comes to the compressed list later, it will also emphasize that the length does not exceed 64 bytes. The principle is similar to this.

3. Hash

(1) Overview

Hash (as a data structure) is not only one of the five object types provided by redis (in parallel with strings, lists, sets, and ordered combinations), but also the data structure used by Redis as a Key-Value database . For the convenience of explanation, when "inner hash" is used later in this article, it represents one of the five object types provided by redis; the use of "outer hash" refers to Redis as the Key-Value database The data structure used.

(2) Internal encoding

The internal encoding used by the inner hash can be compressed list (ziplist) and hashtable (hashtable); the outer hash of Redis only uses hashtable.

Compressed lists were described earlier. Compared with the hash table, the compressed list is used in scenarios with a small number of elements and small element lengths; its advantage lies in centralized storage and space saving; at the same time, although the operation complexity for elements is also changed from O(1) to O( n), but due to the small number of elements in the hash, there is no obvious disadvantage in the time of the operation.

hashtable: A hashtable consists of 1 dict structure, 2 dicttht structures, 1 dictEntry pointer array (called bucket) and multiple dictEntry structures.

Under normal circumstances (that is, when the hashtable is not rehashed), the relationship between each part is shown in the figure below:

The picture is adapted from: "Redis Design and Implementation"

The following describes each part in turn from the bottom up:

dictEntry

The dictEntry structure is used to store key-value pairs, and the structure is defined as follows:

1

2

3

4

5

6

7

8

9

typedef struct dictEntry{

    void *key;

    union{

        void *val;

        uint64_tu64;

        int64_ts64;

    }v;

    struct dictEntry *next;

}dictEntry;

Among them, the functions of each attribute are as follows:

  • key: the key in the key-value pair;
  • val: The value in the key-value pair, which is implemented using a union (that is, a union), and the stored content may be a pointer to a value, or a 64-bit integer, or an unsigned 64-bit integer;
  • next: Point to the next dictEntry, used to solve the hash collision problem

In a 64-bit system, a dictEntry object occupies 24 bytes (key/val/next each occupy 8 bytes).

bucket

Bucket is an array, and each element of the array is a pointer to a dictEntry structure. The calculation rules for the size of the bucket array in redis are as follows: the smallest 2^n that is larger than dictEntry; for example, if there are 1000 dictEntry, the bucket size is 1024; if there are 1500 dictEntry, the bucket size is 2048.

dictht

The dict structure is as follows:

1

2

3

4

5

6

typedef struct dictht{

    dictEntry **table;

    unsigned long size;

    unsigned long sizemask;

    unsigned long used;

}dictht;

Among them, the function description of each attribute is as follows:

  • The table attribute is a pointer to the bucket;
  • The size attribute records the size of the hash table, that is, the size of the bucket;
  • used records the number of dictEntry used;
  • sizemask属性的值总是为size-1,这个属性和哈希值一起决定一个键在table中存储的位置。

dict

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

dict结构如下:

1

2

3

4

5

6

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。

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

1

2

3

4

5

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中验证一下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

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(':') + 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中验证代码如下(只修改插入数据的代码):

1

2

3

4

5

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开发与运维》

《Redis设计与实现》

https://redis.io/documentation

http://redisdoc.com/server/info.html

https://www.cnblogs.com/lhcpig/p/4769397.html

https://searchdatabase.techtarget.com.cn/7-20218/

http://www.cnblogs.com/mushroom/p/4738170.html

http://www.imooc.com/article/3645

http://blog.csdn.net/zhengpeitao/article/details/76573053

Guess you like

Origin blog.csdn.net/qq_41872328/article/details/129918459