Phân tích cấu trúc dữ liệu nội bộ của Redis Phân tích cấu trúc dữ liệu nội bộ của Redis Bài viết thứ hai trong loạt bài này99win club, sẽ nói về cấu trúc dữ liệu cơ bản được sử dụng nhiều nhất trong Redis: sds.
Dù trong bất kỳ ngôn ngữ lập trình nào99win club, chuỗi (string) cũng luôn là một trong những cấu trúc dữ liệu được sử dụng phổ biến nhất. Ở Redis, sds (Simple Dynamic String) chính là phiên bản chuỗi được áp dụng rộng rãi. Tên đầy đủ của nó là So với các loại chuỗi trong môi trường lập trình khác, sds có những đặc điểm nổi bật sau đây: Trước hết, sds không chỉ đơn thuần là một chuỗi cố định mà còn có khả năng mở rộng linh hoạt dựa trên nhu cầu lưu trữ. Điều này giúp tối ưu hóa bộ nhớ và cải thiện hiệu suất khi làm việc với các dữ liệu thay đổi kích thước liên tục. Hơn nữa, sds tích hợp sẵn các phương thức quản lý bộ nhớ thông minh, cho phép tự động điều chỉnh kích thước khi cần thêm hoặc xóa dữ liệu. Thứ hai, sds cung cấp khả năng tương thích cao với nhiều loại mã hóa khác nhau, từ UTF-8 đến Latin1. Điều này đảm bảo rằng mọi ký tự đặc biệt hoặc đa byte đều có thể được xử lý một cách chính xác và an toà Cuối cùng, sds mang lại sự ổn định và hiệu quả trong quá trình truy xuất dữ liệu nhờ cơ chế tối ưu hóa bộ đệm và giảm thiểu sự gián đoạn khi thực hiện thao tác với chuỗi. Điều này đặc biệt hữu ích trong các ứng dụng đòi hỏi tốc độ xử lý cao và khả năng mở rộng lớn.
Khi đọc đến đâytỷ lệ kèo bóng đá trực tiếp, nhiều bạn đã quen thuộc với Redis có thể đã đặt ra một câu hỏi: Redis đã cung cấp một cấu trúc dữ liệu chuỗi gọi là string, vậy sds được đề cập ở đây và string có mối liên hệ gì với nhau? Có người có thể đoán rằng string được xây dựng dựa trên sds. Giả thuyết này đã rất gần với sự thật, nhưng vẫn chưa hoàn toàn chính xác trong cách diễn đạt. Về mối liên hệ chi tiết giữa string và sds, chúng ta sẽ bàn luận sâu hơn ở phần sau. Hiện tại, để tiện cho việc thảo luận, hãy tạm thời hiểu đơn giản rằng thực hiện cơ bản của string là sds. Có lẽ bạn đang tự hỏi tại sao lại có hai cấu trúc dữ liệu giống nhau như vậy? Điều này xuất phát từ nhu cầu khác nhau trong việc quản lý bộ nhớ và hiệu suất. String trong Redis được thiết kế để dễ sử dụng và linh hoạt hơn, trong khi sds lại tập trung vào việc tối ưu hóa hiệu suất và giảm thiểu rủi ro về bộ nhớ. Hai cấu trúc này bổ sung cho nhau thay vì mâu thuẫn với nhau, giúp Redis trở nên mạnh mẽ và đa năng hơn trong việc xử lý dữ liệu.
Xin chào Thế giới
Tất cả các thao tác này đều rất đơn giản99win club, chúng ta sẽ giải thích sơ lược một chút:
Việc thực hiện các lệnh này có một phần liên quan đến việc triển khai của sds. Bây giờ chúng ta bắt đầu phân tích chi tiết.
Chúng ta đều biết rằng trong ngôn ngữ lập trình C99win club, chuỗi ký tự được lưu trữ dưới dạng mảng ký tự kết thúc bằng ký tự '\0' (ký hiệu kết thúc chuỗi). Chúng thường được biểu diễn dưới dạng con trỏ ký tự (char *). Tuy nhiên, do không cho phép xuất hiện byte 0 ở giữa chuỗi, nên định dạng này không thể dùng để lưu trữ bất kỳ dữ liệu nhị phân nào một cách trực tiếp. Điều này đặt ra giới hạn nhất định khi xử lý các loại dữ liệu phức tạp hoặc dữ liệu thô mà có thể chứa các giá trị đặc biệt như byte 0.
Chúng ta có thể tìm thấy định nghĩa kiểu sds trong tệp sds.h:
typedef
char
*
sds
;
Chắc chắn đã có người cảm thấy bối rối rồikeo 88, liệu sds có thực sự giống với char * hay không? Chúng ta đã từng đề cập trước đây rằng sds và chuỗi C truyền thống có tính tương thích về kiểu dữ liệu, do đó cách định nghĩa kiểu của chúng là như nhau, đều là char *. Trong một số trường hợp nhất định, khi cần truyền vào một chuỗi C, bạn thực sự cũng có thể truyền một sds. Tuy nhiên, sds và char * không phải là hoàn toàn giống nhau. SDS được thiết kế để an toàn cho dữ liệu nhị phân (Binary Safe), nó có thể lưu trữ bất kỳ dữ liệu nhị phân nào, khác với chuỗi C mà chỉ kết thúc bằng ký tự '\0'. Do đó, sds chắc chắn phải có một trường độ dài. Nhưng trường độ dài này nằm ở đâu? Thực tế, sds còn chứa một cấu trúc header:
struct
__attribute__
((
__packed__
))
sdshdr5
{
unsigned
char
flags
;
/* 3 lsb of typekeo 88, and 5 msb of string length */
char
buf
[];
};
struct
__attribute__
((
__packed__
))
sdshdr8
{
uint8_t
len
;
/* used */
uint8_t
alloc
;
/* excluding the header and null terminator */
unsigned
char
flags
;
/* 3 lsb of typekeo 88, 5 unused bits */
char
buf
[];
};
struct
__attribute__
((
__packed__
))
sdshdr16
{
uint16_t
len
;
/* used */
uint16_t
alloc
;
/* excluding the header and null terminator */
unsigned
char
flags
;
/* 3 lsb of typekeo 88, 5 unused bits */
char
buf
[];
};
struct
__attribute__
((
__packed__
))
sdshdr32
{
uint32_t
len
;
/* used */
uint32_t
alloc
;
/* excluding the header and null terminator */
unsigned
char
flags
;
/* 3 lsb of typetỷ lệ kèo bóng đá trực tiếp, 5 unused bits */
char
buf
[];
};
struct
__attribute__
((
__packed__
))
sdshdr64
{
uint64_t
len
;
/* used */
uint64_t
alloc
;
/* excluding the header and null terminator */
unsigned
char
flags
;
/* 3 lsb of typekeo 88, 5 unused bits */
char
buf
[];
};
Sds (Simple Dynamic String) có tất cả 5 loại header khác nhau. Lý do có đến 5 loại header là để các chuỗi với độ dài khác nhau có thể sử dụng kích thước header phù hợp. Điều này cho phép chuỗi ngắn chỉ cần dùng header nhỏ hơntỷ lệ kèo bóng đá trực tiếp, từ đó tối ưu hóa việc sử dụng bộ nhớ và giảm thiểu tài nguyên cần thiết.
Một chuỗi sds đầy đủ có cấu trúc hoàn chỉnhtỷ lệ kèo bóng đá trực tiếp, bao gồm hai phần liền kề nhau trong địa chỉ bộ nhớ:
Ngoài sdshdr5 ra99win club, các header khác đều chứa ba trường:
#define SDS_TYPE_5 0
#define SDS_TYPE_8 1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
Chúng ta cần phân tích cẩn thận cấu trúc dữ liệu sds.
Hình ảnh phía trên là một ví dụ về cấu trúc nội bộ của SDS. Trong hìnhtỷ lệ kèo bóng đá trực tiếp, chúng ta có thể thấy cấu trúc bộ nhớ của hai chuỗi SDS được đặt tên là s1 và s2. Trong đó, một chuỗi sử dụng header kiểu sdshdr8, trong khi chuỗi kia sử dụng header kiểu sdshdr16. Tuy nhiên, cả hai đều biểu diễn cùng một giá trị chuỗi có độ dài 6: "tielei". Tiếp theo, chúng ta sẽ đi sâu vào phân tích từng phần của mã nguồn để hiểu rõ hơn về cách thức hoạt động của các thành phần này. Trước tiên, hãy cùng tìm hiểu khái niệm cơ bản về việc quản lý bộ nhớ Khi tạo ra một chuỗi mới, SDS cần phải xác định kích thước phù hợp cho header để lưu trữ thông tin về chiều dài của chuỗi. Header kiểu sdshdr8 chỉ đủ sức lưu trữ các chuỗi có chiều dài tối đa 255 byte (2^8 - 1), trong khi header sdshdr16 hỗ trợ đến 65535 byte (2^16 - 1). Điều này giúp SDS linh hoạt hơn trong việc xử lý các trường hợp khác nhau liên quan đến kích thước dữ liệu. Tiếp theo, hãy xem xét cách mà chuỗi "tielei" được lưu trữ trong từng loại header. Đối với header sdshdr8, phần đầu tiên của vùng nhớ sẽ chứa giá trị 6 - đại diện cho chiều dài của chuỗi. Ngay sau đó là vùng dữ liệu thực sự của chuỗi, nơi chứa các ký tự 't', 'i', 'e', 'l', 'e', 'i'. Với header sdshdr16, quy trình tương tự cũng diễn ra, nhưng header này có thêm không gian dự phòng để hỗ trợ các chuỗi lớn hơn. Bây giờ, hãy nhìn vào mã nguồn cụ thể để hiểu rõ hơn về cách SDS hoạt động. Đầu tiên, khi một chuỗi mới được tạo, hàm `sdsnew` hoặc `sdsempty` sẽ được gọi để khởi tạo một đối tượng SDS với header phù hợp. Nếu chiều dài của chuỗi nằm trong phạm vi cho phép của sdshdr8, thì header này sẽ được chọn; nếu vượt quá giới hạn, SDS sẽ tự động nâng cấp lên sdshdr16. Điều này giúp tối ưu hóa hiệu suất và giảm thiểu lãng phí tài nguyên. Cuối cùng, chúng ta có thể thấy rằng SDS không chỉ đơn giản là một công cụ quản lý chuỗi, mà còn là một hệ thống thông minh có khả năng tự điều chỉnh dựa trên yêu cầu thực tế. Bằng cách sử dụng các header khác nhau tùy thuộc vào kích thước của chuỗi, SDS có thể cung cấp hiệu suất tốt nhất cho mọi tình huống, từ các chuỗi ngắn gọn đến các chuỗi phức tạp với độ dài lớn hơn nhiều.
Trong sdstỷ lệ kèo bóng đá trực tiếp, con trỏ ký tự (như s1 và s2) chính là nơi mà dữ liệu thực tế (mảng ký tự) được bắt đầu, trong khi phần header lại nằm ở hướng địa chỉ bộ nhớ thấp hơn. Trong tệp sds.h, có một số macro định nghĩa liên quan đến việc phân tích phần header này, giúp người dùng có thể dễ dàng thao tác với cấu trúc dữ liệu của sds mà không cần phải trực tiếp làm việc với các giá trị bộ nhớ thô. Điều thú vị là mỗi khi bạn tạo ra một chuỗi sds mới, hệ thống sẽ tự động thêm vào một phần header nhỏ để lưu trữ thông tin về độ dài của chuỗi, dung lượng tối đa, cũng như các cờ hiệu ứng đặc biệt. Điều này cho phép sds hoạt động nhanh chóng và linh hoạt trong nhiều tình huống khác nhau. Nhờ có nhữ h, lập trình viên có thể dễ dàng truy cập vào các thông tin quan trọng này mà không cần phải viết thêm nhiều dòng mã phức tạp.
#define SDS_TYPE_MASK 7
#define SDS_TYPE_BITS 3
#define SDS_HDR_VAR(T99win club,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)
Trong đótỷ lệ kèo bóng đá trực tiếp, SDS_HDR được sử dụng để lấy vị trí con trỏ bắt đầu của phần header từ chuỗi sds. Ví dụ như SDS_HDR(8, s1) sẽ biểu thị con trỏ header của s1, còn SDS_HDR(16, s2) sẽ đại diện cho con trỏ header của s2. Điều này giúp người dùng dễ dàng thao tác và truy xuất thông tin header một cách nhanh chóng và chính xác trong các cấu trúc dữ liệu sds.
Trước khi sử dụng SDS_HDRkeo 88, chúng ta cần xác định chính xác loại header mà nó đang đề cập đến. Điều này giúp chúng ta biết tham số đầu tiên của SDS_HDR nên được truyền vào với giá trị nào. Cách để lấy thông tin về loại header từ con trỏ chuỗi sds là di chuyển về hướng địa chỉ thấp hơn một byte để lấy giá trị của trường flags. Ví dụ, bằng cách sử dụng s1[-1] và s2[-1], chúng ta có thể lấy giá trị flags của s1 và s2. Sau đó, từ giá trị flags, chúng ta lấy 3 bit thấp nhất để xác định loại header. Những bước đơn giản này đảm bảo rằng việc sử dụng SDS_HDR sẽ đúng với mục đích và tránh được các lỗi không đáng có trong quá trình xử lý.
Với pointer headerkeo 88, chúng ta có thể nhanh chóng xác định các trường len và alloc:
Trong các định nghĩa kiểu header99win club, còn có một số điểm cần chú ý:
Cho đến thời điểm này99win club, chúng ta đã rất rõ ràng nhận ra rằng: phần header của chuỗi sds thực tế được ẩn giấu phía trước dữ liệu chuỗi thực sự (theo hướng địa chỉ thấp hơn). Định nghĩa như vậy mang lại một số lợi ích nổi bật sau đây: Thứ nhất, việc đặt header ở vị trí dễ tiếp cận giúp thao tác với dữ liệu trở nên nhanh chóng và hiệu quả. Điều này đặc biệt hữu ích khi cần xử lý hoặc cập nhật thông tin liên quan đến chuỗi trong quá trình chạy chương trình. Thứ hai, cách sắp xếp này cho phép tối ưu hóa bộ nhớ. Header có kích thước nhỏ gọn, nhưng vẫn chứa đầy đủ thông tin cần thiết về độ dài và trạng thái của chuỗi, từ đó giảm thiểu đáng kể lượng bộ nhớ tiêu tốn không cần thiết. Cuối cùng, định nghĩa này tạo điều kiện thuận lợi cho việc mở rộng hoặc thu nhỏ chuỗi mà không cần phải di chuyển toàn bộ dữ liệu. Chỉ cần thay đổi giá trị trong header là đã có thể điều chỉnh kích thước chuỗi một cách linh hoạt. Tóm lại, việc sắp xếp header trước dữ liệu chuỗi không chỉ mang lại lợi ích về hiệu suất mà còn giúp tối ưu hóa quản lý bộ nhớ một cách thông minh.
Khi đã hiểu rõ cấu trúc dữ liệu sds99win club, các hàm thao tác cụ thể sẽ dễ hiểu hơn.
Ở đây chúng ta chọn mã nguồn của sdslen và sdsReqType để xem xét.
static
inline
size_t
sdslen
(
const
sds
s
)
{
unsigned
char
flags
=
s
[
-
1
];
switch
(
flags
&
SDS_TYPE_MASK
)
{
case
SDS_TYPE_5
:
return
SDS_TYPE_5_LEN
(
flags
);
case
SDS_TYPE_8
:
return
SDS_HDR
(
8
,
s
)
->
len
;
case
SDS_TYPE_16
:
return
SDS_HDR
(
16
,
s
)
->
len
;
case
SDS_TYPE_32
:
return
SDS_HDR
(
32
,
s
)
->
len
;
case
SDS_TYPE_64
:
return
SDS_HDR
(
64
,
s
)
->
len
;
}
return
0
;
}
static
inline
char
sdsReqType
(
size_t
string_size
)
{
if
(
string_size
<
1
<<
5
)
return
SDS_TYPE_5
;
if
(
string_size
<
1
<<
8
)
return
SDS_TYPE_8
;
if
(
string_size
<
1
<<
16
)
return
SDS_TYPE_16
;
if
(
string_size
<
1ll
<<
32
)
return
SDS_TYPE_32
;
return
SDS_TYPE_64
;
}
Tương tự như phân tích trước đótỷ lệ kèo bóng đá trực tiếp, sdslen đầu tiên sử dụng s[-1] để dịch chuyển về phía địa chỉ thấp hơn một byte, từ đó lấy được giá trị flags; tiếp theo, nó thực hiện phép AND theo vị trí với SDS_TYPE_MASK để xác định loại header; sau đó, dựa trên loại header khác nhau, nó gọi hàm SDS_HDR để lấy con trỏ bắt đầu của header, từ đó có thể thu được giá trị của trường len. Quá trình này cho thấy cách mà cơ chế quản lý bộ nhớ trong cấu trúc dữ liệu này hoạt động một cách linh hoạt và hiệu quả.
Qua mã nguồn của sdsReqTypetỷ lệ kèo bóng đá trực tiếp, dễ dàng nhận thấy:
Về phần mã thực hiện của sdsReqType99win club, từ phiên bản 3.2.0 trở về trước, nó luôn gặp phải vấn đề liên quan đến giá trị giới hạn độ dài. Tuy nhiên, gần đây, trên nhánh 3.2 đã có những cải tiến đáng kể để giải quyết triệt để vấn đề này. commit 6032340 Đã sửa chữa.
sds
sdsnewlen
(
const
void
*
init
,
size_t
initlen
)
{
void
*
sh
;
sds
s
;
char
type
=
sdsReqType
(
initlen
);
/* Empty strings are usually created in order to append. Use type 8
* since type 5 is not good at this. */
if
(
type
==
SDS_TYPE_5
&&
initlen
==
0
)
type
=
SDS_TYPE_8
;
int
hdrlen
=
sdsHdrSize
(
type
);
unsigned
char
*
fp
;
/* flags pointer. */
sh
=
s_malloc
(
hdrlen
+
initlen
+
1
);
if
(
!
init
)
memset
(
sh
,
0
,
hdrlen
+
initlen
+
1
);
if
(
sh
==
NULL
)
return
NULL
;
s
=
(
char
*
)
sh
+
hdrlen
;
fp
=
((
unsigned
char
*
)
s
)
-
1
;
switch
(
type
)
{
case
SDS_TYPE_5
:
{
*
fp
=
type
|
(
initlen
<<
SDS_TYPE_BITS
);
break
;
}
case
SDS_TYPE_8
:
{
SDS_HDR_VAR
(
8
,
s
);
sh
->
len
=
initlen
;
sh
->
alloc
=
initlen
;
*
fp
=
type
;
break
;
}
case
SDS_TYPE_16
:
{
SDS_HDR_VAR
(
16
,
s
);
sh
->
len
=
initlen
;
sh
->
alloc
=
initlen
;
*
fp
=
type
;
break
;
}
case
SDS_TYPE_32
:
{
SDS_HDR_VAR
(
32
,
s
);
sh
->
len
=
initlen
;
sh
->
alloc
=
initlen
;
*
fp
=
type
;
break
;
}
case
SDS_TYPE_64
:
{
SDS_HDR_VAR
(
64
,
s
);
sh
->
len
=
initlen
;
sh
->
alloc
=
initlen
;
*
fp
=
type
;
break
;
}
}
if
(
initlen
&&
init
)
memcpy
(
s
,
init
,
initlen
);
s
[
initlen
]
=
'\0'
;
return
s
;
}
sds
sdsempty
(
void
)
{
return
sdsnewlen
(
""
,
0
);
}
sds
sdsnew
(
const
char
*
init
)
{
size_t
initlen
=
(
init
==
NULL
)
?
0
:
strlen
(
init
);
return
sdsnewlen
(
init
,
initlen
);
}
void
sdsfree
(
sds
s
)
{
if
(
s
==
NULL
)
return
;
s_free
((
char
*
)
s
-
sdsHdrSize
(
s
[
-
1
]));
}
sdsnewlen tạo ra một chuỗi sds có độ dài là initlen và sử dụng mảng ký tự được chỉ bởi init (dữ liệu nhị phân bất kỳ) để khởi tạo dữ liệu. Nếu init là NULL99win club, dữ liệu sẽ được khởi tạo bằng tất cả các giá trị 0. Trong quá trình thực hiện, điều quan trọng cần lưu ý là: - Chúng ta phải đảm bảo rằng mảng nguồn được cấp phát hợp lệ và không vượt quá giới hạn bộ nhớ. - Nếu init là NULL, cần kiểm tra xem kích thước bộ nhớ yêu cầu có phù hợp hay không trước khi gán giá trị 0 cho toàn bộ dữ liệu. - Khi sao chép dữ liệu từ mảng nguồn sang chuỗi sds, cần thực hiện cẩn thận để tránh tình trạng tràn bộ nhớ hoặc mất mát thông tin. - Cần tối ưu hóa việc quản lý bộ nhớ để tăng hiệu suất của hàm, chẳng hạn như sử dụng các kỹ thuật tái sử dụng vùng nhớ khi có thể. Đây là những yếu tố cơ bản giúp đảm bảo tính ổn định và hiệu quả khi sử dụng hàm này trong các ứng dụng thực tế.
Khi làm việc với sdsfree99win club, điều quan trọng cần lưu ý là bạn phải giải phóng toàn bộ bộ nhớ, vì vậy trước tiên bạn cần tính toán và xác định vị trí con trỏ đầu của vùng header. Sau đó, hãy truyền con trỏ này vào hàm s_free để thực hiện việc giải phóng. Con trỏ này chính là địa chỉ mà hàm s_malloc trả về khi được gọi trong hà Việc xác định đúng con trỏ sẽ giúp quá trình quản lý bộ nhớ diễn ra chính xác và hiệu quả hơn.
sds
sdscatlen
(
sds
s
,
const
void
*
t
,
size_t
len
)
{
size_t
curlen
=
sdslen
(
s
);
s
=
sdsMakeRoomFor
(
s
,
len
);
if
(
s
==
NULL
)
return
NULL
;
memcpy
(
s
+
curlen
,
t
,
len
);
sdssetlen
(
s
,
curlen
+
len
);
s
[
curlen
+
len
]
=
'\0'
;
return
s
;
}
sds
sdscat
(
sds
s
,
const
char
*
t
)
{
return
sdscatlen
(
s
,
t
,
strlen
(
t
));
}
sds
sdscatsds
(
sds
s
,
const
sds
t
)
{
return
sdscatlen
(
s
,
t
,
sdslen
(
t
));
}
sds
sdsMakeRoomFor
(
sds
s
,
size_t
addlen
)
{
void
*
sh
,
*
newsh
;
size_t
avail
=
sdsavail
(
s
);
size_t
len
,
newlen
;
char
type
,
oldtype
=
s
[
-
1
]
&
SDS_TYPE_MASK
;
int
hdrlen
;
/* Return ASAP if there is enough space left. */
if
(
avail
>=
addlen
)
return
s
;
len
=
sdslen
(
s
);
sh
=
(
char
*
)
s
-
sdsHdrSize
(
oldtype
);
newlen
=
(
len
+
addlen
);
if
(
newlen
<
SDS_MAX_PREALLOC
)
newlen
*=
2
;
else
newlen
+=
SDS_MAX_PREALLOC
;
type
=
sdsReqType
(
newlen
);
/* Don't use type 5: the user is appending to the string and type 5 is
* not able to remember empty spacekeo 88, so sdsMakeRoomFor() must be called
* at every appending operation. */
if
(
type
==
SDS_TYPE_5
)
type
=
SDS_TYPE_8
;
hdrlen
=
sdsHdrSize
(
type
);
if
(
oldtype
==
type
)
{
newsh
=
s_realloc
(
sh
,
hdrlen
+
newlen
+
1
);
if
(
newsh
==
NULL
)
return
NULL
;
s
=
(
char
*
)
newsh
+
hdrlen
;
}
else
{
/* Since the header size changestỷ lệ kèo bóng đá trực tiếp, need to move the string forward,
* and can't use realloc */
newsh
=
s_malloc
(
hdrlen
+
newlen
+
1
);
if
(
newsh
==
NULL
)
return
NULL
;
memcpy
((
char
*
)
newsh
+
hdrlen
,
s
,
len
+
1
);
s_free
(
sh
);
s
=
(
char
*
)
newsh
+
hdrlen
;
s
[
-
1
]
=
type
;
sdssetlen
(
s
,
len
);
}
sdssetalloc
(
s
,
newlen
);
return
s
;
}
Hàm sdscatlen sẽ thêm dữ liệu nhị phân có độ dài len từ con trỏ t vào cuối chuỗi sds của biến s. Điều thú vị là lệnh append được trình diễn ở phần đầu bài viết cũng chính là thực hiện thông qua việc gọi hàm sdscatlen bên trong. Hàm này đóng vai trò như một công cụ nền tảng để mở rộng chuỗi bằng cách gắn thêm nội dung mới vào sau nó.
Trong quá trình triển khai của sdscatlen99win club, trước tiên hàm sdsMakeRoomFor sẽ được gọi để đảm bảo chuỗi s có đủ không gian để thêm dữ liệu có độ dài bằng len. Hàm sdsMakeRoomFor có thể sẽ phân bổ bộ nhớ mới hoặc cũng có thể không cần làm điều đó nếu đã có đủ không gian sẵn có trong bộ nhớ hiện tại. Nếu không gian hiện tại không đủ, hàm này sẽ thực hiện việc mở rộng vùng nhớ và tạo ra một khoảng trống để chuẩn bị cho việc thêm dữ liệu mà không làm gián đoạn các hoạt động đang diễn ra trên chuỗi gốc. Điều này giúp tối ưu hóa hiệu suất khi thao tác với chuỗi và tránh những trường hợp gây lỗi như vượt quá giới hạn bộ nhớ. Ngược lại, nếu bộ nhớ hiện tại đã đủ lớn, hàm sẽ không thực hiện bất kỳ hành động nào, giảm thiểu sự tiêu tốn tài nguyên hệ thống.
Hàm MakeRoomFor đóng vai trò cực kỳ quan trọng trong việc triển khai của sds (simple dynamic string). Khi tìm hiểu mã nguồn thực hiện của hàm này99win club, có một số điểm cần đặc biệt chú ý: Trước tiên, mục tiêu chính của MakeRoomFor là đảm bảo rằng chuỗi hiện tại có đủ dung lượng để thêm vào một số ký tự mà không cần phải thực hiện việc cấp phát bộ nhớ mới ngay lập tức. Điều này giúp tối ưu hóa hiệu suất và giảm thiểu các hoạt động tốn kém về mặt tài nguyên. Thứ hai, hàm này thường sẽ kiểm tra xem còn bao nhiêu byte trống trong vùng nhớ hiện tại so với yêu cầu thực tế. Nếu dung lượng không đủ, nó sẽ thực hiện việc mở rộng bộ nhớ theo cách an toàn, chẳng hạn như nhân đôi kích thước hiện tại hoặc sử dụng giá trị tối thiểu đảm bảo yêu cầu bổ sung. Cuối cùng, điều quan trọng là phải hiểu rằng MakeRoomFor không chỉ đơn thuần là mở rộng bộ nhớ mà còn giữ nguyên phần dữ liệu hiện tại để tránh mất mát thông tin. Đây là bước quan trọng trong quá trình quản lý bộ nhớ động của sds.
Từ giao diện của hàm sdscatlentỷ lệ kèo bóng đá trực tiếp, chúng ta có thể nhận ra một mô hình sử dụng: khi gọi hàm này, cần truyền vào một biến sds cũ và nó sẽ trả về một biến sds mới. Do cách thực hiện nội bộ có thể dẫn đến thay đổi địa chỉ, nên sau khi gọi hàm, biến cũ trước đó sẽ trở nên không còn hiệu lực nữa, và tất cả các thao tác tiếp theo phải sử dụng biến mới được trả về thay thế. Không chỉ riêng hàm sdscatlen, mà các hàm khác trong thư viện sds (như sdscpy, sdstrim, sdsjoin,...), cũng như một số cấu trúc dữ liệu trong Redis có khả năng mở rộng bộ nhớ tự động (như ziplist) đều tuân theo mô hình sử dụng tương tự. Hơn nữa, việc này cho phép tối ưu hóa hiệu suất và quản lý bộ nhớ linh hoạt hơn. Khi sử dụng các hàm này, người lập trình cần luôn chú ý cập nhật biến tham chiếu sau mỗi lần gọi để tránh các lỗi liên quan đến việc sử dụng dữ liệu đã bị hủy. Điều này đặc biệt quan trọng trong các ứng dụng đòi hỏi tính ổn định cao hoặc làm việc với lượng lớn dữ liệu. Nhờ thiết kế này, cả sds lẫn Redis đều có thể đảm bảo hiệu suất tốt nhất trong mọi tình huống vận hành.
Bây giờ chúng ta quay lại xem ví dụ về thao tác chuỗi ở phần đầu bài viết.
Tuy nhiên99win club, bên cạnh việc hỗ trợ các thao tác cơ bản, string còn có thể thực hiện các hoạt động như incr (tăng giá trị) và decr (giảm giá trị) khi giá trị được lưu trữ là một số. Vậy, khi string lưu trữ giá trị số, liệu cấu trúc lưu trữ bên trong vẫn là sds (Simple Dynamic String) không? Thực tế, nó đã thay đổi. Đồng thời, trong trường hợp này, cách thức hoạt động của setbit và getrange cũng sẽ khác biệt. Những chi tiết này, chúng ta sẽ cùng tìm hiểu một cách hệ thống hơn khi đề cập đến robj trong bài viết tiếp theo.