Amazon DynamoDB cung cấp tính mở rộng và hiệu suất cao cho các ứng dụng có công việc biến đổi đa dạng. Trong khi DynamoDB xuất sắc trong việc phân phối dữ liệu một cách hiệu quả qua nhiều phân vùng, nó mặc định tuân theo một thứ tự sắp xếp cụ thể dựa trên schema đã chọn. Trong bài viết này, chúng tôi trình bày hai mô hình dữ liệu ví dụ, một được thiết kế để lưu trữ thông tin đơn hàng thương mại điện tử và một để lưu trữ điểm số trò chơi. Chúng tôi sử dụng các mô hình dữ liệu này để khám phá cách DynamoDB tự nhiên sắp xếp các mục và đào sâu vào các chiến lược hiệu quả để thiết lập thứ tự tùy chỉnh.
Trước khi chúng ta xem xét chi tiết, điều quan trọng là hiểu về tầm quan trọng của khóa phân vùng và khóa sắp xếp trong DynamoDB và cách chúng ta có thể tận dụng sức mạnh của chúng để tạo một mô hình dữ liệu hiệu quả và có khả năng mở rộng.
Đặc điểm của Khóa Phân Vùng và Khóa Sắp Xếp
Một khóa chính hợp thành trong DynamoDB bao gồm hai thuộc tính: khóa phân vùng và khóa sắp xếp. Giá trị khóa phân vùng được sử dụng làm đầu vào cho một hàm hash nội bộ, sau đó xác định phân vùng cụ thể (lưu trữ vật lý nội bộ trong DynamoDB) mà mục được lưu trữ. Các mục có cùng giá trị khóa phân vùng được lưu trữ cùng nhau và sắp xếp dựa trên giá trị khóa sắp xếp của chúng.
Trong các bảng có cả khóa phân vùng và khóa sắp xếp, có khả năng cho nhiều mục chia sẻ cùng một giá trị khóa phân vùng. Tuy nhiên, các mục có cùng giá trị khóa phân vùng phải có giá trị khóa sắp xếp khác nhau.
Sắp Xếp
Khóa sắp xếp, còn được gọi là khóa phạm vi, có trách nhiệm xác định thứ tự mà các mục được lưu trữ trong một phân vùng. Khi bạn truy vấn hoặc quét một bảng DynamoDB, khóa sắp xếp cho phép bạn lấy dữ liệu theo một thứ tự cụ thể dựa trên giá trị của khóa sắp xếp.
Các mục có cùng giá trị khóa phân vùng được tổ chức dựa trên khóa sắp xếp. Cơ chế sắp xếp thay đổi tùy theo loại dữ liệu của khóa sắp xếp:
- Nếu kiểu dữ liệu của khóa sắp xếp là Số, DynamoDB sắp xếp các mục theo thứ tự số, đảm bảo so sánh số học là trực quan và hiệu quả.
- Khi khóa sắp xếp có kiểu dữ liệu là Chuỗi, DynamoDB sắp xếp các mục theo thứ tự UTF-8 byte, làm cho nó lý tưởng cho việc sắp xếp từ điển.
- Đối với các kiểu dữ liệu Nhị phân, DynamoDB xử lý từng byte của dữ liệu nhị phân như không dấu, giúp việc sắp xếp cấp byte chính xác.
Điều Kiện
Khóa sắp xếp trong bảng DynamoDB là một công cụ mạnh mẽ để tối ưu hóa hiệu suất truy vấn. Bằng cách kết hợp khóa sắp xếp với điều kiện, bạn có thể thực hiện các truy vấn chính xác và hiệu quả để lấy chỉ dữ liệu bạn cần. Ví dụ, bạn có thể sử dụng điều kiện để lấy các mục có một thuộc tính có thể sắp xếp, chẳng hạn như một ngày. Điều này cho phép lấy dữ liệu mục tiêu, giảm lượng dữ liệu quét và cải thiện hiệu suất truy vấn. Bằng cách thiết kế mô hình dữ liệu của bạn một cách chiến lược và tận dụng khóa sắp xếp một cách hiệu quả, bạn có thể điều chỉnh các truy vấn để phù hợp với các mẫu truy cập khác nhau và truy cập dữ liệu quan trọng nhất cho ứng dụng của bạn.
Ví dụ mô hình dữ liệu thương mại điện tử
Để hiểu rõ hơn về cách sắp xếp hoạt động liên quan đến khóa phân vùng, hãy hình dung khái niệm này. DynamoDB lưu trữ dữ liệu trong các mục (tương tự như hàng) trong đó mỗi mục có một định danh duy nhất được gọi là khóa phân vùng, là cách chính để phân phối dữ liệu qua các phân vùng. Mô hình này sử dụng một khóa sắp xếp, quyết định thứ tự của các mục trong mỗi phân vùng. Bảng DynamoDB của chúng tôi chứa đơn đặt hàng của người dùng, với khóa phân vùng là userID và ngày đặt hàng của họ là khóa sắp xếp. DynamoDB không có kiểu dữ liệu Ngày (Date) mặc định, vì vậy khóa sắp xếp của chúng tôi sử dụng định dạng chuỗi ISO8601.
Đối với khóa phân vùng userID, DynamoDB phân phối dữ liệu của người dùng qua các phân vùng dựa trên ID của họ. Trong mỗi phân vùng, DynamoDB sắp xếp dữ liệu theo khóa sắp xếp là ngày đặt hàng. Để hình dung điều này, chúng ta có thể nghĩ về dữ liệu được tổ chức một cách tương tự như một tủ hồ sơ:
- Mỗi ngăn trong tủ đại diện cho một phân vùng, được xác định bằng một userID duy nhất.
- Bên trong mỗi ngăn (phân vùng), bạn sẽ tìm thấy các tệp (mục) cho mỗi người dùng, được sắp xếp theo ngày đặt hàng của họ.
Bảng dưới đây minh họa cho ví dụ của chúng tôi. Chúng ta có thể gọi một nhóm mục chia sẻ cùng một khóa phân vùng (nhưng khác nhau về khóa sắp xếp) là một tập hợp mục (item collection).
Bài viết này sẽ trình bày cách sử dụng DynamoDB của AWS để truy vấn thông tin đơn hàng của người dùng trong một khoảng thời gian cụ thể. Chúng ta có thể dễ dàng truy xuất tất cả các đơn hàng được đặt bởi “user123” trong vòng ba tháng. Dưới đây là một ví dụ về cách thực hiện yêu cầu đó bằng AWS Command Line Interface (CLI):
aws dynamodb query \
--table-name OrdersTable \
--key-condition-expression "#PK = :PK and #SK between :start and :finish" \
--expression-attribute-values '{":PK":{"S":"USER#user123"},":start":{"S":"2023-08-01"}, ":finish":{"S":"2023-11-31"}}' \
--expression-attribute-names '{"#PK":"PK", "#SK":"SK"}'
Bây giờ, hãy tưởng tượng rằng có các yêu cầu kinh doanh bổ sung với các mẫu truy cập bổ sung sau:
- Lấy tất cả các đơn hàng trong vòng 24 giờ qua.
- Lấy tất cả các đơn hàng trong vòng 7 ngày qua.
- Lấy tất cả các đơn hàng trong vòng 1 tháng qua.
- Lấy tất cả các đơn hàng trong vòng 3 tháng qua.
Chúng ta đã thấy cách DynamoDB duy trì thứ tự trong một bộ sưu tập mục theo giá trị khóa sắp xếp. Những mẫu truy cập mới này đòi hỏi một thứ tự đã sắp xếp mà bao gồm các mục (hoặc tất cả các khóa phân vùng).
Tổng quan về giải pháp:
Để thiết lập một thứ tự đã sắp xếp mà bao gồm tất cả các khóa phân vùng, một quan sát quan trọng là chúng ta thiếu một thuộc tính cho phép chúng ta nhóm dữ liệu thành một bộ sưu tập mục thống nhất.
Nếu việc lấy tất cả các đơn hàng từ quá khứ không phải là một mẫu truy cập thường xuyên được yêu cầu, chúng ta có thể sử dụng hoạt động Scan và lọc kết quả để phù hợp với khoảng thời gian mong muốn. Tuy nhiên, phương pháp này có thể không hiệu quả về cả hiệu suất và chi phí. Do đó, nếu mẫu truy cập này thường xuyên được yêu cầu, chúng ta cần một cách tiếp cận khác.
Tận dụng Global Secondary Index (GSI):
Một Global Secondary Index (GSI) là một tính năng của DynamoDB giữ một bản sao có tính nhất quán của một số hoặc tất cả dữ liệu trong bảng cơ sở. GSI cho phép truy vấn hiệu quả bảng dựa trên các thuộc tính khác với khóa chính chính. Nó cung cấp tính linh hoạt cho việc truy vấn và lọc dữ liệu, hỗ trợ truy vấn song song và là cần thiết để tối ưu hóa hiệu suất truy vấn trong khi đáp ứng các mẫu truy cập khác nhau.
Bây giờ khi chúng ta hiểu cách DynamoDB duy trì thứ tự trong một bộ sưu tập mục, chúng ta có thể thiết kế một lược đồ thay thế để hỗ trợ các mẫu truy cập bổ sung của chúng ta bằng cách sử dụng một GSI.
Tiếp cận 1 (không tối ưu):
Nhận thấy khả năng của bộ sưu tập mục để tổ chức dữ liệu một cách hiệu quả, chúng ta đã triển khai một Global Secondary Index (GSI) sử dụng một thuộc tính ngày với độ granular một ngày. Điều này cho phép chúng ta nhóm đơn hàng một cách hiệu quả cho mỗi ngày cụ thể. Để hỗ trợ điều này, chúng ta đã giới thiệu một thuộc tính bổ sung trong cấu trúc dữ liệu của chúng ta có tên là “gsi1_pk” để lưu trữ các giá trị ngày cần thiết.
Nếu bạn cần mở rộng mô hình dữ liệu hiện tại bằng cách thêm một thuộc tính bổ sung cho mỗi mục, bạn sẽ cần thực hiện một thao tác backfill. Để biết cách thực hiện điều này một cách chi tiết, vui lòng tham khảo bài viết blog chi tiết của chúng tôi.
Bây giờ, chúng ta có khả năng truy vấn dữ liệu một cách hiệu quả dựa trên các ngày cụ thể, chẳng hạn như lấy tất cả đơn hàng cho ngày 2023-10-03. Mặc dù phương pháp này hiệu quả cho các truy vấn trong một ngày, nhưng trường hợp sử dụng của chúng tôi đòi hỏi xử lý các khoảng thời gian lớn hơn. Ví dụ, để có được dữ liệu trong suốt một tuần đòi hỏi thực hiện bảy yêu cầu song song, mỗi yêu cầu cho mỗi ngày trong tuần. Mặc dù có thể quản lý được trong vòng một tuần, nhưng cần lưu ý rằng khi phạm vi ngày tăng lên, số lượng yêu cầu cần thiết cũng tăng tuyến tính, điều này có thể ảnh hưởng đến khả năng mở rộng.
Một ví dụ để lấy tất cả các đơn hàng từ ngày 2023-10-03 đến ngày 2023-10-06 sẽ trông như sau:
aws dynamodb query \
--table-name OrdersTable \
--index-name GSI1 \
--key-condition-expression "#gsi1_pk = :gsi1_pk_val" \
--expression-attribute-values '{":gsi1_pk_val":{"S":"2023-10-03"}}' \
--expression-attribute-names '{"#gsi1_pk":"gsi1_pk"}'
aws dynamodb query \
--table-name OrdersTable \
--index-name GSI1 \
--key-condition-expression "#gsi1_pk = :gsi1_pk_val" \
--expression-attribute-values '{":gsi1_pk_val":{"S":"2023-10-04"}}' \
--expression-attribute-names '{"#gsi1_pk":"gsi1_pk"}'
aws dynamodb query \
--table-name OrdersTable \
--index-name GSI1 \
--key-condition-expression "#gsi1_pk = :gsi1_pk_val" \
--expression-attribute-values '{":gsi1_pk_val":{"S":"2023-10-05"}}' \
--expression-attribute-names '{"#gsi1_pk":"gsi1_pk"}'
aws dynamodb query \
--table-name OrdersTable \
--index-name GSI1 \
--key-condition-expression "#gsi1_pk = :gsi1_pk_val" \
--expression-attribute-values '{":gsi1_pk_val":{"S":"2023-10-06"}}' \
--expression-attribute-names '{"#gsi1_pk":"gsi1_pk"}'
Số lượng yêu cầu: 4
Sản phẩm trả về: 4
Dung lượng tiêu thụ: 2
Mặc dù đơn giản để thực hiện, phương pháp này không hoạt động tốt khi số lượng ngày được truy vấn tăng lên. Một điểm đáng lưu ý là cần phải thực hiện yêu cầu ngay cả cho các ngày như 2023-10-05 khi không có dữ liệu tồn tại, gây ra các chi phí không cần thiết mà không mang lại bất kỳ thông tin liên quan nào.
Phương pháp 2 (tối ưu)
Một chiến lược cải tiến bao gồm việc tận dụng khóa sắp xếp, cho phép chúng ta sử dụng điều kiện một cách hiệu quả. Trong phương pháp này, chúng tôi chọn một giá trị cố định cho khóa phân vùng GSI của chúng tôi, hiệu quả gom góp tất cả dữ liệu vào một bộ sưu tập mục đơn. Khóa sắp xếp được định nghĩa là một dấu thời gian ISO 8601 (chuỗi) xuống đến độ chính xác mili giây. Các dấu thời gian này đã được lưu trữ trong các mục của chúng tôi dưới thuộc tính SK.
Trong bài viết này, chúng ta sẽ tìm hiểu về cách tạo một bộ sưu tập thống nhất chứa các mục được lưu trữ dưới thuộc tính khóa phân vùng GSI1_PK với giá trị cố định là 1. Nhờ đó, tất cả các mục trong bảng của chúng ta sẽ được sắp xếp theo thứ tự từ điển dựa trên thời gian tạo mục.
Bây giờ hãy lặp lại ví dụ của chúng ta để lấy tất cả các đơn đặt hàng từ ngày 03-10-2023 đến ngày 07-10-2023, trông giống như sau:
aws dynamodb query \
--table-name Orders \
--index-name GSI1 \
--key-condition-expression "#gsi1_pk = :gsi1_pk_val AND #SK between :from AND :to" \
--expression-attribute-values '{":gsi1_pk_val":{"S":"1"}, ":from":{"S":"2023-10-03"}, ":to":{"S":"2023-10-07"}}' \
--expression-attribute-names '{"#gsi1_pk":"gsi1_pk", "#SK":"SK"}'
Số lượng yêu cầu: 1
Số mục trả về: 4
Dung lượng tiêu thụ: 0.5
Phương pháp này không chỉ nâng cao hiệu suất mà còn cung cấp tính linh hoạt cao. Nếu nhu cầu kinh doanh của chúng ta phát triển để bao gồm các mẫu truy cập thay thế, chẳng hạn như lấy tất cả các đơn đặt hàng trong 30 phút gần đây, lấy 100 đơn hàng mới nhất hoặc truy cập 100 đơn hàng cũ nhất, mô hình dữ liệu của chúng ta đã trang bị chúng ta với tính đa dạng để thực hiện các truy vấn này một cách hiệu quả.
Tuy nhiên, việc sử dụng một giá trị cố định duy nhất làm khóa phân vùng có thể đưa ra các vấn đề về hiệu suất, chúng ta sẽ đề cập đến điều này sau trong bài viết này.
Mô hình dữ liệu cho trò chơi
DynamoDB thường được sử dụng để lưu trữ thông tin về trò chơi như điểm số và thông tin người chơi. Khả năng mở rộng và hiệu suất của nó làm cho nó phù hợp cho các ứng dụng trò chơi. Thiết kế schema linh hoạt cho phép điều chỉnh cơ chế trò chơi mà không cần sửa đổi cơ sở dữ liệu phức tạp, trong khi các hoạt động có độ trễ thấp đảm bảo cập nhật thời gian thực, biến nó thành một lựa chọn đáng tin cậy cho việc quản lý bảng xếp hạng, hồ sơ người chơi và thành tựu trong các ứng dụng trò chơi. Một mô hình dữ liệu ví dụ có thể trông giống như sau:
Trong ví dụ về mô hình dữ liệu này, chúng ta quan sát một khóa chính đơn giản được xác định là một khóa phân vùng đại diện cho định danh duy nhất của người dùng, được gọi là userId. Thiết kế này đã được chứng minh là hiệu quả cao đối với các truy vấn khóa giá trị đơn giản tập trung vào userId, chẳng hạn như việc lấy điểm số cho user0011 hoặc cập nhật điểm số cho user30046.
Hãy tưởng tượng một trường hợp sử dụng mới yêu cầu tạo ra bảng xếp hạng hiển thị 10 người dùng hàng đầu và 50 người dùng hàng đầu trong trò chơi của chúng tôi. Mặc dù có vẻ hợp lý khi giới thiệu thuộc tính “score” như là khóa sắp xếp để hỗ trợ mẫu truy cập này, nhưng cách tiếp cận này đối mặt với hai thách thức quan trọng khiến nó trở nên không thực tế. Trong DynamoDB, bạn không thể sửa đổi khóa chính trong bảng chính, điều này làm trở ngại cho việc cập nhật hiệu quả giá trị điểm số. Ví dụ, bạn không thể sử dụng UpdateItem để thay đổi các thuộc tính khóa chính. Thay vào đó, bạn phải xóa mục và sau đó sử dụng PutItem để giới thiệu một mục mới với các thuộc tính mong muốn. Quan trọng hơn, DynamoDB giới hạn việc sắp xếp cho bộ sưu tập mục, điều này có nghĩa trong ngữ cảnh này, mỗi mục của người dùng sẽ thực sự bao gồm một bộ sưu tập mục chỉ bao gồm một mục duy nhất, làm suy yếu tính khả thi của chức năng bảng xếp hạng mong muốn.
Chúng ta có thể sử dụng một giải pháp tương tự như bảng đơn hàng thương mại điện tử của chúng ta ở đây, tạo một GSI với giá trị khóa phân vùng tĩnh, để tất cả các mục của người dùng được chứa trong một bộ sưu tập mục duy nhất và sử dụng thuộc tính điểm số làm khóa sắp xếp của GSI. Vì vậy, chúng ta bao gồm gsi1_pk là một thuộc tính trong mô hình dữ liệu của chúng ta:
Trong tình huống hiện tại của chúng ta, khi cần lấy thông tin của người dùng có điểm số cao nhất, chúng ta cần truy vấn dữ liệu từ bảng DynamoDB một cách hiệu quả. Để đạt được điều này, chúng ta có thể tận dụng các dịch vụ AWS và cấu hình cụ thể để tối ưu hóa cả chi phí và hiệu suất.
**Truy Vấn Dữ Liệu Một Cách Hiệu Quả**
Để lấy thông tin của những người dùng có điểm số cao nhất, chúng ta có thể sử dụng API Query trong DynamoDB. Trong tình huống này, chúng ta muốn lấy dữ liệu theo thứ tự giảm dần để có được điểm số cao nhất trước. Chúng ta có thể đạt được điều này bằng cách đặt thuộc tính `ScanIndexForward` thành `False`. Dưới đây là ví dụ về lệnh AWS CLI:
aws dynamodb query \
--table-name GameTable \
--index-name GSI1 \
--key-condition-expression "#gsi1_pk = :gsi1_pk_val" \
--expression-attribute-values '{":gsi1_pk_val":{"S":"1"}}' \
--expression-attribute-names '{"#gsi1_pk":"gsi1_pk"}' \
--no-scan-index-forward \
--limit 10
Lệnh này sẽ truy vấn và lấy ra 10 người dùng có điểm số cao nhất một cách hiệu quả từ bảng DynamoDB của chúng ta, giảm thiểu việc tiêu thụ không cần thiết của năng lực đọc.
**Xem Xét Về Chi Phí**
Khi làm việc với các chỉ mục phụ toàn cầu (GSI) trong DynamoDB, việc xem xét các yếu tố về chi phí là rất quan trọng. Một khía cạnh quan trọng là việc chiếu thuộc tính, quyết định xem các thuộc tính nào sẽ được bao gồm trong chỉ mục. Có hai tùy chọn chiếu thuộc tính: INCLUDE và ALL.
– Chiếu thuộc tính `INCLUDE` cho phép chúng ta chọn một phần của các thuộc tính để bao gồm trong chỉ mục, giúp giảm chi phí lưu trữ.
– Chiếu thuộc tính `ALL` bao gồm tất cả các thuộc tính trong chỉ mục, làm đơn giản hóa việc truy vấn nhưng tăng chi phí lưu trữ và năng lực thông qua.
Cân nhắc cân bằng giữa tối ưu hóa chi phí và tính năng là quan trọng. Chúng ta cần đánh giá sự quan trọng và tần suất truy cập của các thuộc tính được chiếu và điều chỉnh chúng với ngân sách của mình để đưa ra quyết định thông minh về tối ưu hóa chi phí trong khi duy trì hiệu suất.
**Xem Xét Về Hiệu Suất**
Sử dụng một giá trị duy nhất làm khóa phân vùng trong mô hình dữ liệu của DynamoDB có thể có những điểm đánh đổi ảnh hưởng đến hiệu suất. Khi một giá trị tĩnh được sử dụng làm khóa phân vùng cho GSI, tất cả các mục dữ liệu sẽ tập trung trong một phân vùng. Sự tập trung này có thể dẫn đến các vấn đề về hiệu suất, đặc biệt là trong các tình huống có tỷ lệ đọc hoặc ghi cao.
Để tránh những vấn đề này, cân nhắc về giới hạn phân vùng của DynamoDB. Phương pháp này phù hợp cho các bảng mà lưu lượng ghi không vượt quá 1000 WCU (Đơn vị Năng Lực Ghi) và lưu lượng đọc chỉ mục không vượt quá 3000 RCU (Đơn vị Năng Lực Đọc).
**Tối Ưu Hiệu Suất Qua Việc Chia Khóa Phân Vùng**
Chia khóa phân vùng là một kỹ thuật để phân phối công việc một cách đều đặn qua nhiều phân vùng, tăng cường hiệu suất và khả năng mở rộng. Nó mang lại một số lợi ích:
– Khả năng mở rộng tốt hơn khi ứng dụng phát triển.
– Hiệu suất được cải thiện bằng cách tránh các phân vùng quá tải.
– Linh hoạt trong việc phân phối dữ liệu dựa trên mẫu truy cập thay đổi và yêu cầu ứng dụng tiến hóa.
Một phương pháp để chia khóa phân vùng là chia khóa phân vùng theo dải được tính toán. Trong phương pháp này, các khóa phân vùng được gán giá trị từ 1 đến N, với N được xác định bằng công thức sau:
“`plaintext
N = expected_peak_throughput / 1000
“`
Bằng cách chia lượng ghi dự kiến cao điểm cho 1000, chúng ta có thể tính toán số lượng phân vùng (N) cần thiết để phân phối công việc đều đặn.