Computer >> 컴퓨터 >  >> 프로그램 작성 >> Redis

Redis Streams에 대해 잘못 생각하고 있을 것입니다.

나는 개인적으로 잘못된 방식으로 스트림을 기술한 것에 대해 죄책감을 가지고 있습니다. 저는 이것을 "하나의 키 아래, 시간순으로 정렬된 일련의 해시맵과 같은 요소"로 정의했습니다. 잘못되었습니다 . 시간과 키에 관한 마지막 비트는 괜찮지만 첫 번째 비트는 모두 틀립니다.

스트림이 왜 잘못 이해되고 실제로 어떻게 작동하는지 살펴보겠습니다. 우리는 이 오해의 좋은 점과 나쁜 점과 그것이 소프트웨어에 어떤 영향을 미칠 수 있는지 평가할 것입니다. 마지막으로 Redis Streams의 잘 알려지지 않은 속성을 활용하는 몇 가지 명확하지 않은 패턴을 살펴보겠습니다.

배경

먼저 XADD 명령을 살펴보겠습니다. 여기서부터 오해가 시작됩니다. 공식 redis.io 문서에 있는 명령 서명은 다음과 같습니다.

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

자명하다. <마크>아이디 는 새 항목에 대한 타임스탬프/시퀀스 콤보이지만 실제로는 거의 항상 *가 자동 생성을 나타냅니다. 이러한 혼란의 근원은 필드와 값에서 시작됩니다. HSET의 명령 서명을 보면 매우 유사한 패턴을 볼 수 있습니다.

HSET key field value [field value ...]

두 명령의 서명은 단 하나의 인수가 꺼져 있고 XADD의 해당 인수는 거의 항상 단일 *입니다. 꽤 비슷하게 생겼고, 같은 용어를 사용하고, 같아야 하지 않을까요?

확인. 문제를 계속하기 위해 Redis는 제쳐두고 프로그래밍 언어가 필드-값 쌍을 처리하는 방법을 살펴보겠습니다. 대부분의 경우 언어에 관계없이 필드 값을 표현하는 가장 대표적인 방법은 값과 상관 관계가 있는 필드 집합(반복 없음)을 사용하는 것입니다. 일부 언어는 필드 순서를 유지하고 일부는 그렇지 않습니다. 언어 간 비교를 통해 더 자세히 살펴보겠습니다.

이것은 Redis 해시에 잘 매핑됩니다. 이 모든 것이 순서가 없고 반복이 없는 해시의 속성을 명확하게 나타낼 수 있습니다. PHP 배열, Python dict 및 JavaScript 맵은 필드 순서를 정의할 수 있지만 Redis에서 해시로 작업하는 경우 중요하지 않습니다. 애플리케이션 수준에서 이 순서에 의존할 수 없다는 것을 알아야 합니다. .

대부분의 사람들에게 자연스러운 결론은 HSET 및 XADD의 명령 서명에 상관 관계가 있으므로 그 대가로 유사한 상관 관계가 있을 수 있다는 것입니다. 실제로 RESP2의 프로토콜 수준에서 이 둘은 인터리브된 RESP 배열로 반환됩니다. 이것은 HGETALL 및 XREAD에 대한 응답이 모두 맵인 초기 버전의 RESP3에서 계속됩니다(나중에 자세히 설명).

버그가 내 마음을 바꿨습니다

일반적으로 JavaScript로 코딩하고 때로는 Python으로 코딩합니다. Redis에 대해 소통하는 사람으로서 저는 가능한 한 많은 사람들에게 다가갈 필요가 있으며 이 두 언어는 꽤 잘 이해되고 있으므로 어느 쪽의 데모나 샘플 코드도 개발자 세계의 높은 비율로 이해할 것입니다. 최근에 PHP 컨퍼런스에서 이야기할 기회가 있었고 기존 Python 코드를 PHP로 변환해야 했습니다. 저는 거의 20년 동안 PHP를 켜고 끌 수 있었지만 결코 제 열정이 아니었습니다. 특정 데모는 mod_php 스타일 실행에 잘 맞지 않았기 때문에 swoole과 그 공동 루틴 실행을 사용했습니다. 라이브러리는 약간 구식이었고 높은 수준의 방식으로 반환을 디코딩하는 데 실제 클라이언트 라이브러리 지원 없이 원시 Redis 명령을 보내야 했습니다. 일반적으로 원시 Redis 명령을 보내고 결과를 디코딩하면 제어가 조금 더 쉬워지고 번거롭지 않습니다.

그래서 XADD에 명령을 보낼 때 필드 값 부분을 구축하고 있었고 오프바이원 오류가 발생했습니다(몇 년 동안 부재한 후 PHP로 다시 뛰어들기 위해 이것을 분필). 이로 인해 의도치 않게 다음과 같은 내용을 보냈습니다.

XADD myKey * field0 value1 field0 value2 field2 value3

상관 필드 및 값을 보내는 대신(field0 값0으로 등등).

코드 뒷부분에서 XREAD 결과를 루프의 기존 PHP 배열(연관형)에 넣고 각 필드를 각 값의 키로 할당하고 이미 설정된 모든 항목은 건너뜁니다. 그래서 저는 다음과 같은 배열로 시작했습니다:

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

그리고 다음과 같은 배열로 끝났습니다.

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

나는 그것이 어떻게 가능했는지 짐작할 수 없었다. 왜 value1인지에 대한 버그를 빠르게 추적할 수 있었습니다. field0에 할당되었습니다. (위에서 언급한 내 XADD의 off-by-one 오류), 하지만 field0이(가) 아닌 이유는 무엇입니까? value2로 설정 ? HSET에서 필드를 추가하기 위한 이 동작은 기본적으로 upsert입니다. 필드가 있으면 업데이트하고, 그렇지 않으면 필드를 추가하고 값을 설정합니다.

MONITOR 로그를 확인하고 값을 재생하면서 다음과 같이 XREAD를 실행했습니다.

> 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"

반복이 존재하고 기록되며, 업데이트되지 않습니다. 추가로 주문이 보존됩니다. 이것은 해시와 같은 것이 아닙니다!

JSON을 근사치로 사용하여 이것을 생각하면서 스트림 항목이 다음과 같다고 생각했습니다.

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

하지만 실제로는 다음과 같습니다.

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

이것은 무엇을 의미합니까?

좋은 소식

현재 Streams에서 작동하는 코드가 있고 항목이 해시 맵과 같다고 가정하면 문제가 없을 것입니다. 주어진 응용 프로그램에서 예상대로 작동하지 않을 수 있으므로 중복 필드를 넣는 것과 관련하여 잠재적인 버그를 관찰하기만 하면 됩니다. 이는 모든 상황이나 클라이언트 라이브러리에 적용되지 않을 수 있지만 반복을 허용하지 않는 데이터 구조를 제공하고(위 참조) 이를 XADD에 제공할 때 인수로 직렬화하는 것이 좋습니다. 고유 필드 인 및 고유 필드 아웃.

나쁜 소식

모든 클라이언트 라이브러리와 도구가 올바른 것은 아닙니다. 상대적으로 낮은 수준의 클라이언트 라이브러리(전체가 아닌:node_redis, hiredis)는 Redis의 출력을 언어 구성으로 변경하는 것만큼 많은 일을 하지 않습니다. 다른 상위 수준 라이브러리가 하는 Redis의 실제 반환을 언어 구성으로 추상화합니다. 선택한 라이브러리에서 이 작업을 수행하는지 확인하고 문제가 있는 경우 문제를 제기해야 합니다. 일부 상위 수준 라이브러리(stackexchange.redis)는 처음부터 바로 얻었으므로 여기에서 찬사를 보냅니다.

약간 나쁜 다른 부분:RESP3의 얼리 어답터라면 RESP3 맵 유형을 반환하는 XREAD / XREADGROUP을 경험했을 것입니다. 4월 초까지 개발 중인 Redis 6 버전은 Streams를 읽을 때 반복되는 맵을 혼란스럽게 반환했습니다. 고맙게도 이 문제가 해결되었고 프로덕션에서 RESP3를 처음으로 실제로 사용했어야 하는 Redis 6의 GA 버전이 XREAD/XREADGROUP에 대한 적절한 반환과 함께 배송되었습니다.

재미있는 부분

당신이 Streams에 대해 어떻게 잘못 알고 있는지에 대해 살펴보았으므로 지금까지 잘못 이해된 이 구조를 활용할 수 있는 방법에 대해 잠시 생각해 보겠습니다.

스트림 항목의 순서에 의미론적 의미 적용

따라서 실제로 이 패턴에서 사용할 3개의 벡터가 있습니다. 벡터 그래픽을 위한 경로를 저장한다고 상상해 보십시오. 각 스트림 항목은 고유한 다각형 또는 경로가 되고 필드와 값은 좌표가 됩니다. 예를 들어 다음 SVG 조각을 사용하세요.

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

이것은 다음과 같이 표현될 수 있습니다:

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

각각의 추가 모양은 동일한 키에 대한 또 다른 항목이 됩니다. Redis Hash로 이와 같은 작업을 시도했다면 좌표가 두 개뿐이며 주문 보장이 없습니다. 물론 비트 필드와 같은 작업을 통해 이 작업을 수행할 수 있지만 길이 및 좌표 크기와 관련하여 많은 유연성을 잃게 됩니다. Streams를 사용하면 시간이 지남에 따라 나타나는 일련의 모양을 나타내기 위해 타임스탬프로 깔끔한 작업을 수행할 수도 있습니다.

시퀀싱된 항목의 시간 순서 세트 만들기

이것은 작은 해킹이 필요하지만 많은 기능을 제공할 수 있습니다. 배열과 같은 데이터 시퀀스를 유지한다고 상상해보십시오. 효과적으로 배열 배열—JSON에서는 다음과 같이 생각할 수 있습니다.

[
  ["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"]
]

이것을 하나의 작은 뉘앙스가 있는 일련의 스트림 항목으로 설명할 수 있습니다. 내부 목록의 (의사) 요소 수가 홀수인지 확인해야 합니다. 위와 같이 홀수인 경우 어떻게든 기록해야 합니다. 빈 문자열로 이 작업을 수행하는 방법은 다음과 같습니다.

> 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) ""

길이가 0인 문자열을 필터링해야 하는 (사소한) 비용으로 이 패턴에서 많은 것을 얻을 수 있습니다.

페이지 매김 캐시로서의 스트림

꽤 자주 볼 수 있는 까다로운 것은 사이트의 항목 목록(전자 상거래, 게시판 등)입니다. 이것은 일반적으로 캐시되지만 사람들은 이러한 유형의 데이터를 캐시하는 가장 좋은 방법을 찾으려고 노력하는 것을 보았습니다. 전체 결과 세트를 정렬된 세트와 같은 것으로 캐시하고 ZRANGE를 사용하여 바깥쪽으로 페이지를 매기거나 전체 페이지를 저장합니까? 문자열 키에서? 두 가지 방법 모두 장점과 단점이 있습니다.

결과적으로 Streams는 실제로 이것을 위해 작동합니다. 예를 들어 전자 상거래 목록을 살펴보겠습니다. 각각 ID가 있는 일련의 항목이 있습니다. 이러한 항목은 일반적으로 반전이 있는 유한 계열의 종류로 나열됩니다(A-Z, Z-A, 낮음에서 높음, 높음에서 낮음, 최고에서 최저 등급, 최저에서 최고 등급 등).

스트림에서 이러한 유형의 데이터를 모델링하려면 특정 "청크" 크기를 결정하고 이를 항목으로 만듭니다. 항목에서 전체 결과 페이지가 아닌 청크가 필요한 이유는 무엇입니까? 이를 통해 페이지 매김에 다양한 크기의 페이지를 가질 수 있습니다(예:페이지당 10개 항목은 각각 5개씩 2개의 청크로 구성될 수 있는 반면 페이지당 25개는 각각 5개씩 5개의 청크로 구성될 수 있음). 각 항목에는 제품 ID에 매핑되는 필드가 포함되며 값은 제품 데이터가 됩니다. 인위적으로 작은 청크 크기를 사용하여 이 단순화된 예를 살펴보십시오.

Redis Streams에 대해 잘못 생각하고 있을 것입니다.

캐시된 값을 검색하려면 COUNT 인수를 인터페이스의 결과 페이지를 구성하는 청크 수로 설정하여 XRANGE를 실행합니다. 따라서 4개 항목의 첫 페이지를 얻으려면 다음을 실행합니다.

> 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\", ... }"

4개 항목의 두 번째 페이지를 얻으려면 1씩 증가하는 하한 스트림 ID를 제공해야 합니다. 이 경우 하한은 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\", ... }"

이것은 XRANGE가 이 사용에서 사실상 O(1)이기 때문에 Sorted Sets 또는 Lists에 비해 계산상의 복잡성 이점을 제공하지만 명심해야 할 몇 가지 사항이 있습니다.

  • XREVRANGE는 반대로 사용할 수 있지만 "청크"의 순서만 반대로 할 수 있습니다. 각 청크 내에서 상대적으로 간단해야 하는 애플리케이션 로직의 순서를 반대로 해야 합니다.
  • 스트림 ID를 선형으로 수동으로 설정하면 목록의 다른 부분을 찾는 것이 "무료"이므로 청크 1은 스트림 ID 0-1입니다. , 청크 2는 스트림 ID 0-2입니다. 등등. 0-0에 스트림 항목을 추가할 수 없으므로 0 대신 1부터 시작해야 합니다.

다른 키와 마찬가지로 만료를 사용하여 스트림이 유지되는 기간을 관리할 수 있습니다. 이를 수행하는 방법의 예는 stream-row-cache에 있습니다.

이 게시물이 Streams가 실제로 작동하는 방식과 응용 프로그램에서 Streams의 이러한 거의 알려지지 않은 속성을 활용하는 방법에 대한 추가 컨텍스트를 제공하기를 바랍니다.