Trang chủ > Công nghệ máy chủ > Nội dung chính

Phân tích sâu cấu trúc dữ liệu bên trong Redis (3) —— robj


Phân tích cấu trúc dữ liệu bên trong Redis Phân tích cấu trúc dữ liệu bên trong Redis Bài thứ ba trong loạt bài nàybầu cua, nói về một cấu trúc dữ liệu cơ bản trong việc thực hiện Redis: robj.

Vậy rốt cuộc điều gì là robj? Nó có tác dụng gì?

Từ góc độ người sử dụng Redis789 Club, một nút Redis có thể chứa nhiều database (mặc định là 16 database trong chế độ không phải cluster và chỉ có thể có 1 database trong chế độ cluster). Mỗi database chịu trách nhiệm quản lý mối quan hệ ánh xạ giữa không gian key và khô Trong đó, key luôn có kiểu dữ liệu string, còn giá trị (value) có thể là nhiều loại khác nhau như: chuỗi (string), danh sách (list), bản đồ (hash) hoặc thậm chí là các tập hợp (set). Điều này cho thấy rằng mặc dù kiểu của key luôn cố định ở dạng chuỗi, nhưng kiểu của value lại linh hoạt và có thể thay đổi tùy thuộc vào nhu cầu sử dụng.

Từ góc độ thực hiện bên trong của Redisxem ngoại hạng anh, như đã đề cập trong bài đầu tiên của loạt bài viết này, một database duy trì mối quan hệ ánh xạ thông qua một dict. Đối với phần key của dict, chúng ta chỉ cần sử dụng một cấu trúc dữ liệu để biểu diễn, và đó chính là chuỗi động sds (simple dynamic string). Còn đối với phần value, nó phức tạp hơn nhiều vì cần một cơ chế để lưu trữ các giá trị khác nhau về kiểu dữ liệu trong cùng một dict. Để làm được điều đó, Redis sử dụng một cấu trúc dữ liệu chung gọi là robj (tên đầy đủ là redisObject). Hãy lấy ví dụ: nếu value là một list, thì cấu trúc lưu trữ nội bộ sẽ là quicklist (chúng ta sẽ thảo luận chi tiết về quicklist trong những bài viết sau). Nếu value là một chuỗi (string), thì cấu trúc lưu trữ nội bộ thường là sds. Tuy nhiên, thực tế có phần phức tạp hơn chút nữa. Ví dụ, một giá trị kiểu string có thể chứa một số nguyên, khi đó Redis sẽ chuyển đổi nó thành kiểu long để giảm thiểu việc sử dụng bộ nhớ. Một robj không chỉ có thể biểu diễn một sds hay một quicklist mà còn có thể biểu diễn cả kiểu dữ liệu long. Điều này giúp cho Redis linh hoạt hơn trong việc quản lý các loại giá trị khác nhau mà không cần tạo ra quá nhiều cấu trúc dữ liệu riêng biệt.

Định nghĩa cấu trúc dữ liệu robj

Trong tệp server.hxem ngoại hạng anh, chúng ta tìm thấy mã liên quan đến định nghĩa của robj, như bên dưới (cần lưu ý rằng các đoạn mã trong loạt bài viết này đều được lấy từ nhánh 3.2 của mã nguồn 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
25
26
27
28
29
													
														/* Object types */
													
#define OBJ_STRING 0
#define OBJ_LIST 1
#define OBJ_SET 2
#define OBJ_ZSET 3
#define OBJ_HASH 4

													
/* Objects encoding. Some kind of objects like Strings and Hashes can be
 * internally represented in multiple ways. The 'encoding' field of the object
 * is set to one of this fields for this object. */
													
#define OBJ_ENCODING_RAW 0     
													/* Raw representation */
													
#define OBJ_ENCODING_INT 1     
													/* Encoded as integer */
													
#define OBJ_ENCODING_HT 2      
													/* Encoded as hash table */
													
#define OBJ_ENCODING_ZIPMAP 3  
													/* Encoded as zipmap */
													
#define OBJ_ENCODING_LINKEDLIST 4 
													/* Encoded as regular linked list */
													
#define OBJ_ENCODING_ZIPLIST 5 
													/* Encoded as ziplist */
													
#define OBJ_ENCODING_INTSET 6  
													/* Encoded as intset */
													
#define OBJ_ENCODING_SKIPLIST 7  
													/* Encoded as skiplist */
													
#define OBJ_ENCODING_EMBSTR 8  
													/* Embedded sds string encoding */
													
#define OBJ_ENCODING_QUICKLIST 9 
													/* Encoded as linked list of ziplists */
													

													
#define LRU_BITS 24

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

Một robj bao gồm 5 trường như sau:

  • Loại dữ liệu của đối tượng: Là loại thông tin xác định kiểu dữ liệu mà đối tượng đang lưu trữ789 Club, chiếm 4 bit. Có tổng cộng 5 giá trị có thể xảy ra: OBJ_STRING, OBJ_LIST, OBJ_SET, OBJ_ZSET và OBJ_HASH, tương ứng với 5 loại cấu trúc dữ liệu mà Redis cung cấp cho người dùng bên ngoài (tức là 5 loại cấu trúc dữ liệu đầu tiên mà chúng ta đã đề cập trong bài viết đầu tiên). Mỗi giá trị này giúp mô tả một cách rõ ràng loại dữ liệu cụ thể mà Redis đang xử lý trong hệ thống của nó.
  • Phương thức mã hóa: Cách biểu diễn nội bộ của đối tượng (cũng có thể được gọi là mã hóa). Nó chiếm 4 bit. Có thể có 10 giá trị khác nhauxem ngoại hạng anh, cụ thể là 10 hằng số OBJ_ENCODING_XXX trong mã nguồn trước đây. Mỗi hằng số này đại diện cho một cách mã hóa đặc biệt mà đối tượng có thể sử dụng để lưu trữ dữ liệu một cách hiệu quả tùy theo loại của nó.
  • LRU: Dùng để thực hiện thuật toán thay thế LRUbầu cua, chiếm 24 bit. Điều này không phải là trọng tâm trong phạm vi thảo luận của chúng ta, vì vậy hãy tạm thời bỏ qua nó. Trong lĩnh vực quản lý bộ nhớ, có rất nhiều khái niệm phức tạp cần hiểu rõ, nhưng ở đây, chúng ta tập trung vào các vấn đề chính và sẽ trở lại với chi tiết về LRU sau. Thuật toán LRU tuy quan trọng nhưng không phải là yếu tố duy nhất quyết định hiệu suất hệ thống, do đó việc tạm thời gác lại sẽ không làm ảnh hưởng đến phần lớn nội dung mà chúng ta đang nghiên cứu.
  • refcount: Số lần tham chiếu. Nó cho phép các đối tượng robj được chia sẻ trong một số tình huống.
  • Con trỏ ptr: Đây là con trỏ dữ liệubầu cua, dẫn đến phần thực sự chứa dữ liệu. Ví dụ, đối với một robj đại diện cho chuỗi (string), con trỏ ptr của nó có thể hướng tới một cấu trúc sds; còn đối với một robj đại diện cho danh sách (list), con trỏ ptr của nó có thể dẫn đến mộ Điều này giúp hệ thống quản lý và truy cập dữ liệu một cách hiệu quả và linh hoạt trong bộ nhớ.

Điều cần chú ý đặc biệt ở đây chính là trường "encoding". Đối với cùng một loại "type"xem ngoại hạng anh, vẫn có thể tồn tại nhiều kiểu "encoding" khác nhau. Điều này cho thấy rằng, ngay cả khi cùng một loại dữ liệu, nó vẫn có thể được biểu diễn theo những cách nội bộ khác biệt. Và mỗi cách biểu diễn nội bộ này sẽ ảnh hưởng đến việc sử dụng bộ nhớ cũng như hiệu suất tìm kiếm. Có thể nói, việc lựa chọn đúng encoding không chỉ giúp tối ưu hóa về mặt tài nguyên mà còn nâng cao hiệu quả hoạt động của hệ thống.

Ví dụbầu cua, khi type = OBJ_STRING, điều này cho biết rằng robj đang lưu trữ một chuỗi và encoding có thể là một trong ba loại sau:

  • Với OBJ_ENCODING_RAW789 Club, chuỗi được biểu diễn theo cách gốc mà không có sự mã hóa phức tạp nào, cụ thể là sử dụng sds (simple dynamic string) để lưu trữ và quản lý dữ liệu. Đây là một phương pháp trực tiếp và đơn giản, giúp duy trì tính toàn vẹn của chuỗi trong quá trình xử lý.
  • Với OBJ_ENCODING_INTbầu cua, chuỗi được biểu diễn dưới dạng số nguyên, cụ thể là một kiểu dữ liệu long. Đây là cách lưu trữ tối ưu khi giá trị trong chuỗi hoàn toàn có thể biểu diễn dưới dạng số nguyên mà không cần tốn thêm bộ nhớ cho các ký tự khác.
  • Khi sử dụng OBJ_ENCODING_EMBSTR789 Club, chuỗi sẽ được biểu diễn bằng một loại sds (simple dynamic string) đặc biệt có tên là "embedded". Loại sds này tích hợp trực tiếp dữ liệu vào bộ nhớ, giúp tối ưu hóa việc quản lý bộ nhớ và tăng tốc độ xử lý. Chúng ta sẽ đi sâu hơn vào chi tiết của cơ chế này trong phần thảo luận tiếp theo.

Hãy lấy một ví dụ khác: Khi type được thiết lập thành OBJ_HASHbầu cua, điều này cho thấy rằng đối tượng robj đang lưu trữ một cấu trúc hash. Ở trường hợp này, encoding có thể là một trong hai tùy chọn sau đây: - **Encoding loại 1:** Sử dụng phương thức lưu trữ dựa trên danh sách liên kết đôi (doubly linked list) để quản lý các cặp khóa-giá trị. Phương pháp này giúp truy xuất từng phần tử nhanh chóng và hiệu quả. - **Encoding loại 2:** Sử dụng cấu trúc cây cân bằng (balanced tree), cho phép sắp xếp tự động các khóa theo thứ tự từ điển, giúp tối ưu hóa việc tìm kiếm và sắp xếp khi làm việc với tập dữ liệu lớn. Cả hai cách tiếp cận đều có những ưu điểm riêng, tùy thuộc vào yêu cầu cụ thể của ứng dụng mà bạn có thể lựa chọn encoding phù hợp nhất.

  • OBJ_ENCODING_HT: Hash sử dụng một dict để biểu diễn.
  • Với OBJ_ENCODING_ZIPLISTbầu cua, hash sẽ được biểu diễn bằng một ziplist (cách thực hiện cụ thể của ziplist chúng ta sẽ trong các bài viết tiếp theo). Đây là một phương pháp lưu trữ tiết kiệm không gian, nơi các cặp khóa-giá trị được sắp xếp liên tiếp trong cùng một cấu trúc dữ liệu.

Phần chính còn lại của bài viết sẽ tập trung vào đối tượng robj biểu diễn stringxem ngoại hạng anh, đi sâu phân tích ba dạng encoding khác nhau của nó. Ở các đoạn mã trước đó, chúng ta đã thấy mười dạng encoding khác nhau xuất hiện. Trước khi đi sâu vào chi tiết, hãy cùng điểm qua nhanh những dạng encoding này. Chúng ta có thể sẽ gặp lại chúng trong các bài viết tiếp theo của loạt bài này.

  • Với OBJ_ENCODING_RAWbầu cua, đây là cách biểu diễn nguyên thủy nhất. Thực tế, chỉ có kiểu dữ liệu string mới sử dụng giá trị mã hóa này (được biểu thị dưới dạng sds). Dạng raw encoding giữ nguyên dữ liệu mà không thực hiện bất kỳ thay đổi hay tối ưu hóa nào, đảm bảo tính toàn vẹn của chuỗi gốc mà không có bất kỳ sự can thiệp nào từ hệ thống.
  • OBJ_ENCODING_INT: Biểu diễn dưới dạng số. Thực tế sử dụng long để thể hiện.
  • OBJ_ENCODING_HT: Biểu diễn dưới dạng dict.
  • OBJ_ENCODING_ZIPMAP: Đây là một định dạng cũ và đã bị loại bỏ. Bạn chỉ có thể tìm thấy nó trong các phiên bản Redis nhỏ hơn 2.6. Định dạng này từng được sử dụng để lưu trữ các trường và giá trị trong một cách tương đối đơn giảnbầu cua, nhưng đã được thay thế bởi các phương pháp hiệu quả hơn qua các phiên bản cập nhật.
  • OBJ_ENCODING_LINKEDLIST: Cách biểu diễn cũbầu cua, hiện không còn sử dụng.
  • OBJ_ENCODING_ZIPLIST: Biểu diễn dưới dạ
  • OBJ_ENCODING_INTSET: đại diện cho kiểu intset. Được sử dụng trong cấu trúc dữ liệu setbầu cua, giúp lưu trữ các phần tử số nguyên một cách hiệu quả và tiết kiệm bộ nhớ.
  • OBJ_ENCODING_SKIPLIST: đại diện cho định dạ Đây là phương thức được sử dụng trong cấu trúc dữ liệu sorted setbầu cua, giúp tối ưu hóa việc tìm kiếm và sắp xếp các phần tử một cách hiệu quả.
  • OBJ_ENCODING_EMBSTR: Biểu diễn dưới dạng một loại sds nhúng đặc biệt.
  • OBJ_ENCODING_QUICKLIST: đại diệ Được sử dụng cho cấu trúc dữ liệu list. Quicklist là một sự kết hợp hiệu quả giữa các nút list và các khối memory liên kếtbầu cua, giúp tối ưu hóa bộ nhớ và tăng tốc độ truy xuất cho các hoạt động liên quan đến danh sách. Với đặc tính này, nó trở thành lựa chọn phổ biến trong các ứng dụng cần xử lý dữ liệu theo chuỗi hoặc hàng đợi.

Chúng ta hãy tổng kết lại vai trò của robj:

  • Cung cấp một cách thống nhất để biểu diễn nhiều loại dữ liệu.
  • Cho phép cùng một loại dữ liệu sử dụng các phương thức biểu diễn nội bộ khác nhauxem ngoại hạng anh, từ đó tiết kiệm bộ nhớ trong một số trường hợp.
  • Hỗ trợ chia sẻ đối tượng và đếm tham chiếu. Khi đối tượng được chia sẻxem ngoại hạng anh, chỉ chiếm một bản sao bộ nhớ, tiếp tục tiết kiệm bộ nhớ.

Quy trình mã hóa string robj

Khi chúng ta thực hiện lệnh SET trong Redisbầu cua, Redis sẽ đầu tiên biểu diễn giá trị value (kiểu chuỗi) nhận được thành một đối tượng robj với type = OBJ_STRING và encoding = OBJ_ENCODING_RAW. Sau đó, trước khi lưu vào bộ nhớ nội bộ, nó sẽ thực hiện một quá trình mã hóa để cố gắng biểu diễn giá trị này theo một cách mã hóa khác tiết kiệm bộ nhớ hơn. Phần lõi của quy trình này nằm trong hà c. Quá trình này không chỉ giúp tối ưu hóa việc quản lý bộ nhớ mà còn tăng cường hiệu suất hoạt động của Redis. Bằng cách chọn loại mã hóa phù hợp, Redis có thể giảm thiểu việc sử dụng tài nguyên hệ thống mà vẫn đảm bảo hiệu quả xử lý dữ liệu cao. Điều này đặc biệt quan trọng khi làm việc với các ứng dụng cần xử lý khối lượng lớn dữ liệu trong thời gian ngắn.

							
								
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
													
														robj
													 *
													tryObjectEncoding
													(
													robj
													 *
													o
													)
													 {
													
    long
													 value
													;
													
    sds
													 s
													 =
													 o
													->
													ptr
													;
													
    size_t
													 len
													;
													

    /* Make sure this is a string object789 Club, the only type we encode
     * in this function. Other types use encoded memory efficient
     * representations but are handled by the commands implementing
     * the type. */
    serverAssertWithInfo
													(
													NULL
													,
													o
													,
													o
													->
													type
													 ==
													 OBJ_STRING
													);
													

    /* We try some specialized encoding only for objects that are
     * RAW or EMBSTR encodedbầu cua, in other words objects that are still
     * in represented by an actually array of chars. */
    if
													 (
													!
													sdsEncodedObject
													(
													o
													))
													 return
													 o
													;
													

    /* It's not safe to encode shared objects: shared objects can be shared
     * everywhere in the "object space" of Redis and may end in places where
     * they are not handled. We handle them only as values in the keyspace. */
													
     if
													 (
													o
													->
													refcount
													 >
													 1
													)
													 return
													 o
													;
													

    /* Check if we can represent this string as a long integer.
     * Note that we are sure that a string larger than 21 chars is not
     * representable as a 32 nor 64 bit integer. */
													
    len
													 =
													 sdslen
													(
													s
													);
													
    if
													 (
													len
													 <=
													 21
													 &&
													 string2l
													(
													s
													,
													len
													,
													&
													value
													))
													 {
													
        /* This object is encodable as a long. Try to use a shared object.
         * Note that we avoid using shared integers when maxmemory is used
         * because every object needs to have a private LRU field for the LRU
         * algorithm to work well. */
													
        if
													 ((
													server
													.
													maxmemory
													 ==
													 0
													 ||
													
             (
													server
													.
													maxmemory_policy
													 !=
													 MAXMEMORY_VOLATILE_LRU
													 &&
													
              server
													.
													maxmemory_policy
													 !=
													 MAXMEMORY_ALLKEYS_LRU
													))
													 &&
													
            value
													 >=
													 0
													 &&
													
            value
													 <
													 OBJ_SHARED_INTEGERS
													)
													
        {
													
            decrRefCount
													(
													o
													);
													
            incrRefCount
													(
													shared
													.
													integers
													[
													value
													]);
													
            return
													 shared
													.
													integers
													[
													value
													];
													
        }
													 else
													 {
													
            if
													 (
													o
													->
													encoding
													 ==
													 OBJ_ENCODING_RAW
													)
													 sdsfree
													(
													o
													->
													ptr
													);
													
            o
													->
													encoding
													 =
													 OBJ_ENCODING_INT
													;
													
            o
													->
													ptr
													 =
													 (
													void
													*
													)
													 value
													;
													
            return
													 o
													;
													
        }
													
    }
													

    /* If the string is small and is still RAW encodedbầu cua,
     * try the EMBSTR encoding which is more efficient.
     * In this representation the object and the SDS string are allocated
     * in the same chunk of memory to save space and cache misses. */
    if
													 (
													len
													 <=
													 OBJ_ENCODING_EMBSTR_SIZE_LIMIT
													)
													 {
													
        robj
													 *
													emb
													;
													

        if
													 (
													o
													->
													encoding
													 ==
													 OBJ_ENCODING_EMBSTR
													)
													 return
													 o
													;
													
        emb
													 =
													 createEmbeddedStringObject
													(
													s
													,
													sdslen
													(
													s
													));
													
        decrRefCount
													(
													o
													);
													
        return
													 emb
													;
													
    }
													

    /* We can't encode the object...
     *
     * Do the last trybầu cua, and at least optimize the SDS string inside
     * the string object to require little space, in case there
     * is more than 10% of free space at the end of the SDS string.
     *
     * We do that only for relatively large strings as this branch
     * is only entered if the length of the string is greater than
     * OBJ_ENCODING_EMBSTR_SIZE_LIMIT. */
    if
													 (
													o
													->
													encoding
													 ==
													 OBJ_ENCODING_RAW
													 &&
													
        sdsavail
													(
													s
													)
													 >
													 len
													/
													10
													)
													
    {
													
        o
													->
													ptr
													 =
													 sdsRemoveFreeSpace
													(
													o
													->
													ptr
													);
													
    }
													

    /* Return the original object. */
													
    return
													 o
													;
													
}
													

Đoạn mã này thực hiện các hoạt động phức tạp789 Club, chúng ta cần xem xét kỹ từng bước:

  • Bước 1 kiểm trabầu cua, kiể Đảm bảo chỉ thực hiện thao tác trên các đối tượng kiểu string.
  • Bước kiểm tra thứ haibầu cua, hãy đảm bảo rằng bạn đã kiểm tra encoding (mã hóa) cẩn thận. Macro sdsEncodedObject được định nghĩ h, và nó chỉ nên áp dụng cho các đối tượng chuỗi có mã hóa là OBJ_ENCODING_RAW hoặc OBJ_ENCODING_EMBSTR. Hai loại mã hóa này đều sử dụng sds (simple dynamic string) để lưu trữ dữ liệu. Bạn có thể thử tiếp tục xử lý mã hóa sâu hơn đối với các đối tượng thuộc hai loại này, vì chúng thường có khả năng tối ưu hóa tốt hơn so với các loại mã hóa khác.
							
								
									#define sdsEncodedObject(objptr) (objptr->encoding == OBJ_ENCODING_RAW || objptr->encoding == OBJ_ENCODING_EMBSTR)
								
							
  • Bước kiểm tra thứ ba là kiểm tra refcount (số lần tham chiếu). Những đối tượng được chia sẻ có số lần tham chiếu lớn hơn 1 sẽ được sử dụng ở nhiều nơi khác nhau. Khi quá trình mã hóa kết thúcbầu cua, con trỏ đối tượng của biến robj có thể thay đổi (chúng ta đã đề cập đến mô hình sử dụng giao diện tương tự khi giải thích hàm sdscatlen trong bài trước). Do đó, đối với các đối tượng có số lần tham chiếu lớn hơn 1, việc cập nhật tất cả các nơi tham chiếu trở nên rất phức tạp. Vì lý do này, chúng tôi quyết định không thực hiện quá trình mã hóa đối với các đối tượng có số lần tham chiếu lớn hơn 1.
  • Bạn có thể thử chuyển đổi chuỗi thành số nguyên dài 64 bit. Phạm vi giá trị mà một số nguyên dài 64 bit có thể biểu diễn là từ -2^63 đến 2^63 - 1xem ngoại hạng anh, khi viết dưới dạng thập phân sẽ dài nhất là 20 chữ số (bao gồm dấu trừ). Ở đây, việc kiểm tra điều kiện nhỏ hơn hoặc bằng 21 dường như là thừa thãi, thực tế chỉ cần kiểm tra nhỏ hơn hoặc bằng 20 là đủ (nếu tôi tính nhầm, hãy chắc chắn cho tôi biết nhé). Hàm string2l nếu thành công trong việc chuyển đổi chuỗi thành số nguyên dài, nó sẽ trả về 1 và lưu giá trị đã chuyển đổi vào biến value.
  • Khi chuyển đổi thành long thành côngbầu cua, phân ra hai trường hợp.
    • Trường hợp đầu tiên: Nếu cấu hình Redis không yêu cầu sử dụng thuật toán thay thế LRU và giá trị số nguyên được chuyển đổi có giá trị nhỏ (dưới OBJ_SHARED_INTEGERS789 Club, hiện tại giá trị này là 10.000), thì các đối tượng số chung sẽ được sử dụng để biểu diễn. Lý do tại sao điều kiện ở đây liên quan đến LRU là vì thuật toán LRU yêu cầu mỗi robj có giá trị trường lru khác nhau, do đó khi sử dụng LRU, các robj không thể được chia sẻ. Mảng shared.integers có độ dài là 10.000 và đã lưu trữ trước 10.000 đối tượng số nhỏ. Những đối tượng số này đều là các đối tượng string robj với thuộc tính encoding = OBJ_ENCODING_INT. Đi sâu hơn vào chi tiết, mảng shared.integers được tạo ra để tối ưu hóa việc quản lý bộ nhớ khi xử lý các giá trị số nhỏ thường xuyên được sử dụng. Việc tái sử dụng các đối tượng số chung giúp giảm thiểu lượng bộ nhớ tiêu tốn và tăng tốc độ truy xuất. Điều này đặc biệt hữu ích trong các trường hợp mà các giá trị số nhỏ được tạo ra một cách thường xuyên nhưng không cần duy trì trạng thái độc lập giữa các giá trị. Tuy nhiên, nếu cần thiết phải áp dụng thuật toán LRU, thì mỗi giá trị sẽ được xử lý riêng lẻ để đảm bảo tính phân biệt của các giá trị lru. Với cấu trúc hiện tại của Redis, việc sử dụng shared.integers mang lại lợi ích đáng kể cho hiệu suất hệ thống khi không cần thiết phải thực hiện các hoạt động thay thế dựa trên LRU.
    • Trường hợp thứ hai: Nếu bước trước đó không thể sử dụng đối tượng nhỏ chung để biểu diễnbầu cua, thì robj ban đầu sẽ được mã hóa thành encoding = OBJ_ENCODING_INT. Khi đó, trường ptr sẽ lưu trữ trực tiếp giá trị kiểu long này. Lưu ý rằng ptr vốn là một con trỏ void * (tức là nó lưu trữ địa chỉ bộ nhớ), vì vậy trên máy tính 64-bit, nó có độ rộng 64 bit, hoàn toàn đủ khả năng để lưu trữ một giá trị kiểu long 64 bit. Điều này có nghĩa là ngoài việc giữ robj ra, không cần thêm bất kỳ vùng nhớ bổ sung nào để lưu trữ giá trị chuỗi. Bằng cách này, Redis tối ưu hóa không gian bộ nhớ và tăng hiệu suất khi xử lý các số nguyên lớn mà không cần phải sử dụng vùng nhớ riêng cho chuỗi ký tự. Đây là một trong những kỹ thuật thông minh giúp giảm thiểu sự phức tạp trong việc quản lý bộ nhớ trong hệ thống Redis.
  • Tiếp theo là xử lý các chuỗi không thể chuyển đổi thành long 64 bit. Cuối cùng thực hiện thêm hai bước xử lý:
    • Nếu độ dài chuỗi đủ nhỏ (ít hơn hoặc bằng OBJ_ENCODING_EMBSTR_SIZE_LIMIT789 Club, được định nghĩa là 44), thì việc gọi createEmbeddedStringObject sẽ mã hóa nó thành encoding = OBJ_ENCODING_EMBSTR. Khi đó, thay vì sử dụng kiểu chuỗi thường, hệ thống sẽ lưu trữ chuỗi trong bộ nhớ theo một cấu trúc đặc biệt, giúp tối ưu hóa bộ nhớ và tăng tốc độ truy xuất. Điều này đặc biệt hữu ích khi xử lý các chuỗi ngắn, nơi mà việc quản lý bộ nhớ hiệu quả đóng vai trò quan trọng trong hiệu suất tổng thể của ứng dụng. Với kích thước nhỏ như vậy, việc chọn kiểu mã hóa OBJ_ENCODING_EMBSTR không chỉ tiết kiệm tài nguyên mà còn giảm thiểu chi phí phân bổ bộ nhớ, mang lại lợi thế rõ rệt trong các tình huống đòi hỏi hiệu suất cao.
    • Nếu tất cả các lần thử mã hóa trước đó đều thất bại (vẫn còn ở trạng thái OBJ_ENCODING_RAW) và trong sds có quá nhiều byte dư thừa789 Club, thì sẽ có một nỗ lực cuối cùng được thực hiện để gọi phương thức sdsRemoveFreeSpace của sds nhằm giải phóng những byte dư thừa này. Điều này giúp tối ưu hóa bộ nhớ và cải thiện hiệu suất của cấu trúc dữ liệu.

Chúng ta nên xem qua mã nguồn của hàm createEmbeddedStringObject mà đoạn mã đang gọi đến. Đây là một bước quan trọng để hiểu rõ cách hoạt động bên trong và đảm bảo mọi thứ diễn ra như mong đợi.

							
								
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
													
														robj
													 *
													createEmbeddedStringObject
													(
													const
													 char
													 *
													ptr
													,
													 size_t
													 len
													)
													 {
													
    robj
													 *
													o
													 =
													 zmalloc
													(
													sizeof
													(
													robj
													)
													+
													sizeof
													(
													struct
													 sdshdr8
													)
													+
													len
													+
													1
													);
													
    struct
													 sdshdr8
													 *
													sh
													 =
													 (
													void
													*
													)(
													o
													+
													1
													);
													

    o
													->
													type
													 =
													 OBJ_STRING
													;
													
    o
													->
													encoding
													 =
													 OBJ_ENCODING_EMBSTR
													;
													
    o
													->
													ptr
													 =
													 sh
													+
													1
													;
													
    o
													->
													refcount
													 =
													 1
													;
													
    o
													->
													lru
													 =
													 LRU_CLOCK
													();
													

    sh
													->
													len
													 =
													 len
													;
													
    sh
													->
													alloc
													 =
													 len
													;
													
    sh
													->
													flags
													 =
													 SDS_TYPE_8
													;
													
    if
													 (
													ptr
													)
													 {
													
        memcpy
													(
													sh
													->
													buf
													,
													ptr
													,
													len
													);
													
        sh
													->
													buf
													[
													len
													]
													 =
													 '\0'
													;
													
    }
													 else
													 {
													
        memset
													(
													sh
													->
													buf
													,
													0
													,
													len
													+
													1
													);
													
    }
													
    return
													 o
													;
													
}
													

Hàm createEmbeddedStringObject thực hiện việc phân bổ lại bộ nhớ cho sds và đặt cả robj lẫn sds vào một khối bộ nhớ liên tục. Điều này giúp tối ưu hóa việc lưu trữ chuỗi ngắnbầu cua, giảm thiểu sự xuất hiện của các mảnh vụn bộ nhớ (memory fragmentation). Khối bộ nhớ liên tục này bao gồm các phần sau: 1. Phần đầu tiên dành riêng cho cấu trúc dữ liệu robj, nơi lưu trữ thông tin cơ bản về đối tượng như kiểu dữ liệu, trạng thái, và các thuộc tính khác. 2. Tiếp theo là vùng dành riêng cho sds, nơi chứa chuỗi ký tự cần lưu trữ. Kích thước của phần này có thể thay đổi linh hoạt tùy theo độ dài của chuỗi. 3. Ngoài ra, còn có một vùng đệm nhỏ (padding) để đảm bảo việc canh chỉnh kích thước và sắp xếp bộ nhớ hợp lý, giúp tăng hiệu suất truy cập dữ liệu. Việc tổ chức như vậy không chỉ cải thiện hiệu suất mà còn giúp tối ưu hóa cách quản lý bộ nhớ trong hệ thống.

  • Kết cấu robj 16 byte.
  • Tiêu đề sdshdr8 3 byte.
  • Mảng sds ký tự tối đa 44 byte.
  • 1 ký tự NULL kết thúc.

Tổng cộng không vượt quá 64 byte (16 + 3 + 44 + 1)bầu cua, vì vậy chuỗi ngắn này hoàn toàn có thể được phân bổ trong một khối bộ nhớ 64 byte mà không gặp bất kỳ vấn đề nào.

Quy trình giải mã string robj

Khi chúng ta cần lấy giá trị của chuỗi789 Club, chẳng hạn như khi thực hiện lệnh get, chúng ta cần tiến hành các bước giải mã ngược lại so với quy trình mã hóa đã được đề cập trước đó. Quy trình này giúp chuyển đổi dữ liệu từ dạng đã được mã hóa trở về trạng thái ban đầu, cho phép chúng ta đọc và hiểu nội dung một cách chính xác.

Tâm điểm của quy trình giải mã nằm ở hàm getDecodedObject trong tệp object.c. Đây là phần mã cốt lõixem ngoại hạng anh, nơi thực hiện việc xử lý và trích xuất thông tin từ các đối tượng đã được mã hóa, đảm bảo dữ liệu được khôi phục chính xác như ban đầu. Hàm này đóng vai trò quan trọng trong việc duy trì tính toàn vẹn của toàn bộ hệ thống giải mã, cho phép các thao tác tiếp theo diễn ra suôn sẻ.

							
								
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
													
														robj
													 *
													getDecodedObject
													(
													robj
													 *
													o
													)
													 {
													
    robj
													 *
													dec
													;
													

    if
													 (
													sdsEncodedObject
													(
													o
													))
													 {
													
        incrRefCount
													(
													o
													);
													
        return
													 o
													;
													
    }
													
    if
													 (
													o
													->
													type
													 ==
													 OBJ_STRING
													 &&
													 o
													->
													encoding
													 ==
													 OBJ_ENCODING_INT
													)
													 {
													
        char
													 buf
													[
													32
													];
													

        ll2string
													(
													buf
													,
													32
													,(
													long
													)
													o
													->
													ptr
													);
													
        dec
													 =
													 createStringObject
													(
													buf
													,
													strlen
													(
													buf
													));
													
        return
													 dec
													;
													
    }
													 else
													 {
													
        serverPanic
													(
													"Unknown encoding type"
													);
													
    }
													
}
													

Quy trình này khá đơn giảnbầu cua, điểm cần chú ý có:

  • Các đối tượng robj có mã hóa là OBJ_ENCODING_RAW và OBJ_ENCODING_EMBSTR sẽ được trả về mà không có bất kỳ thay đổi nào789 Club, giữ nguyên như ban đầu. Từ góc nhìn của người sử dụng, hai loại mã hóa này không có sự khác biệt rõ rệt, vì cả hai đều được đóng gói bằng sds (simple dynamic string) ở bên trong. Điều này có nghĩa là, dù bạn đang làm việc với mã hóa nào, cách lưu trữ và xử lý dữ liệu bên trong sẽ giống nhau, mang lại trải nghiệm liền mạch cho người dùng.
  • Một chuỗi ký tự được mã hóa dưới dạng số và đối tượng robjxem ngoại hạng anh, khi chuyển đổi từ long trở lại thành chuỗi thập phân, sau đó sẽ được gọi hàm createStringObject để biểu diễn dưới dạng sds. Hãy lưu ý rằng: khi chuyển đổi từ long thành chuỗi sds, độ dài của chuỗi này chắc chắn không vượt quá 20 ký tự, và dựa trên cách triển khai của createStringObject, chúng sẽ luôn được mã hóa thành đối tượng OBJ_ENCODING_EMBSTR. Dưới đây là mã nguồn của hàm createStringObject: ```c robj *createStringObject(const char *ptr, size_t len) { robj *o = zmalloc(sizeof(robj)); o->type = OBJ_STRING; o->encoding = OBJ_ENCODING_EMBSTR; o->length = len; o->ptr = sdsnewlen(ptr, len); return o; } ``` Trong đoạn mã trên, hàm `zmalloc` được sử dụng để cấp phát bộ nhớ cho đối tượng robj, sau đó giá trị `ptr` (biểu diễn chuỗi đầu vào) và độ dài `len` được sao chép vào đối tượng mới. Đặc biệt, thuộc tính `encoding` được đặt thành `OBJ_ENCODING_EMBSTR`, cho phép tối ưu hóa bộ nhớ cho các chuỗi có độ dài nhỏ hơn hoặc bằng 39 byte (trong trường hợp này là 20 byte).
							
								
1
2
3
4
5
6
													
														robj
													 *
													createStringObject
													(
													const
													 char
													 *
													ptr
													,
													 size_t
													 len
													)
													 {
													
    if
													 (
													len
													 <=
													 OBJ_ENCODING_EMBSTR_SIZE_LIMIT
													)
													
        return
													 createEmbeddedStringObject
													(
													ptr
													,
													len
													);
													
    else
													
        return
													 createRawStringObject
													(
													ptr
													,
													len
													);
													
}
													

Nói thêm về mối quan hệ giữa sds và string

Trong bài viết trước789 Club, chúng ta đã sơ lược đề cập đến mối liên hệ giữa sds và string; sau khi bài viết này trình bày khái niệm về robj, chúng ta sẽ tổng hợp lại một lần nữa về mối quan hệ giữa sds và string. Sau khi hiểu rõ hơn về robj, chúng ta nhận ra rằng sds đóng vai trò như một công cụ cốt lõi trong việc quản lý chuỗi dữ liệ Robj là cấu trúc đại diện cho các kiểu đối tượng khác nhau mà Redis xử lý, bao gồm cả chuỗi. Khi nói về string, thực tế chúng ta đang nói đến cách sds được sử dụng để lưu trữ và thao tác với dữ liệu chuỗi bên trong các đối tượng robj. Tóm lại, sds không chỉ là nền tảng cơ bản để lưu trữ chuỗi trong Redis mà còn là yếu tố cốt yếu để đảm bảo hiệu suất cao của các hoạt động với chuỗi thông qua sự tích hợp chặt chẽ với robj. Điều này giúp tối ưu hóa bộ nhớ và tăng tốc độ xử lý dữ liệu, làm nổi bật vai trò quan trọng của sds trong hệ thống Redis.

  • Chính xác mà nói789 Club, trong Redis, string được biểu diễn bằng một robj.
  • Biến robj dùng để biểu diễn chuỗi có thể được mã hóa thành ba dạng biểu diễn nội bộ: OBJ_ENCODING_RAW789 Club, OBJ_ENCODING_EMBSTR và OBJ_ENCODING_INT. Trong đó, hai dạng mã hóa đầu tiên sử dụng sds (simple dynamic string) để lưu trữ dữ liệu, còn dạng mã hóa cuối cùng, OBJ_ENCODING_INT, sẽ trực tiếp lưu chuỗi dưới dạng kiểu số nguyên long. Điều này cho phép tối ưu hóa bộ nhớ và hiệu suất tùy thuộc vào loại dữ liệu cụ thể mà chuỗi cần lưu giữ.
  • Khi thực hiện các thao tác như tăng (incr) hoặc giảm (decr) trên chuỗi stringxem ngoại hạng anh, nếu bên trong nó được mã hóa theo OBJ_ENCODING_INT, Redis có thể thực hiện phép tính cộng trừ trực tiếp mà không cần bước chuyển đổi nào. Tuy nhiên, nếu chuỗi được mã hóa dưới dạng OBJ_ENCODING_RAW hoặc OBJ_ENCODING_EMBSTR, Redis sẽ cố gắng chuyển đổi chuỗi sds lưu trữ thành kiểu long trước khi thực hiện các phép toán số học. Nếu quá trình chuyển đổi này thành công, Redis mới tiến hành thực hiện các phép tính; ngược lại, nếu không thể chuyển đổi thành công, nó sẽ không thực hiện bất kỳ thao tác nào và trả về lỗi tương ứng.
  • Khi bạn thực hiện các lệnh như appendxem ngoại hạng anh, setbit và getrange đối với một chuỗi được biểu diễn dưới dạng long (kiểu số nguyên lớn), điều đó không ảnh hưởng đến giá trị long bên trong mà vẫn hoạt động trên giá trị chuỗi (dưới dạng chuỗi thập phân). Ví dụ, chuỗi "32", khi được xem như một mảng ký tự, bao gồm hai ký tự có mã ASCII lần lượt là 0x33 và 0x32. Khi bạn thực hiện lệnh setbit key 7 0, nó sẽ thay đổi ký tự 0x33 thành 0x32, khiến chuỗi trở thành "22". Tuy nhiên, nếu bạn giải thích chuỗi "32" dưới dạng long 64-bit, nó sẽ được hiểu là 0x0000000000000020. Khi áp dụng lệnh setbit vào trường hợp này, kết quả sẽ không chính xác. Do đó, trong quá trình thực hiện các lệnh này, hệ thống sẽ chuyển đổi giá trị long sang dạng chuỗi trước khi thực hiện các thao tác tương ứng. Vì lý do giới hạn về không gian, mã nguồn chi tiết của ba lệnh này sẽ không được trình bày ở đây. Những ai quan tâm có thể tham khảo mã nguồn gốc của Redis để tìm hiểu sâu hơn.
    • Hàm appendCommand trong t_string.c;
    • Hàm setbitCommand trong biops.c;
    • Hàm getrangeCommand trong t_string.c.

Đáng chú ý là trong việc thực hiện các lệnh append và setbitxem ngoại hạng anh, chúng đều cuối cùng sẽ gọi đến hàm dbUnshareStringValue trong tệp db.c. Hàm này chuyển đổi mã hóa nội bộ của đối tượng chuỗi thành OBJ_ENCODING_RAW (chỉ có các đối tượng robj có mã hóa này mới cho phép thêm nội dung mới vào sds bên trong một cách tự do sau này) và giải phóng trạng thái chia sẻ đối tượng nếu nó tồn tại trước đó. Quá trình này cũng sử dụng hàm getDecodedObject mà đã được đề cập trước đó. Trong quá trình này, hàm getDecodedObject đảm bảo rằng mọi đối tượng cần thiết đều ở dạng giải mã đầy đủ trước khi tiếp tục xử lý. Điều này rất quan trọng để tránh các vấn đề về tính nhất quán dữ liệu khi thực hiện thao tác trên cơ sở dữ liệu. Hơn nữa, việc chuyển đổi sang OBJ_ENCODING_RAW không chỉ giúp tối ưu hóa hiệu suất mà còn tăng cường khả năng mở rộng của dữ liệu trong tương lai.

							
								
1
2
3
4
5
6
7
8
9
10
													
														robj
													 *
													dbUnshareStringValue
													(
													redisDb
													 *
													db
													,
													 robj
													 *
													key
													,
													 robj
													 *
													o
													)
													 {
													
    serverAssert
													(
													o
													->
													type
													 ==
													 OBJ_STRING
													);
													
    if
													 (
													o
													->
													refcount
													 !=
													 1
													 ||
													 o
													->
													encoding
													 !=
													 OBJ_ENCODING_RAW
													)
													 {
													
        robj
													 *
													decoded
													 =
													 getDecodedObject
													(
													o
													);
													
        o
													 =
													 createRawStringObject
													(
													decoded
													->
													ptr
													,
													 sdslen
													(
													decoded
													->
													ptr
													));
													
        decrRefCount
													(
													decoded
													);
													
        dbOverwrite
													(
													db
													,
													key
													,
													o
													);
													
    }
													
    return
													 o
													;
													
}
													

Thao tác đếm tham chiếu robj

Các thao tác tăng và giảm đếm tham chiếu robj được định nghĩ c:

							
								
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
													
														void
													 incrRefCount
													(
													robj
													 *
													o
													)
													 {
													
    o
													->
													refcount
													++
													;
													
}
													

void
													 decrRefCount
													(
													robj
													 *
													o
													)
													 {
													
    if
													 (
													o
													->
													refcount
													 <=
													 0
													)
													 serverPanic
													(
													"decrRefCount against refcount <= 0"
													);
													
    if
													 (
													o
													->
													refcount
													 ==
													 1
													)
													 {
													
        switch
													(
													o
													->
													type
													)
													 {
													
        case
													 OBJ_STRING
													:
													 freeStringObject
													(
													o
													);
													 break
													;
													
        case
													 OBJ_LIST
													:
													 freeListObject
													(
													o
													);
													 break
													;
													
        case
													 OBJ_SET
													:
													 freeSetObject
													(
													o
													);
													 break
													;
													
        case
													 OBJ_ZSET
													:
													 freeZsetObject
													(
													o
													);
													 break
													;
													
        case
													 OBJ_HASH
													:
													 freeHashObject
													(
													o
													);
													 break
													;
													
        default:
													 serverPanic
													(
													"Unknown object type"
													);
													 break
													;
													
        }
													
        zfree
													(
													o
													);
													
    }
													 else
													 {
													
        o
													->
													refcount
													--
													;
													
    }
													
}
													

Chúng ta hãy cùng tập trung vào hoạt động giảm tham chiếubầu cua, được gọi là Khi tham chiếu cuối cùng còn lại (tức refcount đã giảm xuống 1), sau khi decrRefCount được thực hiện, toàn bộ đối tượng robj sẽ bị giải phóng hoàn toàn khỏi bộ nhớ. Điều này có nghĩa là không chỉ giá trị của nó bị xóa mà cả cấu trúc lưu trữ dữ liệu cũng bị loại bỏ, đảm bảo tài nguyên được quản lý một cách hiệu quả và sạch sẽ.

Lưu ý: Lệnh del trong Redis dựa vào hoạt động giảm tham chiếu (decrRefCount) để giải phóng giá trị (value) liên quan. Khi thực hiện lệnh nàyxem ngoại hạng anh, hệ thống sẽ giảm số lần tham chiếu của giá trị xuống 0, từ đó tự động xóa bỏ dữ liệu khỏi bộ nhớ nếu không còn đối tượng nào khác đang sử dụng nó. Điều này giúp tối ưu hóa hiệu suất và quản lý tài nguyên một cách hiệu quả trong cơ sở dữ liệu Redis.


Dựa trên những gì đã thảo luận trong bài viết nàyxem ngoại hạng anh, chúng ta có thể dễ dàng nhận ra rằng robj đại diện cho cấu trúc dữ liệu đầu tiên mà Redis công khai ra bên ngoài: string, list, hash, set và Mỗi loại cấu trúc dữ liệu ở cấp độ này sẽ liên kết với một hoặc nhiều cấu trúc dữ liệu thứ hai ở cấp độ sâu hơn (như dict, sds, ziplist, quicklist, skiplist, v.v.) thông qua các phương thức mã hóa khác nhau. Có thể nói rằng, robj chính là cây cầu nối giữa hai cấp độ cấu trúc dữ liệu này. Ngoài ra, việc sử dụng robj không chỉ giúp Redis tối ưu hóa hiệu suất mà còn mang lại khả năng linh hoạt trong việc chọn lựa phương án lưu trữ phù hợp nhất với yêu cầu cụ thể của từng trường hợp. Điều này làm nổi bật vai trò quan trọng của robj như một phần không thể thiếu trong cơ chế hoạt động của Redis.

Bài viết này sẽ đi sâu vào tìm hiểu chi tiết về việc triển khai của đối tượng chuỗi kiểu OBJ_ Quy trình mã hóa và giải mã của loại đối tượng này đóng vai trò rất quan trọng và được sử dụng rộng rãi trong hệ thống Redis. Chúng ta có thể còn gặp lại các khái niệm này trong những cuộc thảo luận tiếp theo. Hiện tạixem ngoại hạng anh, khi đã nắm rõ khái niệm cơ bản về robj, bài viết kế tiếp chúng ta sẽ bàn về ziplist và mối liên hệ giữa nó với kiểu dữ liệ


Kết luận mã hóa chuỗi thành kiểu long commit f648c5a


Bài viết gốc789 Club, xin vui lòng trích dẫn nguồn và bao gồm mã QR bên dưới! Nếu không, từ chối tái bản!
Liên kết bài viết: /w8pksnr4.html
Hãy theo dõi tài khoản Weibo cá nhân của tôi: Tìm kiếm tên tôi "Trương Thiết Lệ" trên Weibo.
Tài khoản WeChat của tôi: tielei-blog (Trương Thiết Lệ)
Bài trước: Phân tích sâu cấu trúc dữ liệu bên trong Redis (2) —— sds
Bài sau: Con gái và những cuốn truyện tranh của nó

Bài viết mới nhất