JSON을 나타내는 Redis 문자열과 Redis 해시 : 효율성?


287

JSON 페이로드를 redis에 저장하고 싶습니다. 이 작업을 수행 할 수있는 방법은 두 가지가 있습니다.

  1. 하나는 간단한 문자열 키와 값을 사용합니다.
    key : user, value : payload (100-200KB 일 수있는 전체 JSON Blob)

    SET user:1 payload

  2. 해시 사용

    HSET user:1 username "someone"
    HSET user:1 location "NY"
    HSET user:1 bio "STRING WITH OVER 100 lines"

해시를 사용하면 값 길이를 예측할 수 없습니다. 위의 바이오 예제와 같이 모두 짧은 것은 아닙니다.

어느 쪽이 더 효율적인 메모리입니까? 문자열 키와 값을 사용하거나 해시를 사용합니까?


37
또한 중첩 JSON 객체를 해시 세트에 (쉽게) 저장할 수는 없습니다.
Jonatan Hedborg

3
ReJSON도 여기에서 도움을 줄 수 있습니다 : redislabs.com/blog/redis-as-a-json-store
Cihan B.

2
누군가 여기서 ReJSON을 사용 했습니까?
Swamy

답변:


168

데이터에 액세스하는 방법에 따라 다릅니다.

옵션 1로 가십시오.

  • 대부분의 액세스에서 대부분의 필드를 사용하는 경우.
  • 가능한 키에 차이가있는 경우

옵션 2로 가십시오.

  • 대부분의 액세스에서 단일 필드 만 사용하는 경우
  • 어떤 필드를 사용할 수 있는지 항상 알고 있다면

추신 : 일반적으로 대부분의 사용 사례에서 더 적은 쿼리가 필요한 옵션을 선택하십시오.


28
페이로드 의 동시 수정JSON 이 예상되는 경우 옵션 1은 좋은 생각이 아닙니다 ( 비 원자 적인 고전적인 문제 read-modify-write).
Samveen

1
Redis에서 json blob을 json 문자열 또는 바이트 배열로 저장하는 사용 가능한 옵션 중에서 더 효율적인 것은 무엇입니까?
Vinit89

422

이 기사는 여기에 많은 통찰력을 제공 할 수 있습니다 : http://redis.io/topics/memory-optimization

Redis에 객체 배열을 저장하는 방법에는 여러 가지가 있습니다 ( 스포일러 : 대부분의 경우 옵션 1을 좋아합니다).

  1. 전체 객체를 단일 키에 JSON 인코딩 문자열로 저장하고 세트 (또는 더 적절한 경우 목록)를 사용하여 모든 객체를 추적합니다. 예를 들면 다음과 같습니다.

    INCR id:users
    SET user:{id} '{"name":"Fred","age":25}'
    SADD users {id}

    일반적으로 이것은 대부분의 경우에 가장 좋은 방법 일 것입니다. 오브젝트에 많은 필드가 있고 오브젝트가 다른 오브젝트와 중첩되지 않고 한 번에 작은 필드 서브 세트에만 액세스하는 경향이있는 경우 옵션 2를 사용하는 것이 좋습니다.

    장점 : "좋은 습관"으로 간주됩니다. 각 오브젝트는 완전한 Redis 키입니다. JSON 구문 분석이 빠릅니다. 특히이 객체의 여러 필드에 한 번에 액세스해야 할 때 특히 그렇습니다. 단점 : 단일 필드에만 액세스해야 할 때 속도가 느려집니다.

  2. 각 객체의 속성을 Redis 해시에 저장하십시오.

    INCR id:users
    HMSET user:{id} name "Fred" age 25
    SADD users {id}

    장점 : "좋은 습관"으로 간주됩니다. 각 오브젝트는 완전한 Redis 키입니다. JSON 문자열을 구문 분석 할 필요가 없습니다. 단점 : 객체의 모든 / 대부분의 필드에 액세스해야 할 때 속도가 느려질 수 있습니다. 또한 중첩 된 객체 (객체 내의 객체)를 쉽게 저장할 수 없습니다.

  3. 각 오브젝트를 Redis 해시에 JSON 문자열로 저장하십시오.

    INCR id:users
    HMSET users {id} '{"name":"Fred","age":25}'

    이를 통해 비트를 통합하고 많은 키 대신 두 개의 키만 사용할 수 있습니다. 명백한 단점은 TTL (및 기타 항목)을 전체 사용자 Redis 키가 아닌 Redis 해시의 필드이기 때문에 각 사용자 Object에 TTL (및 기타 항목)을 설정할 수 없다는 것입니다.

    장점 : JSON 구문 분석이 빠릅니다. 특히이 객체의 여러 필드에 한 번에 액세스해야하는 경우에 특히 그렇습니다. 기본 키 네임 스페이스의 "오염"이 적습니다. 단점 : 많은 오브젝트가있을 때 # 1과 동일한 메모리 사용량. 단일 필드에만 액세스해야하는 경우 # 2보다 느립니다. 아마도 "좋은 습관"으로 간주되지 않을 것입니다.

  4. 각 객체의 각 속성을 전용 키에 저장하십시오.

    INCR id:users
    SET user:{id}:name "Fred"
    SET user:{id}:age 25
    SADD users {id}

    위의 기사에 따르면이 옵션은 거의 선호되지 않습니다 (Object의 속성에 특정 TTL 또는 다른 것이 필요하지 않은 한 ).

    장점 : 객체 속성은 완전히 새로 워진 Redis 키이므로 앱에 과도하지 않을 수 있습니다. 단점 : 느리고 메모리를 많이 사용하며 "모범 사례"로 간주되지 않습니다. 기본 키 네임 스페이스가 많이 오염되었습니다.

전체 요약

옵션 4는 일반적으로 바람직하지 않습니다. 옵션 1과 2는 매우 유사하며 둘 다 일반적입니다. 옵션 1 (일반적으로 말하면)을 선호합니다. 더 복잡한 객체 (여러 계층의 중첩 등)를 저장할 수 있기 때문에 옵션 3은 기본 키 네임 스페이스를 오염시키지 않도록 주의 할 때 사용됩니다 (예 : 원하지 않는 경우). 데이터베이스에 많은 키가 있어야하며 TTL, 키 샤딩 또는 기타 항목에 대해서는 신경 쓰지 마십시오.

여기에 잘못된 점이 있으면 의견을 남기고 다운 보트하기 전에 답변을 수정하도록하십시오. 감사! :)


4
옵션 # 2의 경우 "개체의 모든 / 대부분의 필드에 액세스해야 할 때 속도가 느려질 수 있습니다"라고 말합니다. 이것이 테스트 되었습니까?
mikegreiling

4
hmget을 위한 O (N)는 N 필드는 얻을 여전히 O (1) 될 것이다 옵션 1. 이론적으로는 더 빠릅니다.
Aruna Herath

4
옵션 1과 2를 해시와 결합하는 것은 어떻습니까? 자주 업데이트되지 않는 데이터에는 옵션 1을, 자주 업데이트되는 데이터에는 옵션 2를 사용 하시겠습니까? 예를 들어 기사를 저장하고 제목, 저자 및 URL과 같은 obj필드를 일반 키와 같은 JSON 문자열에 저장하고 뷰, 투표 및 유권자 같은 필드를 별도의 키로 저장합니까? 이 방법으로 단일 READ 쿼리를 사용하면 전체 객체를 가져와도 객체의 동적 부분을 빠르게 업데이트 할 수 있습니까? JSON 문자열의 필드에 대한 비교적 드물게 업데이트는 트랜잭션에서 전체 객체를 읽고 쓰는 방법으로 수행 할 수 있습니다.
arun

2
이것에 따르면 ( instagram-engineering.tumblr.com/post/12202313862/… ) 메모리 소비 측면에서 여러 해시로 저장하는 것이 좋습니다. 따라서 arun의 최적화 후에는 다음과 같은 작업을 수행 할 수 있습니다. 1- 자주 업데이트되지 않는 데이터에 대한 문자열로 json 페이로드를 저장하는 다중 해시를 만들고, 자주 업데이트되는 데이터에 대한 json 필드를 저장하는 다중 해시를 만듭니다
Aboelnour

2
option1의 경우 왜 세트에 추가합니까? 왜 단순히 Get 명령을 사용하여 nil이 아닌 리턴인지 확인할 수없는 이유는 무엇입니까?
Pragmatic

8

주어진 답변에 몇 가지 추가 사항 :

우선 Redis 해시를 효율적으로 사용하려면 키 수 최대 개수와 최대 크기를 알아야합니다. 그렇지 않으면 해시-최대-ziplist 값 또는 해시-최대-ziplist 항목을 분류하면 Redis는이를 실제적으로 변환합니다 후드 아래에서 일반적인 키 / 값 쌍. (해시 -max-ziplist-value, hash-max-ziplist-entries 참조) Redis 내부의 각 일반적인 키 / 값 쌍은 쌍당 +90 바이트를 사용하기 때문에 해시 옵션에서 후드를 깨는 것은 정말 나쁜 일입니다.

옵션 2로 시작하여 실수로 max-hash-ziplist-value를 벗어나면 사용자가 가지고있는 각 속성마다 +90 바이트를 얻게됩니다! (실제로 +90은 아니지만 +70은 아래 콘솔 출력 참조)

 # you need me-redis and awesome-print gems to run exact code
 redis = Redis.include(MeRedis).configure( hash_max_ziplist_value: 64, hash_max_ziplist_entries: 512 ).new 
  => #<Redis client v4.0.1 for redis://127.0.0.1:6379/0> 
 > redis.flushdb
  => "OK" 
 > ap redis.info(:memory)
    {
                "used_memory" => "529512",
          **"used_memory_human" => "517.10K"**,
            ....
    }
  => nil 
 # me_set( 't:i' ... ) same as hset( 't:i/512', i % 512 ... )    
 # txt is some english fictionary book around 56K length, 
 # so we just take some random 63-symbols string from it 
 > redis.pipelined{ 10000.times{ |i| redis.me_set( "t:#{i}", txt[rand(50000), 63] ) } }; :done
 => :done 
 > ap redis.info(:memory)
  {
               "used_memory" => "1251944",
         **"used_memory_human" => "1.19M"**, # ~ 72b per key/value
            .....
  }
  > redis.flushdb
  => "OK" 
  # setting **only one value** +1 byte per hash of 512 values equal to set them all +1 byte 
  > redis.pipelined{ 10000.times{ |i| redis.me_set( "t:#{i}", txt[rand(50000), i % 512 == 0 ? 65 : 63] ) } }; :done 
  > ap redis.info(:memory)
   {
               "used_memory" => "1876064",
         "used_memory_human" => "1.79M",   # ~ 134 bytes per pair  
          ....
   }
    redis.pipelined{ 10000.times{ |i| redis.set( "t:#{i}", txt[rand(50000), 65] ) } };
    ap redis.info(:memory)
    {
             "used_memory" => "2262312",
          "used_memory_human" => "2.16M", #~155 byte per pair i.e. +90 bytes    
           ....
    }

TheHippo 답변의 경우 옵션 1에 대한 의견이 잘못되었습니다.

모든 필드 또는 여러 개의 get / set 작업이 필요한 경우 hgetall / hmset / hmget을 복구합니다.

BMiner의 답변입니다.

세 번째 옵션은 실제로 정말 재미 있습니다 .max (id) <has-max-ziplist-value 값이있는 데이터 세트의 경우이 솔루션에는 O (N) 복잡성이 있습니다. 놀랍게도 Reddis는 작은 해시를 길이 / 키 / 값의 배열과 같은 컨테이너로 저장하기 때문입니다 사물!

그러나 많은 경우 해시는 몇 개의 필드 만 포함합니다. 해시가 작을 때 대신 길이가 미리 지정된 키 값 쌍을 가진 선형 배열처럼 O (N) 데이터 구조로 인코딩 할 수 있습니다. N이 작을 때만이 작업을 수행하므로 HGET 및 HSET 명령의 상각 시간은 여전히 ​​O (1)입니다. 해시가 포함 된 요소 수가 너무 많아지면 실제 해시 테이블로 변환됩니다.

그러나 걱정하지 않아도됩니다. 해시-최대-지퍼리스트 항목을 매우 빠르게 중단하면 실제로 솔루션 번호 1에 도달하게됩니다.

두 번째 옵션은 질문에 따르면 다음과 같은 이유로 네 번째 솔루션으로 갈 것입니다.

해시를 사용하면 값 길이를 예측할 수 없습니다. 위의 바이오 예제와 같이 모두 짧은 것은 아닙니다.

이미 말했듯이 네 번째 솔루션은 각 속성 당 가장 비싼 +70 바이트입니다.

이러한 데이터 세트를 최적화하는 방법에 대한 제안 :

두 가지 옵션이 있습니다.

  1. 첫 번째 솔루션보다 일부 사용자 속성의 최대 크기를 보장 할 수없고 메모리 문제가 중요한 경우 redis에 저장하기 전에 사용자 json을 압축하는 것보다 중요합니다.

  2. 모든 속성의 최대 크기를 강제 할 수있는 경우 당신은 레디 스 가이드의이 주제에서 해시 메모리 최적화 사용자 표현 당 또는 하나의 해시로 중 해시 최대-ziplist-항목 / 값 및 사용 해시를 설정할 수 있습니다보다 : https://redis.io/topics/memory-optimization 및 사용자를 json 문자열로 저장하십시오. 어느 쪽이든 긴 사용자 속성을 압축 할 수도 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.