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 viết này là phần thứ sáu trong loạt bài của chúng ta. Trong nội dung hôm naybacarat, chúng ta sẽ đi sâu tìm hiểu về một cấu trúc dữ liệu nội bộ của Redis — skiplist. Đây là một thành phần quan trọng đóng góp vào hiệu suất và khả năng hoạt động của hệ thống, vì vậy hãy cùng khám phá chi tiết về cách nó hoạt động và vai trò của nó trong cơ chế lưu trữ dữ liệu của Redis.
Trong Redis123win+club, việc sử dụng skiplist là để hỗ trợ việc triển khai cấu trúc dữ liệu sorted set mà Redis cung cấp ra bên ngoài. Sorted set mang đến rất nhiều thao tác phong phú, đủ để đáp ứng hầu hết các nhu cầu thực tế trong các ứng dụng khác nhau. Tuy nhiên, điều này cũng đồng nghĩa với việc sorted set có thể được xem là một trong những phần phức tạp hơ Bên cạnh đó, skiplist - cấu trúc dữ liệu mà nó sử dụng - cũng không phải là một chủ đề quá quen thuộc đối với nhiều người, bởi vì không phải trường học nào cũng dành thời gian chi tiết để giảng dạy về nó trong các khóa học về thuật toán. Do đó, để đảm bảo bài viết được dễ hiểu và đầy đủ thông tin nhất, bài này sẽ cần nhiều không gian hơn so với các phần còn lại trong loạt bài này. Điều thú vị là, mặc dù skiplist nghe có vẻ xa lạ, nhưng nó lại là một công cụ mạnh mẽ giúp tăng tốc độ truy xuất dữ liệ Trong khi cây đỏ-đen (red-black tree) hoặc các cấu trúc khác cũng có thể được dùng cho mục đích tương tự, skiplist lại nổi bật nhờ khả năng cân bằng giữa hiệu suất và đơn giản hóa việc triển khai. Điều này làm cho nó trở thành một lựa chọn lý tưởng trong môi trường như Redis, nơi mà hiệu suất và tính linh hoạt đóng vai trò cực kỳ quan trọng. Vì vậy, trong bài viết này, chúng ta sẽ đi sâu vào chi tiết về cách skiplist hoạt động, lý do tại sao nó lại phù hợp với sorted set, và làm thế nào nó giúp tối ưu hóa hiệu suất của Redis trong các tình huống cụ thể. Hy vọng qua đó, bạn sẽ có cái nhìn rõ hơn về lý do tại sao Redis lại chọn skiplist làm nền tảng chính cho sorted set của mình.
Chúng ta sẽ giới thiệu nó theo ba phần chính:
Trong quá trình thảo luận123win+club, chúng ta sẽ đề cập đến hai cấu hình Redis (ở phần CẤU HÌNH NÂNG CAO trong tệp redis.conf):
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
Trong quá trình thảo luậnxem ngoại hạng anh, chúng tôi sẽ giải thích chi tiết ý nghĩa của hai cấu hình này.
Lưu ý: Việc phân tích mã nguồn trong bài viết này dựa trên nhánh 3.2 của mã nguồn Redis.
Skiplist về bản chất cũng là một loại cấu trúc tìm kiếm123win+club, được thiết kế để giải quyết các vấn đề tìm kiếm trong thuật toán (Searching). Cụ thể, nó giúp xác định nhanh chóng vị trí của một key đã cho (hoặc giá trị tương ứng với key đó) trong tập dữ liệu. Với khả năng tối ưu hóa việc duyệt qua các phần tử, skiplist trở thành một lựa chọn hiệu quả khi cần xử lý các thao tác tìm kiếm liên tục và nhanh chóng.
Phân tích cấu trúc dữ liệu bên trong Redis Phần đầu tiên Trong phần giới thiệu về dictbacarat, chúng ta đã từng thảo luận rằng các phương pháp giải quyết vấn đề tìm kiếm thường được chia thành hai nhóm lớn: một nhóm dựa trên các cây cân bằng và một nhóm khác dựa trên bảng băm (hash table). Tuy nhiên, skiplist lại khá đặc biệt, vì nó không thể được xếp vào bất kỳ nhóm nào trong hai nhóm này. Nó có những đặc điểm độc đáo khiến nó trở thành một lựa chọn thú vị trong việc xử lý dữ liệu.
Loại cấu trúc dữ liệu này do ... William Pugh Phát minh ra nó123win+club, lần đầu tiên xuất hiện trong bài báo năm 1990 của ông ... Skip Lists: A Probabilistic Alternative to Balanced Trees "..." Các bạn quan tâm đến chi tiết có thể tải bản gốc của bài báo để đọc.
Skiplist123win+club, như tên gọi của nó, trước hết là một danh sách. Thực tế, nó được phát triển dựa trên ý tưởng của danh sách liên kết có thứ tự. Tuy nhiên, khác với danh sách liên kết thông thường, skiplist sử dụng nhiều lớp hàng đợi xếp chồng lên nhau, cho phép truy xuất dữ liệu nhanh hơn nhờ các thao tác nhảy giữa các nút. Điều này làm cho việc tìm kiếm trở nên hiệu quả hơn rất nhiều so với việc duyệt tuần tự từng phần tử trong danh sách liên kết cơ bản.
Trước tiênxem ngoại hạng anh, hãy nhìn vào một danh sách liên kết đã sắp xếp, như hình dưới đây (nút màu xám ở phía bên trái biểu thị một nút đầu rỗng):
Trong một danh sách liên kết như vậyxem ngoại hạng anh, nếu chúng ta muốn tìm kiếm một giá trị cụ thể, thì cần phải bắt đầu từ đầu danh sách và so sánh lần lượt từng nút cho đến khi tìm thấy nút chứa giá trị đó hoặc gặp nút có giá trị lớn hơn giá trị cần tìm (trường hợp không tìm thấy). Điều này có nghĩa là độ phức tạp thời gian sẽ là O(n). Tương tự, khi chúng ta muốn chèn thêm một giá trị mới, cũng cần thực hiện quá trình tìm kiếm giống như trên để xác định vị trí chính xác mà giá trị mới cần được chèn vào.
Giả sử rằng chúng ta thêm một con trỏ giữa mỗi hai nút kế tiếpxem ngoại hạng anh, cho phép con trỏ chỉ xuống nút tiếp theo, như hình dưới đây:
Kết quả là tất cả các con trỏ mới được kết nối với nhau thành một danh sách liên kết hoàn toàn mớixem ngoại hạng anh, nhưng số lượng nút trong danh sách này chỉ bằng một nửa so với danh sách gốc (trong hình trên là các nút có giá trị 7, 19 và 26). Hiện tại, khi muốn tìm kiếm dữ liệu, chúng ta có thể bắt đầu bằng cách duyệt qua danh sách liên kết mới này. Khi gặp một nút có giá trị lớn hơn giá trị cần tìm, hãy quay lại danh sách ban đầu để tiếp tục tìm kiếm. Ví dụ, nếu muốn tìm giá trị 23, quá trình tìm kiếm sẽ đi theo hướng mà các mũi tên màu đỏ trong hình dưới đây đã chỉ ra.
Trong quá trình tìm kiếm này123win+club, nhờ có sự thêm vào của con trỏ mới, chúng ta không cần phải so sánh từng bước với tất cả các nút trong danh sách liên kết nữa. Số lượng nút cần so sánh chỉ còn khoảng một nửa so với trước đây. Điều này giúp tiết kiệm đáng kể thời gian và công sức trong việc xử lý dữ liệu.
Chúng ta có thể áp dụng cách tương tự để tiếp tục thêm một con trỏ vào mỗi cặp nút mới xuất hiện trong chuỗi liên kết ở tầng trênxem ngoại hạng anh, từ đó tạo ra một chuỗi liên kết thứ ba. Hãy tham khảo hình minh họa dưới đây: [Trong phần hình ảnh, giả sử bạn sẽ có một biểu đồ hoặc sơ đồ giải thích cách hoạt động của các lớp chuỗi liên kết] Mỗi lần lặp lại quá trình này không chỉ làm tăng độ phức tạp của cấu trúc dữ liệu mà còn giúp tối ưu hóa hiệu suất tìm kiếm và thao tác dữ liệu. Với cách sắp xếp hợp lý, chúng ta có thể giảm đáng kể thời gian thực thi các lệnh xử lý trong hệ thống.
Trong cấu trúc bảng liên kết ba tầng mới nàyxem ngoại hạng anh, nếu chúng ta vẫn đang tìm kiếm số 23, thì khi bắt đầu từ bảng liên kết ở tầng trên cùng, điều đầu tiên cần so sánh sẽ là số 19. Khi nhận ra rằng 23 lớn hơn 19, chúng ta ngay lập tức biết rằng chỉ cần tiếp tục tìm kiếm từ phần sau của vị trí 19 và hoàn toàn có thể bỏ qua tất cả các nút trước nó. Có thể tưởng tượng rằng, khi bảng liên kết đủ dài, cách tìm kiếm dựa trên nhiều lớp bảng liên kết này sẽ giúp chúng ta bỏ qua rất nhiều nút ở các tầng dưới, từ đó làm tăng đáng kể tốc độ tìm kiếm. Đặc biệt, với một lượng lớn dữ liệu, cấu trúc này không chỉ tiết kiệm thời gian mà còn tối ưu hóa hiệu suất hoạt động của hệ thống. Giả sử bạn đang xử lý một cơ sở dữ liệu khổng lồ, thay vì phải duyệt từng bước từ đầu đến cuối, việc sử dụng bảng liên kết nhiều tầng cho phép bạn nhanh chóng xác định phạm vi cần tìm kiếm, giảm thiểu công sức kiểm tra không cần thiết. Điều này đặc biệt hữu ích trong các ứng dụng yêu cầu tốc độ xử lý cao như hệ thống quản lý thông tin, mạng xã hội hoặc phân tích dữ liệu lớn.
Skiplist được thiết kế dựa trên ý tưởng của danh sách liên kết đa lớp này. Thực tếbacarat, theo cách tạo danh sách liên kết như đã mô tả ở trên, số lượng các nút ở mỗi lớp phía trên sẽ bằng một nửa số lượng nút ở lớp dưới cùng, điều này làm cho quá trình tìm kiếm trở nên rất giống với việc thực hiện tìm kiếm nhị phân, giúp giảm thời gian phức tạp của việc tìm kiếm xuống mức O(log n). Tuy nhiên, phương pháp này gặp vấn đề lớn khi thêm dữ liệu mới. Khi một nút mới được chèn vào, sự cân bằng 2:1 giữa các nút ở các lớp liền kề sẽ bị phá vỡ. Nếu muốn duy trì tỷ lệ này, tất cả các nút sau khi chèn (bao gồm cả nút vừa được thêm vào) phải được điều chỉnh lại, dẫn đến việc thời gian phức tạp độ trở lại mức O(n). Vấn đề tương tự cũng xảy ra khi xóa dữ liệu. Sự bất ổn này khiến skiplist cần một cơ chế đặc biệt để giữ cho cấu trúc luôn cân bằng mà không làm ảnh hưởng quá nhiều đến hiệu suất tổng thể. Các kỹ thuật bổ sung như xác suất ngẫu nhiên hoặc các thuật toán tối ưu hóa khác đã được áp dụng trong skiplist để giải quyết những hạn chế này, từ đó tạo ra một cấu trúc dữ liệu linh hoạt và hiệu quả trong nhiều trường hợp sử dụng thực tế.
Để giải quyết vấn đề nàyxem ngoại hạng anh, skiplist không yêu cầu số lượng nút giữa các danh sách liên kết liền kề phải tuân theo mối quan hệ nhất định mà thay vào đó sẽ ngẫu nhiên xác định một mức độ (level) cho từng nút. Ví dụ, nếu một nút được chọn có mức độ là 3, nó sẽ được thêm vào cả ba danh sách liên kết từ tầng thứ nhất đến tầng thứ ba. Để làm rõ hơn, hình ảnh dưới đây minh họa quá trình hình thành skiplist thông qua từng bước chèn nút. Hình ảnh minh họa này cho thấy cách mà mỗi nút mới được thêm vào dựa trên xác suất ngẫu nhiên của nó, từ đó tạo ra cấu trúc phân tầng linh hoạt và hiệu quả trong việc tìm kiếm và truy xuất dữ liệu. Mỗi bước chèn không chỉ giúp duy trì sự cân bằng của cấu trúc mà còn tối ưu hóa thời gian thực thi các thao tác cơ bản như thêm, xóa hoặc tìm kiếm.
Qua quá trình tạo và chèn của Skip List123win+club, có thể nhận thấy rằng số tầng (level) của mỗi nút là ngẫu nhiên, và việc chèn thêm một nút mới sẽ không ảnh hưởng đến số tầng của các nút khác. Do đó, thao tác chèn chỉ cần chỉnh sửa các con trỏ trước và sau nút được chèn, mà không cần phải điều chỉnh nhiều nút khác. Điều này làm giảm độ phức tạp của thao tác chèn. Thực tế, đây là một tính năng quan trọng của Skip List, giúp nó vượt trội hơn so với các giải pháp sử dụng cây cân bằng về hiệu suất khi chèn. Chúng ta sẽ đề cập lại điều này trong phần sau.
Dựa trên cấu trúc skiplist trong hình ảnh được cung cấpxem ngoại hạng anh, chúng ta có thể dễ dàng hiểu vì sao tên của cấu trúc dữ liệu này lại được đặt như vậy. Khi dịch sang tiếng Việt, skiplist có thể được gọi là "bảng nhảy" hoặc "bảng vượt qua", ám chỉ rằng ngoài danh sách liên kết ở tầng thứ nhất (tầng cơ sở), nó sẽ tạo ra nhiều tầng danh sách liên kết thưa thớt hơn. Ở các tầng này, các con trỏ cố ý bỏ qua một số nút (và càng ở các tầng cao hơn, số lượng nút bị bỏ qua sẽ càng nhiều). Điều này cho phép chúng ta tìm kiếm dữ liệu bằng cách bắt đầu từ các tầng cao hơn, rồi dần dần di chuyển xuống tầng thấp hơn, cuối cùng đến tầng thứ nhất để xác định chính xác vị trí của dữ liệu. Trong quá trình này, chúng ta đã bỏ qua một số nút, nhờ đó làm tăng tốc độ tìm kiếm. Ngoài ra, việc sử dụng skiplist không chỉ giúp cải thiện hiệu suất tìm kiếm mà còn mang lại khả năng thêm/xóa phần tử dễ dàng hơn so với các cấu trúc dữ liệu truyền thống khác. Chính sự linh hoạt và hiệu quả này đã khiến skiplist trở thành một lựa chọn phổ biến trong các ứng dụng cần xử lý dữ liệu lớn và yêu cầu tốc độ cao.
Skiplist mới được tạo ra này bao gồm tổng cộng 4 danh sách liên kếtbacarat, và bây giờ giả sử chúng ta vẫn đang tìm kiếm giá trị 23 trong đó, hình ảnh bên dưới cho thấy đường đi tìm kiếm: Hình ảnh chỉ ra rằng khi thực hiện tìm kiếm, các nút được duyệt qua sẽ được đánh dấu rõ ràng. Đầu tiên, chúng ta bắt đầu từ nút ở tầng cao nhất của skiplist, tiến hành so sánh và di chuyển sang trái hoặc xuống dưới sao cho giá trị cần tìm (23) luôn nằm bên phải của nút hiện tại. Quá trình này tiếp tục cho đến khi chúng ta tìm thấy giá trị mong muốn hoặc xác định rằng nó không tồn tạ Skiplist là một cấu trúc dữ liệu rất hiệu quả vì nó giảm thiểu số lần so sánh cần thiết trong quá trình tìm kiếm thông qua việc sử dụng nhiều cấp độ danh sách liên kết. Điều này giúp tăng tốc đáng kể so với việc sử dụng một danh sách liên kết đơn thuần.
Điều cần lưu ý làbacarat, quá trình chèn các nút đã được trình bày trước đó thực tế cũng đòi hỏi một bước tìm kiếm tương tự trước khi thực hiện việc chèn. Đầu tiên, bạn phải xác định vị trí chính xác để chèn bằng cách tìm kiếm trong cấu trúc dữ liệu, sau đó mới tiến hành hoàn thành thao tác chèn. Quy trình này không chỉ giúp đảm bảo tính đúng đắn mà còn tăng hiệu quả hoạt động của toàn bộ hệ thống.
Cho đến thời điểm nàybacarat, chúng ta đã hiểu rõ về các thao tác tìm kiếm và chè Thao tác xóa cũng tương tự như thao tác chèn, và có lẽ bạn cũng dễ dàng tưởng tượng ra cách thực hiện nó. Tất cả những thao tác này chắc chắn không quá khó để bạn viết thành mã nguồn, giúp chúng trở nên cụ thể và hữu ích hơn bao giờ hết.
Trong thực tếbacarat, cấu trúc dữ liệu skip list mỗi nút nên chứa cả phần key và phần value. Ở phần trước, chúng ta chưa phân biệt rõ ràng giữa key và value, nhưng trên thực tế, danh sách sẽ được sắp xếp theo thứ tự của các key, và quá trình tìm kiếm cũng dựa vào việc so sánh các key. Điều này có nghĩa là khi bạn duyệt qua danh sách, hệ thống sẽ sử dụng key để xác định vị trí của từng phần tử một cách hiệu quả, từ đó giúp tối ưu hóa thời gian truy xuất thông tin.
Tuy nhiênbacarat, nếu đây là lần đầu tiên bạn tiếp cận với cấu trúc dữ liệu skiplist, chắc chắn bạn sẽ đặt ra một câu hỏi: Việc chèn nút vào trong skiplist sử dụng việc sinh ngẫu nhiên số tầng cho mỗi nút, chỉ dựa trên một thao tác ngẫu nhiên đơn giản như vậy, liệu có đảm bảo được hiệu suất tìm kiếm tốt không? Để trả lời câu hỏi này, chúng ta cần phân tích các đặc tính thống kê củ Trong thực tế, khi nói về skiplist, nhiều người thường cảm thấy tò mò về cách hoạt động của nó. Cấu trúc này không chỉ phụ thuộc vào việc ngẫu nhiên chọn số tầng cho mỗi nút mà còn liên quan đến việc sắp xếp hợp lý các phần tử trong từng lớp. Chính sự kết hợp giữa yếu tố ngẫu nhiên và cơ chế sắp xếp này đã tạo nên khả năng tìm kiếm nhanh chóng mà skiplist mang lại. Vì vậy, việc hiểu rõ về mặt thống kê là vô cùng quan trọng để đánh giá hiệu suất thực sự của nó.
Trước khi tiến hành phân tíchbacarat, chúng ta cần nhấn mạnh rằng việc tính toán số ngẫu nhiên trong quá trình thực hiện thao tác chèn là một bước vô cùng quan trọng. Bước này có tác động lớn đến các đặc tính thống kê củ Số ngẫu nhiên này không phải là loại số ngẫu nhiên thông thường tuân theo phân phối đều mà nó được tính toán theo quy trình như sau:
Pseudocode để tính toán số tầng ngẫu nhiên như sau:
randomLevel
()
level
:=
1
// random() trả về một số ngẫu nhiên trong khoảng [0...1)
while
random
()
<
p
and
level
<
MaxLevel
do
level
:=
level
+
1
return
level
Trong mã giả của hàm randomLevel()xem ngoại hạng anh, có hai tham số được đề cập, đó là p và MaxLevel. Trong cơ chế thực hiện skiplist của Redis, hai tham số này có giá trị cụ thể như sau: Tham số p (probability) thường được đặt ở mức 0.25, nghĩa là xác suất để một nút xuất hiện ở cấp độ tiếp theo sẽ là 1/4. Điều này giúp duy trì sự cân bằng giữa chiều cao của skiplist và hiệu suất tìm kiếm. Tham số MaxLevel lại đóng vai trò là giới hạn tối đa của các lớ Giá trị này trong Redis thường được thiết lập ở mức 32, cho phép tối ưu hóa không gian và đảm bảo rằng các hoạt động trên skiplist diễn ra nhanh chóng mà không gây quá tải về bộ nhớ. Việc lựa chọn những giá trị này không chỉ giúp tối ưu hóa hiệu suất mà còn đảm bảo rằng skiplist hoạt động ổn định và hiệu quả trong môi trường Redis.
p = 1/4
MaxLevel = 32
Trong phần này123win+club, chúng ta sẽ phân tích sơ lược về độ phức tạp thời gian và không gian của cấu trúc dữ liệu skiplist để có thể hiểu rõ hơn về hiệu suất hoạt động của nó. Nếu bạn không quá bận tâm đến việc phân tích kỹ lưỡng về hiệu năng của thuật toán, thì bạn hoàn toàn có thể bỏ qua đoạn văn này mà không vấn đề gì. Skiplist là một cấu trúc dữ liệu khá thú vị, được thiết kế để hỗ trợ tìm kiếm nhanh chóng trong các tập dữ liệu lớn. Độ phức tạp thời gian trung bình của skiplist cho các thao tác như tìm kiếm, chèn và xóa thường nằm ở mức O(log n), nhờ vào việc sử dụng các cấp lớp xếp chồng lên nhau. Điều này giúp skiplist đạt được tốc độ gần giống với cây nhị phân cân bằng (balanced binary tree) nhưng với cài đặt đơn giản hơn nhiều. Tuy nhiên, đổi lại, skiplist cần thêm không gian lưu trữ để quản lý các cấp lớp. Mỗi nút trong skiplist có thể chứa nhiều con trỏ dẫn đến các nút ở các tầng khác nhau, điều này làm cho độ phức tạp không gian của skiplist có thể tăng lên đến O(n log n). Dù vậy, thực tế cho thấy rằng số lượng lớp trung bình thường khá thấp, do đó không gian sử dụng thực tế vẫn tương đối hợp lý. Nếu bạn chỉ muốn hiểu về khái niệm cơ bản của skiplist mà không cần đi sâu vào các chi tiết kỹ thuật, thì đừng lo lắng nếu bạn bỏ qua phần phân tích này. Có rất nhiều khía cạnh khác của skiplist mà bạn có thể khám phá sau này!
Chúng ta hãy bắt đầu bằng cách tính toán số lượng trung bình các con trỏ được chứa trong mỗi nút (mong đợi xác suất). Số lượng con trỏ mà một nút chứa chính là chi phí bổ sung (overhead) của thuật toán trong không gian123win+club, và nó có thể được sử dụng để đo lường độ phức tạp về mặt không gian. Mỗi nút trong cấu trúc dữ liệu này không chỉ đơn thuần là nơi lưu trữ thông tin mà còn đóng vai trò như một mắt xích quan trọng kết nối với các nút khác. Việc đánh giá số lượng trung bình các con trỏ sẽ giúp chúng ta hiểu rõ hơn về cách thuật toán này quản lý tài nguyên trong bộ nhớ và liệu nó có hiệu quả hay không trong việc tối ưu hóa không gian. Ngoài ra, khi tính toán overhead này, chúng ta cũng cần cân nhắc đến việc liệu thuật toán có thể tự động điều chỉnh hoặc tối ưu hóa số lượng con trỏ theo từng trường hợp cụ thể hay không. Điều này không chỉ ảnh hưởng đến hiệu suất tổng thể mà còn quyết định khả năng mở rộng của thuật toán khi đối mặt với khối lượng dữ liệu lớn hơn.
Dựa trên mã giả của hàm randomLevel()bacarat, chúng ta có thể dễ dàng nhận thấy rằng việc tạo ra các lớp nút ở mức cao hơn sẽ có xác suất xảy ra thấp hơn. Để phân tích cụ thể hơn, chúng ta có thể xem xét như sau: Trong cấu trúc Skip List, mỗi lần gọi hàm randomLevel(), một giá trị ngẫu nhiên sẽ được sinh ra để xác định chiều cao của nút mới. Khi giá trị ngẫu nhiên đạt đến một giới hạn nhất định (thường là một giá trị mũ hai), nó sẽ quyết định xem nút đó có được thêm vào một lớp cao hơn hay không. Điều này dẫn đến thực tế rằng, các lớp cao hơn chỉ xuất hiện với tần suất thấp hơn so với các lớp thấp hơn. Về mặt định lượng, xác suất để một nút nằm ở một lớp thứ k (k > 1) thường giảm theo một chuỗi nhân cấp số nhân, ví dụ: xác suất cho lớp thứ hai thường bằng một nửa xác suất cho lớp đầu tiên, và xác suất cho lớp thứ ba lại bằng một nửa xác suất của lớp thứ hai, và cứ thế tiếp tục. Điều này tạo nên một sự phân bố hình thang, nơi các lớp thấp hơn thường xuyên xuất hiện hơn, trong khi các lớp cao hơn chỉ xuất hiện hiếm hoi nhưng đóng vai trò quan trọng trong việc tối ưu hóa hiệu suất tìm kiếm.
Do đóxem ngoại hạng anh, số tầng trung bình của một nút (cũng chính là số lượng con trỏ trung bình mà nó chứa), được tính như sau:
Bây giờ rất dễ dàng tính toán được:
Tiếp theoxem ngoại hạng anh, để phân tích độ phức tạp thời gian, chúng ta sẽ tính toán chiều dài trung bình của quá trình tìm kiế Chiều dài tìm kiếm được xác định bởi số lần nhảy vượt qua các nút trên đường đi tìm kiếm, và số lần so sánh trong quá trình tìm kiếm sẽ bằng với chiều dài tìm kiếm cộng thêm 1. Lấy ví dụ về đường đi tìm kiếm số 23 được đánh dấu trước đó trong hình, bắt đầu từ nút đầu tiên ở góc trên bên trái và tiếp tục đến nút 22, chiều dài tìm kiếm trong trường hợp này là 6. Chúng ta có thể thấy rõ hơn rằng mỗi bước nhảy đại diện cho một quyết định mà thuật toán đưa ra trong quá trình tìm kiếm, và mỗi bước này đều yêu cầu một phép so sánh để xác định xem liệu cần di chuyển xuống hàng thấp hơn hay tiếp tục tiến về phía phải trên cùng một hàng. Điều này giải thích tại sao việc tính toán chính xác số lần so sánh rất quan trọng đối với hiệu suất tổng thể củ
Để tính toán độ dài của việc tìm kiếmbacarat, chúng ta cần sử dụng một chút mẹo nhỏ. Chúng ta nhận thấy rằng khi mỗi nút được chèn vào, số tầng của nó được xác định bởi hàm randomLevel(). Hơn nữa, cách tính ngẫu nhiên này không phụ thuộc vào các nút khác, và mỗi lần chèn là hoàn toàn độc lập. Do đó, về mặt thống kê, cấu trúc của skip list không phụ thuộc vào thứ tự chèn các nút. Ngoài ra, điều thú vị là cấu trúc skip list cho phép việc tìm kiếm trở nên hiệu quả hơn so với danh sách liên kết thông thường. Khi một nút được thêm vào, hàm randomLevel() tạo ra một xác suất mà có thể làm tăng hoặc giảm số tầng của nút đó. Điều này có nghĩa là cấu trúc của skip list luôn được tối ưu hóa để đảm bảo rằng các nút quan trọng sẽ nằm ở tầng cao hơn, từ đó giúp việc tìm kiếm diễn ra nhanh chóng hơn.
Với cách này123win+club, để tính toán độ dài của quá trình tìm kiếm, chúng ta có thể xem xét lại hành trình tìm kiếm từ góc nhìn ngược lại. Bắt đầu từ nút cuối cùng ở hàng thứ nhất trong góc dưới bên phải, sau đó di chuyển ngược lên và sang trái theo đường đi tìm kiếm, giống như việc leo cầu thang vậy. Giả sử rằng khi quay lại một nút nào đó, nó mới được chèn vào – điều này, dù về mặt logic có vẻ thay đổi thứ tự chèn của các nút, nhưng về mặt thống kê, không làm ảnh hưởng đến cấu trúc tổng thể của danh sách nhảy (skiplist). Thêm vào đó, vì mỗi nút có xác suất độc lập được chọn để nằm ở bất kỳ cấp bậc nào, nên dù chúng ta đảo ngược quá trình, sự phân bố của các cấp vẫn giữ nguyên. Điều này cho phép chúng ta hiểu rõ hơn về tính ngẫu nhiên và ổn định của skiplist trong việc tối ưu hóa thời gian truy xuất dữ liệu.
Giả sử hiện tại chúng ta đang đứng tại một nút x thuộc tầng thứ i và cần di chuyển lên trên và sang trái thêm k tầng. Ở thời điểm nàyxem ngoại hạng anh, có hai khả năng xảy ra: Hoặc là chúng ta tiếp tục di chuyển theo hướng thẳng đứng lên trên trong suốt quãng đường, hoặc là chúng ta sẽ phải thực hiện một chuỗi các bước xoay tréo để đạt được mục tiêu. Dù lựa chọn nào đi chăng nữa, cả hai phương án đều đòi hỏi sự cẩn trọng và chiến lược rõ ràng để đảm bảo không bỏ lỡ bất kỳ cơ hội nào trong quá trình tiến lên.
Hai trường hợp này được minh họa trong hình dưới đây:
Sử dụng C(k) để biểu thị độ dài trung bình của đường đi tìm kiếm cần phải đi qua khi leo lên k cấpxem ngoại hạng anh, thì:
Bạn có thể thấy rằng giá trị ban đầu C(0) bằng 0. Còn đối với C(k)bacarat, nó được tính theo công thức sau: C(k) = (1-p) × (chiều dài tìm kiếm trong trường hợp b ở hình trên) + p × (chiều dài tìm kiếm trong trường hợp c ở hình trên). Trong đó, tham số p là xác suất xảy ra của một sự kiện nào đó, và (1-p) đại diện cho xác suất của trường hợp còn lại. Điều này cho phép chúng ta xác định độ dài trung bình cần thiết để thực hiện việc tìm kiếm dựa trên hai kịch bản khác nhau được minh họa trong hình vẽ.
Thay thế123win+club, thu được một phương trình sai phân và giản lược:
C(k)=(1-p)(C(k)+1) + p(C(k-1)+1)
C(k)=1/p+C(k-1)
C(k)=k/p
Kết quả này cho thấy rằng mỗi khi chúng ta di chuyển lên một cấp độbacarat, cần phải di chuyển thêm 1/p bước trên đường đi tìm kiếm. Hơn nữa, tổng số cấp độ mà chúng ta cần leo lên sẽ bằng với số lượng tầng của danh sách nhảy (skiplist) trừ đi một.
Tiếp theo123win+club, chúng ta cần phân tích xem khi skiplist có n nút, giá trị kỳ vọng của tổng số tầng sẽ là bao nhiêu. Đây là một câu hỏi khá dễ hiểu nếu nhìn nhận trực quan. Dựa trên thuật toán xác định độ sâu của các nút, chúng ta có thể suy ra rằng: Trong skiplist, mỗi nút có khả năng xuất hiện ở bất kỳ tầng nào từ 1 đến k (k là số tầng tối đa). Xác suất để một nút nằm ở tầng i được tính theo công thức 1/2^i, nghĩa là xác suất giảm đi một nửa sau mỗi tầng. Điều này cho phép chúng ta xây dựng mô hình phân phối xác suất cho các tầng củ Do đó, khi có n nút, chúng ta có thể tính toán giá trị kỳ vọng bằng cách nhân số lượng nút với xác suất mà một nút xuất hiện ở từng tầng. Kết quả cuối cùng sẽ cho thấy rằng tổng số tầng của skiplist tăng dần nhưng vẫn duy trì được hiệu quả trong việc tìm kiếm và chèn/xóa nút.
Vì vậyxem ngoại hạng anh, từ tầng 1 đến tầng cao nhất, số lượng nút trung bình trên mỗi danh sách liên kết tạo thành một cấp số nhân giảm dần. Có thể dễ dàng tính toán được rằng giá trị trung bình của tổng số tầng bằng log. Điều thú vị là cấu trúc này cho phép hệ thống hoạt động hiệu quả trong việc tìm kiếm và cập nhật dữ liệu. Với mỗi tầng, số lượng nút giảm dần theo tỷ lệ cố định, tạo ra một cơ chế tối ưu hóa về mặt tài nguyên. Điều này đặc biệt hữu ích khi xử lý khối lượng lớn thông tin, giúp giảm thiểu thời gian truy xuất và tăng tốc độ phản hồi đáng kể. 1/p nútbacarat, và số lượng nút trung bình của tầng cao nhất là 1/p.
Tổng quát123win+club, nếu tính nhanh, độ dài tìm kiếm trung bình gần bằng:
Tức làbacarat, độ phức tạp thời gian trung bình là O(log n).
Đương nhiênxem ngoại hạng anh, phân tích độ phức tạp thời gian ở đây vẫn còn tương đối sơ lược. Ví dụ, khi di chuyển ngược theo đường dẫn tìm kiếm về phía bên trái và phía trên, có thể bạn sẽ chạm đến nút đầu tiên bên trái trước, sau đó đi lên dọc theo chuỗi các nút này; hoặc cũng có thể bạn chạm đến nút ở tầng cao nhất trước, rồi tiếp tục di chuyển sang trái theo chuỗi liên kết ở tầng đó. Tuy nhiên, những chi tiết này không làm thay đổi kết quả độ phức tạp thời gian trung bình cuối cùng. Ngoài ra, độ phức tạp thời gian được đưa ra ở đây chỉ là một giá trị trung bình xác suất, nhưng thực tế hoàn toàn có thể tính toán một phân phối xác suất chính xác hơn. Để hiểu rõ hơn về vấn đề này, bạn vui lòng tham khảo thêm. William Pugh Bài báo của ... Skip Lists: A Probabilistic Alternative to Balanced Trees 》。
Trong phần nàybacarat, chúng ta sẽ thảo luận về việc thực hiệ
Trong Redisxem ngoại hạng anh, skiplist được sử dụng để triển khai một cấu trúc dữ liệu mà người dùng có thể tương tác trực tiếp: tập hợp đã sắp xếp (sorted set). Thực tế, sorted set không chỉ dựa vào skiplist mà còn kết hợp cả ziplist và dict. Chúng ta sẽ tìm hiểu thêm về mối quan hệ giữa các cấu trúc này trong chương sau. Hiện tại, hãy dành chút thời gian để khám phá những lệnh quan trọng củ Những lệnh này đóng vai trò vô cùng quan trọng đối với cách skiplist được thực hiệ Skiplist không chỉ là nền tảng cốt lõi mà còn hỗ trợ hiệu quả cho việc truy xuất nhanh chóng các phần tử Điều này đặc biệt hữu ích khi bạn cần thêm, xóa hoặc tìm kiếm các phần tử dựa trên thứ tự giá trị của chúng. Các lệnh như ZADD, ZREM, ZRANGE hay ZSCORE đều phụ thuộc vào cách hoạt động củ Bên cạnh đó, ziplist được sử dụng khi số lượng phần tử trong sorted set nhỏ, giúp tiết kiệm bộ nhớ hơn. Trong trường hợp dữ liệu lớn hơn, dict sẽ được ưu tiên để đảm bảo hiệu suất truy xuất. Điều này cho thấy sự linh hoạt và tối ưu hóa của Redis trong việc lựa chọn cấu trúc dữ liệu phù hợp với từng tình huống cụ thể.
Sorted set (tập hợp đã sắp xếp) là một loại tập dữ liệu có thứ tự123win+club, rất lý tưởng cho các tình huống sử dụng như bảng xếp hạng. Với đặc tính giữ thứ tự của các phần tử theo giá trị, nó giúp người dùng dễ dàng truy xuất thông tin theo thứ tự ưu tiên hoặc điểm số, từ đó tạo ra trải nghiệm liền mạch trong các ứng dụng yêu cầu tính năng phân cấp hoặc đánh giá. Ví dụ điển hình là một bảng xếp hạng trò chơi, nơi người dùng luôn muốn nhìn thấy tên người chiến thắng ở đầu danh sách một cách nhanh chóng và chính xác.
Bây giờ chúng ta hãy xem qua một ví dụ123win+club, sử dụng tập hợp đã sắp xếp (sorted set) để lưu trữ bảng điểm của môn học đại số (algebra). Dữ liệu gốc như sau:
Dữ liệu này liệt kê tên và điểm số của từng học sinh. Tiếp theo123win+club, chúng ta sẽ lưu dữ liệu này vào tập hợp đã sắp xếp (sorted set) như sau:
Đối với các lệnh trênbacarat, những điểm cần chú ý bao gồm:
Tóm lại123win+club, mỗi phần tử trong sorted set chủ yếu thể hiện 3 thuộc tính:
Chúng ta hãy phân tích sơ lược một vài lệnh truy vấn xuất hiện trước đó:
Thực tếbacarat, việc thực hiện sorted set trong Redis như sau:
Cấu trúc của sorted set sẽ được thảo luận chi tiết hơn trong chương tiếp theo. Hiện tạibacarat, chúng ta hãy tập trung tìm hiểu mối liên hệ giữa sorted set và skiplist:
Quy trình tìm kiếm vừa rồi cũng ám chỉ độ phức tạp thời gian của các hoạt động:
Tóm lạibacarat, skiplist trong Redis so với skiplist cổ điển mà chúng ta đã đề cập trước đó có một số điểm khác biệt như sau:
#define ZSKIPLIST_MAXLEVEL 32
#define ZSKIPLIST_P 0.25
typedef
struct
zskiplistNode
{
robj
*
obj
;
double
score
;
struct
zskiplistNode
*
backward
;
struct
zskiplistLevel
{
struct
zskiplistNode
*
forward
;
unsigned
int
span
;
}
level
[];
}
zskiplistNode
;
typedef
struct
zskiplist
{
struct
zskiplistNode
*
header
,
*
tail
;
unsigned
long
length
;
int
level
;
}
zskiplist
;
Đoạn mã này xuất phát từ server.h123win+club, chúng ta sẽ phân tích ngắn gọn:
Hình ảnh dưới đây sử dụng bảng điểm của một lớp học đại số được chèn trước đó làm ví dụ123win+club, minh họa cấu trúc có thể của một skiplist trong Redis:
Lưu ý: Các số trong dấu ngoặc nhỏ phía trên mũi tên chỉ báo giá trị của span tương ứng. Điều này có nghĩa là mũi tên hiện tại vượt qua bao nhiêu nútxem ngoại hạng anh, nhưng việc đếm này không tính đến nút xuất phát của mũi tên mà chỉ tính đến nút đích cuối cùng.
Giả sử chúng ta đang tìm kiếm phần tử có score = 89.0 trong skiplist (tức là điểm số của Bob) và theo dõi con đường tìm kiếmbacarat, chúng ta sẽ vượt qua các mũi tên được đánh dấu đỏ trên đồ thị. Tổng các giá trị span của những mũi tên này sẽ cho ta thứ hạng của Bob, tính bằng cách cộng (2 + 2 + 1) rồi trừ đi 1, vì thứ hạng bắt đầu từ 0. Điều này áp dụng cho thứ hạng tăng dần. Nếu muốn tính thứ hạng giảm dần, chúng ta chỉ cần lấy độ dài của skiplist và trừ đi tổng các giá trị span đã tính trước đó, tức là 6 - (2 + 2 + 1) = 1. Điều này giúp xác định vị trí của Bob dựa trên thứ hạng giảm dần so với toàn bộ danh sách.
Rõ ràng123win+club, trong quá trình tìm kiếm một skiplist, bằng cách cộng dồn giá trị span, chúng ta có thể dễ dàng tính được thứ hạng của phần tử. Ngược lại, nếu muốn tìm kiếm dữ liệu dựa trên thứ hạng đã cho (giống như zrange và zrevrange), ta cũng có thể liên tục cộng dồn span và luôn đảm bảo tổng không vượt quá thứ hạng đã chỉ định. Bằng cách này, ta sẽ có được một đường dẫn tìm kiếm với độ phức tạp thời gian O(log n). Điều thú vị là việc này không chỉ giúp tối ưu hóa quá trình tìm kiếm mà còn tăng cường hiệu quả quản lý dữ liệu trong cấu trú
Chúng ta đã đề cập trước đây rằng sorted set trong Redis được xây dựng dựa trên skiplistxem ngoại hạng anh, dict và ziplist:
Hãy cùng tìm hiểu sơ lược về trường hợp đầu tiên mà chúng ta sẽ đề cập đến — đó là việc thực hiện sorted set dựa trên ziplist. Trong phần trước của chuỗi bài viết này123win+club, chúng ta đã từng đề cập đến... (Đây là một cách mở đầu linh hoạt hơn để tiếp cận chủ đề, đồng thời tạo sự kết nối với nội dung đã được trình bày trước đó.) Trong quá trình triển khai Redis, ziplist thường được sử dụng khi các tập dữ liệu nhỏ và đơn giản. Khi đó, cấu trúc ziplist giúp tối ưu hóa bộ nhớ và tăng hiệu suất cho các hoạt động cơ bản. Tuy nhiên, khi kích thước hoặc số lượng phần tử trong sorted set gia tăng, việc chuyển sang sử dụng linked list hoặc skip list có thể mang lại lợi ích hơn. Bạn có thấy điều này phù hợp với nhu cầu sử dụng của mình không? Bài viết về ziplist Trong phần trước123win+club, chúng ta đã đề cập rằng ziplist là một khối bộ nhớ liên tục gồm nhiều mục dữ liệu. Do mỗi phần tử của sorted set bao gồm cả dữ liệu và score, khi sử dụng lệnh zadd để thêm vào một cặp (dữ liệu, score), ở mức cơ sở, hệ thống sẽ chèn hai mục dữ liệu tương ứng vào ziplist: dữ liệu được đặt trước và score được đặt sau. Điều này giúp tối ưu hóa không gian lưu trữ trong trường hợp các phần tử có kích thước nhỏ và số lượng phần tử không quá lớn.
Một trong những ưu điểm chính của ziplist là tiết kiệm bộ nhớ123win+club, nhưng việc tìm kiếm trên nó chỉ có thể thực hiện theo thứ tự (có thể từ đầu đến cuối hoặc ngược lại). Do đó, các thao tác truy vấn khác nhau của sorted set sẽ tiến hành duyệt qua ziplist từ đầu đến cuối (hoặc từ cuối đến đầu), từng bước di chuyển hai phần tử tại một thời điểm, để "bước qua" mỗi cặp (dữ liệu, điểm số). Ngoài ra, do bản chất tuần tự của ziplist, hiệu suất tìm kiếm trong trường hợp này phụ thuộc rất nhiều vào vị trí của phần tử cần tìm. Nếu phần tử nằm ở gần cuối danh sách, số lần duyệt qua các phần tử có thể khá lớn, ảnh hưởng đến hiệu quả tổng thể của hoạt động truy vấn. Điều này cũng lý giải vì sao Redis thường chỉ sử dụng ziplist cho các trường hợp dữ liệu nhỏ và đơn giản, nhằm tối ưu hóa về mặt tài nguyên hệ thống.
Khi các phần tử được chèn vàobacarat, cấu trúc dữ liệu ziplist của sorted set có thể sẽ được chuyển đổi sang phiên bản đầy đủ của zset (chi tiết về quá trình chuyển đổi có thể tìm thấy trong t_zset.c ở hàm zsetConvert). Nhưng rốt cuộc thì cần phải chèn bao nhiêu phần tử thì mới xảy ra việc chuyển đổi này?
Nhớ lại hai cấu hình Redis mà chúng tôi đề cập ở đầu bài không?
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
Cấu hình này có nghĩa là khi một trong hai điều kiện sau đây được đáp ứngxem ngoại hạng anh, ziplist sẽ được chuyển đổi thành zset (các điều kiện cụ thể có thể được tìm thấy trong mã nguồn t_zset.c, ở hàm zaddGenericCommand liên quan):
Cuối cùngbacarat, định nghĩa mã nguồn của cấu trúc zset như sau:
typedef
struct
zset
{
dict
*
dict
;
zskiplist
*
zsl
;
}
zset
;
giữ mọi thứ thật đơn giản và hiệu quả
There are a few reasons:
1) They are not very memory intensive. It’s up to you basically. Changing parameters about the probability of a node to have a given number of levels will make then less memory intensive than btrees.
2) A sorted set is often target of many ZRANGE or ZREVRANGE operations123win+club, that is, traversing the skip list as a linked list. With this operation the cache locality of skip lists is at least as good as with other kind of balanced trees.
3) They are simpler to implement123win+club, debug, and so forth. For instance thanks to the skip list simplicity I received a patch (already in Redis master) with augmented skip lists implementing ZRANK in O(log(N)). It required little changes to the code.
Nguồn gốc câu nói này:
Những lý do được tóm tắt từ ba khía cạnh chính là mức sử dụng bộ nhớbacarat, khả năng hỗ trợ tìm kiếm phạm vi và độ dễ dàng trong việc triển khai – những chủ đề này chúng ta đã thực sự đề cập đến ở phần trước. Bên cạnh đó, mỗi khía cạnh này đều mang lại những lợi ích và thách thức riêng biệt. Về mặt sử dụng bộ nhớ, việc tối ưu hóa không chỉ giúp cải thiện hiệu suất mà còn giảm chi phí tài nguyên. Đối với khả năng tìm kiếm phạm vi, điều này đặc biệt quan trọng khi bạn cần xử lý các tập dữ liệu lớn hoặc yêu cầu tính toán phức tạp. Cuối cùng, độ dễ dàng trong việc triển khai cũng ảnh hưởng trực tiếp đến thời gian và nguồn lực cần thiết để phát triển hệ thống.
Trong phần tiếp theo của loạt bài viết nàybacarat, chúng ta sẽ cùng tìm hiểu về intset và mối liên hệ của nó với kiểu dữ liệu set mà Redis cung cấp ra bên ngoài. Đừng bỏ lỡ nhé!
(Kết thúc)
Các bài viết được chọn lọc khác :