Computer >> Máy Tính >  >> Lập trình >> Redis

Có lẽ bạn đang nghĩ sai về luồng Redis

Cá nhân tôi cảm thấy tội lỗi khi mô tả các luồng theo cách sai — Tôi đã định nghĩa nó là “một chuỗi các phần tử giống như bản đồ băm, được sắp xếp theo thời gian, dưới một khóa duy nhất”. Điều này không chính xác . Bit cuối cùng liên quan đến thời gian và chìa khóa là OK, nhưng bit đầu tiên đều sai.

Hãy cùng xem lý do tại sao các luồng bị hiểu nhầm và cách chúng thực sự hoạt động. Chúng tôi sẽ đánh giá mặt tốt và mặt xấu của sự hiểu lầm này và cách nó có thể ảnh hưởng đến phần mềm của bạn. Cuối cùng, chúng tôi sẽ xem xét một số mẫu không rõ ràng tận dụng các đặc tính ít được biết đến của Redis Streams.

Bối cảnh

Trước tiên, hãy xem lệnh XADD, vì đây là nơi bắt đầu hiểu lầm. Chữ ký lệnh giống như trong tài liệu redis.io chính thức trông giống như sau:

XADD key ID field value [field value ...]

phím là tự giải thích. id là tổ hợp dấu thời gian / chuỗi cho mục nhập mới, nhưng trên thực tế, nó hầu như luôn luôn * để chỉ ra việc tạo tự động. Căn nguyên của sự nhầm lẫn này bắt đầu với trường và giá trị. Nếu bạn nhìn vào chữ ký lệnh cho HSET, bạn sẽ thấy một mẫu khá giống:

HSET key field value [field value ...]

Chữ ký cho hai lệnh chỉ là một đối số duy nhất và đối số đó trong XADD hầu như luôn luôn là một * duy nhất. Trông khá giống nhau, sử dụng các thuật ngữ giống nhau, phải giống nhau, phải không?

ĐƯỢC RỒI. Để tiếp tục vấn đề, hãy để Redis sang một bên và xem cách ngôn ngữ lập trình xử lý các cặp giá trị trường. Đối với hầu hết các phần, bất kể ngôn ngữ, cách biểu diễn nhất để khớp trường-giá trị là với một tập hợp (không lặp lại) các trường tương quan với các giá trị — một số ngôn ngữ sẽ giữ lại thứ tự của các trường và một số thì không. Hãy xem xét sâu hơn với một so sánh nhỏ giữa các ngôn ngữ:

Điều này ánh xạ độc đáo đến các hàm băm của Redis — tất cả những thứ này đều có thể nêu rõ các thuộc tính của một hàm băm, không có thứ tự và không có lần lặp lại. Mảng PHP, mảng Python và bản đồ JavaScript có thể xác định thứ tự trường, nhưng nếu bạn đang làm việc với hàm băm trong Redis, thì điều đó không quan trọng, bạn chỉ cần biết rằng bạn không thể phụ thuộc vào thứ tự này ở cấp ứng dụng .

Đối với hầu hết mọi người, kết luận tự nhiên là vì các chữ ký lệnh của HSET và XADD có mối tương quan, nên đổi lại chúng có thể có mối tương quan tương tự. Thật vậy, ở cấp độ giao thức trong RESP2, cả hai đều được trả về dưới dạng Mảng RESP xen kẽ. Điều này được tiếp tục trong các phiên bản đầu tiên của RESP3 trong đó phản hồi cho HGETALL và XREAD đều là bản đồ (sẽ nói thêm về điều đó sau).

Một lỗi đã thay đổi suy nghĩ của tôi

Thông thường, tôi viết mã bằng JavaScript và đôi khi là Python. Là một người giao tiếp về Redis, tôi cần tiếp cận càng nhiều người càng tốt và hai ngôn ngữ này được hiểu khá rõ, vì vậy, một tỷ lệ phần trăm cao trong thế giới nhà phát triển sẽ hiểu được mã demo hoặc mã mẫu. Gần đây, tôi đã có cơ hội nói chuyện tại một hội nghị PHP và cần chuyển đổi một số mã Python hiện có sang PHP. Tôi đã sử dụng PHP liên tục trong gần 20 năm, nhưng đó chưa bao giờ là niềm đam mê của tôi. Bản demo cụ thể không phù hợp tốt với thực thi kiểu mod_php, vì vậy tôi đã sử dụng swoole và thực thi đồng quy trình của nó (một lưu ý nhỏ là cảm thấy thoải mái trong thế giới JavaScript, swoole làm cho PHP rất, rất quen thuộc với tôi). Thư viện đã lỗi thời và yêu cầu gửi các lệnh Redis thô mà không có bất kỳ sự hỗ trợ nào của thư viện khách hàng thực trong việc giải mã các kết quả trả về theo cách cấp cao. Nói chung, việc gửi các lệnh Redis thô và giải mã kết quả cung cấp nhiều quyền kiểm soát hơn một chút và không phức tạp.

Vì vậy, khi gửi các lệnh tới XADD, tôi đang xây dựng phần giá trị trường và đã gặp lỗi từng lỗi một (chuyển lỗi này đến việc quay trở lại PHP sau nhiều năm vắng bóng). Điều này dẫn đến việc tôi vô tình gửi một cái gì đó dọc theo dòng:

XADD myKey * field0 value1 field0 value2 field2 value3

Thay vì gửi các trường và giá trị tương quan ( field0 thành value0 vân vân).

Sau đó trong đoạn mã, tôi đã đưa các kết quả của XREAD vào một mảng PHP hiện có (có tính chất liên kết) trong một vòng lặp, gán mỗi trường làm khóa với mỗi giá trị và bỏ qua bất kỳ thứ gì đã được thiết lập. Vì vậy, tôi đã bắt đầu với một mảng như thế này:

array(1) {
  ["foo"]=>
  string(3) "bar"
}

Và kết thúc với một mảng như thế này:

array(3) {
  ["foo"]=>
  string(3) "bar"
  ["field0"]=>
  string(6) "value1"
  ["field2"]=>
  string(6) "value3"
}

Tôi không thể hiểu được làm thế nào điều đó có thể xảy ra. Tôi đã nhanh chóng tìm ra lỗi tại sao value1 được chỉ định cho field0 (lỗi từng lỗi được đề cập ở trên trong XADD của tôi), nhưng tại sao không phải là field0 đặt thành value2 ? Trong HSET, hành vi thêm trường này về cơ bản là nâng cấp — cập nhật một trường nếu nó tồn tại, nếu không, hãy thêm trường và đặt giá trị.

Kiểm tra nhật ký MONITOR và phát lại các giá trị, tôi đã chạy XREAD như sau:

> XREAD STREAMS myKey 0
1) 1) "myKey"
   2) 1) 1) "1592409586907-0"
         2) 1) "field0"
            2) "value1"
            3) "field0"
            4) "value2"
            5) "field2"
            6) "value3"

Các lần lặp lại có mặt và được ghi lại, không lặp lại; bổ sung đơn đặt hàng được giữ nguyên . Đây không có gì giống như một băm!

Suy nghĩ về điều này bằng cách sử dụng JSON như một giá trị gần đúng, tôi nghĩ rằng các mục nhập luồng trông như thế này:

{
  "field1"  : "value",
  "field2"  : "value",
  "field3"  : "value"
}

Nhưng chúng thực sự trông như thế này:

[
  ["field1", "value"],
  ["field2", "value"],
  ["field3", "value"]
]

Điều này có nghĩa là gì?

Tin tốt

Nếu bạn có mã hiện đang hoạt động với Luồng và bạn cho rằng các mục nhập giống như bản đồ băm, thì có lẽ bạn vẫn ổn. Bạn chỉ cần để ý các lỗi tiềm ẩn liên quan đến việc đưa vào các trường trùng lặp, vì chúng có thể không hoạt động theo cách bạn mong đợi trong một ứng dụng nhất định. Điều này có thể không áp dụng trong mọi trường hợp hoặc thư viện máy khách, nhưng một phương pháp hay là cung cấp cấu trúc dữ liệu không cho phép lặp lại (xem ở trên) và tuần tự hóa cấu trúc này thành các đối số khi cung cấp chúng cho XADD. Các lĩnh vực duy nhất trong và các lĩnh vực duy nhất ngoài.

Tin xấu

Không phải tất cả các thư viện và công cụ ứng dụng khách đều làm đúng như vậy. Các thư viện máy khách cấp tương đối thấp (không toàn diện:node_redis, Rentis) không làm được nhiều việc thay đổi kết quả đầu ra từ Redis thành các cấu trúc ngôn ngữ. Các thư viện cấp cao hơn do trừu tượng hóa trả về thực tế của Redis thành các cấu trúc ngôn ngữ — bạn nên kiểm tra xem liệu thư viện bạn chọn có đang thực hiện điều này hay không và đặt vấn đề nếu có. Một số thư viện cấp cao hơn đã hiểu nó ngay từ đầu (stackexchange.redis), do đó, kudo sẽ có ở đó.

Phần khác hơi tệ:nếu bạn là người sử dụng RESP3 rất sớm, bạn có thể đã gặp phải trường hợp XREAD / XREADGROUP trả về loại bản đồ RESP3. Cho đến đầu tháng 4, phiên bản Redis 6 đang trong quá trình phát triển đã trả lại các bản đồ một cách khó hiểu với những lần lặp lại khi đọc Luồng. Rất may, điều này đã được giải quyết và phiên bản GA của Redis 6 — lần đầu tiên bạn thực sự nên sử dụng RESP3 trong sản xuất — được giao hàng với lợi tức thích hợp cho XREAD / XREADGROUP.

Phần thú vị

Vì tôi đã xem qua cách bạn có thể sai về Luồng, chúng ta hãy suy nghĩ một chút về cách bạn có thể tận dụng cấu trúc trước đây đã bị hiểu lầm này.

Áp dụng ý nghĩa ngữ nghĩa để sắp xếp trong các mục nhập Luồng

Vì vậy, bạn thực sự có ba vectơ để chơi với mẫu này. Hãy tưởng tượng lưu trữ một đường dẫn cho đồ họa vector. Mỗi mục nhập Luồng sẽ là một đa giác hoặc đường dẫn duy nhất và các trường và giá trị sẽ là tọa độ. Ví dụ:lấy đoạn SVG này:

<polyline points="50,150 50,200 200,200 200,100">

Điều này có thể được hiểu là:

> XADD mySVG * 50 150 50 200 200 200 200 100

Mỗi hình dạng bổ sung sẽ là một mục nhập khác trên cùng một khóa. Nếu bạn đã cố gắng làm điều gì đó như vậy với Redis Hash, bạn sẽ chỉ có hai tọa độ và không có đảm bảo thứ tự. Phải thừa nhận rằng bạn có thể làm điều này với những thứ như trường bit, nhưng bạn sẽ mất rất nhiều tính linh hoạt liên quan đến chiều dài và kích thước tọa độ. Với Luồng, bạn thậm chí có thể làm điều gì đó gọn gàng với dấu thời gian để thể hiện một loạt các hình dạng xuất hiện theo thời gian.

Tạo một tập hợp các mục được sắp xếp theo thứ tự thời gian

Cái này yêu cầu một bản hack nhỏ, nhưng có thể cung cấp rất nhiều chức năng. Hãy tưởng tượng bạn đang giữ một chuỗi dữ liệu giống như mảng. Một cách hiệu quả, một mảng các mảng — trong JSON, bạn có thể nghĩ nếu nó giống như sau:

[
  ["A New Hope", "The Empire Strikes Back", "Return of the Jedi"],
  ["The Phantom Menace", "Attack of the Clones", "Revenge of the Sith"],
  ["The Force Awakens", "The Last Jedi", "The Rise of Skywalker"]
]

Bạn có thể nói rõ điều này như một loạt các mục Luồng với một sắc thái nhỏ:bạn cần đảm bảo số lượng phần tử (giả) trong danh sách bên trong của bạn không phải là số lẻ. Nếu chúng kỳ lạ, như ở trên, bạn sẽ cần ghi lại điều đó bằng cách nào đó — đây là cách tôi thực hiện với một chuỗi trống:

> XADD starwars * "A New Hope" "The Empire Strikes Back" "Return of the Jedi" ""
"1592427370458-0"
> XADD starwars * "The Phantom Menace" "Attack of the Clones" "Revenge of the Sith" ""
"1592427393492-0"
> XADD starwars * "The Force Awakens" "The Last Jedi" "The Rise of Skywalker" ""
"1592427414475-0"
> XREAD streams starwars 0
1# "starwars" => 
   1) 1) "1592427370458-0"
      2) 1) "A New Hope"
         2) "The Empire Strikes Back"
         3) "Return of the Jedi"
         4) ""
   2) 1) "1592427393492-0"
      2) 1) "The Phantom Menace"
         2) "Attack of the Clones"
         3) "Revenge of the Sith"
         4) ""
   3) 1) "1592427414475-0"
      2) 1) "The Force Awakens"
         2) "The Last Jedi"
         3) "The Rise of Skywalker"
         4) ""

Bạn thu được rất nhiều trong mô hình này với chi phí (nhỏ) là phải lọc ra bất kỳ chuỗi nào có độ dài bằng 0.

Luồng dưới dạng bộ nhớ cache phân trang

Một điều khó khăn mà bạn thường thấy là danh sách các mặt hàng trên một trang web (thương mại điện tử, bảng tin, v.v.). Điều này thường được lưu vào bộ nhớ cache nhưng tôi thấy mọi người đã cố gắng tìm ra phương pháp tốt nhất để lưu vào bộ nhớ cache loại dữ liệu này — bạn có lưu toàn bộ kết quả vào bộ nhớ cache vào một thứ gì đó giống như một tập hợp được sắp xếp và phân trang ra bên ngoài bằng ZRANGE không, hay bạn lưu trữ các trang đầy đủ tại các phím chuỗi? Cả hai cách đều có ưu điểm và nhược điểm.

Hóa ra, Luồng thực sự hoạt động cho điều này. Lấy ví dụ, danh sách thương mại điện tử. Bạn có một loạt các mục, mỗi mục có một ID. Các mục đó được liệt kê trong một loạt các loại hữu hạn thường có sự đảo ngược (A-Z, Z-A, thấp đến cao, cao xuống thấp, xếp hạng cao nhất đến thấp nhất, xếp hạng thấp nhất đến cao nhất, v.v.).

Để lập mô hình loại dữ liệu này trong Luồng, bạn sẽ xác định kích thước "đoạn" cụ thể và tạo mục nhập đó. Tại sao lại có nhiều phần và không phải trang kết quả đầy đủ tại một mục nhập? Điều này cho phép bạn có các trang có kích thước khác nhau trong phân trang của mình (ví dụ:10 mục trên mỗi trang có thể được tạo thành 2 phần 5 mỗi trang, trong khi 25 mục trên mỗi trang sẽ là 5 phần 5 mỗi trang). Mỗi mục nhập sẽ chứa các trường ánh xạ tới ID sản phẩm và các giá trị sẽ là dữ liệu sản phẩm. Hãy xem ví dụ đơn giản này với kích thước phân đoạn thấp giả tạo:

Có lẽ bạn đang nghĩ sai về luồng Redis

Khi bạn muốn truy xuất các giá trị đã lưu trong bộ nhớ cache, bạn sẽ chạy XRANGE với đối số COUNT được đặt thành số phần tạo nên trang kết quả cho giao diện của bạn. Vì vậy, nếu bạn muốn lấy trang đầu tiên trong số bốn mục, bạn sẽ chạy:

> XRANGE listcache - + COUNT 2
1) 1) "0-1"
   2) 1) "123"
      2) "{ \"Red Belt\", ... }"
      3) "456"
      4) "{ \"Yellow Belt\", ... }"
2) 1) "0-2"
   2) 1) "789"
      2) "{ \"Blue Belt\", ... }"
      3) "012"
      4) "{ \"Purple Belt\", ... }"

Để có trang thứ hai trong số 4 mục, bạn sẽ cần cung cấp ID luồng giới hạn dưới tăng lên 1, trong trường hợp của chúng tôi, giới hạn dưới sẽ là 0- 2 .

> XRANGE listcache 0-3 + COUNT 2
1) 1) "0-3"
   2) 1) "345"
      2) "{ \"Black Belt\", ... }"
      3) "678"
      4) "{ \"Red Boa\", ... }"
2) 1) "0-4"
   2) 1) "901"
      2) "{ \"Yellow Boa\", ... }"
      3) "234"
      4) "{ \"Green Belt\", ... }"

Điều này mang lại lợi thế về độ phức tạp tính toán so với Bộ hoặc Danh sách được sắp xếp vì XRANGE thực sự là O (1) trong việc sử dụng này, nhưng có một số điều cần lưu ý:

  • XREVRANGE có thể được sử dụng để đảo ngược, tuy nhiên, điều này sẽ chỉ đảo ngược thứ tự của “các phần”. Bên trong mỗi đoạn, bạn sẽ cần đảo ngược thứ tự trong logic ứng dụng, điều này sẽ tương đối nhỏ.
  • Việc tìm kiếm các phần khác nhau của danh sách là "miễn phí" nếu bạn đặt tuyến tính các ID luồng theo cách thủ công, vì vậy đoạn 1 là ID luồng 0-1 , đoạn 2 là ID luồng 0-2 và như thế. Lưu ý rằng bạn phải thực hiện việc này bắt đầu bằng 1 thay vì 0 vì bạn không thể thêm mục Luồng tại 0-0

Giống như bất kỳ khóa nào, bạn có thể sử dụng thời hạn sử dụng để quản lý thời gian Luồng tồn tại. Một ví dụ về cách có thể thực hiện điều này là trong stream-row-cache.

Tôi hy vọng bài đăng này cung cấp cho bạn một số ngữ cảnh bổ sung về cách Luồng thực sự hoạt động và cách bạn có thể tận dụng các thuộc tính chưa được biết đến nhiều này của Luồng trong ứng dụng của mình.