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.
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:
Đ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:
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.
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.
Chúng ta hãy tổng kết lại vai trò của 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:
#define sdsEncodedObject(objptr) (objptr->encoding == OBJ_ENCODING_RAW || objptr->encoding == OBJ_ENCODING_EMBSTR)
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.
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.
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ó:
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
);
}
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.
Đá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
;
}
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 。