Redis chuỗi vs Redis băm để thể hiện JSON: hiệu quả?


287

Tôi muốn lưu trữ một tải trọng JSON vào redis. Thực sự có 2 cách tôi có thể làm điều này:

  1. Một sử dụng một khóa chuỗi và giá trị đơn giản.
    key: user, value: payload (toàn bộ blob JSON có thể là 100-200 KB)

    SET user:1 payload

  2. Sử dụng băm

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

Hãy nhớ rằng nếu tôi sử dụng hàm băm, độ dài giá trị không thể dự đoán được. Chúng không phải là tất cả ngắn như ví dụ sinh học ở trên.

Bộ nhớ nào hiệu quả hơn? Sử dụng khóa và giá trị chuỗi, hoặc sử dụng hàm băm?


37
Ngoài ra, hãy nhớ rằng bạn không thể (dễ dàng) lưu trữ một đối tượng JSON lồng nhau trong một bộ băm.
Jonatan Hedborg

3
ReJSON cũng có thể giúp đỡ ở đây: redislabs.com/blog/redis-as-a-json-store
Cihan B.

2
ai đó đã sử dụng ReJSON ở đây?
Swamy

Câu trả lời:


168

Nó phụ thuộc vào cách bạn truy cập dữ liệu:

Đi đến Tùy chọn 1:

  • Nếu bạn sử dụng hầu hết các trường trên hầu hết các truy cập của bạn.
  • Nếu có sự khác biệt về các phím có thể

Đi đến lựa chọn 2:

  • Nếu bạn chỉ sử dụng các trường duy nhất trên hầu hết các truy cập của bạn.
  • Nếu bạn luôn biết những lĩnh vực có sẵn

PS: Theo nguyên tắc thông thường, hãy chọn tùy chọn yêu cầu ít truy vấn hơn trong hầu hết các trường hợp sử dụng của bạn.


28
Lựa chọn 1 không phải là một ý tưởng tốt nếu đồng thời sửa đổi của JSONtải trọng dự kiến (một vấn đề cổ điển của phi nguyên tử read-modify-write ).
Samveen

1
Cái nào hiệu quả hơn trong số các tùy chọn có sẵn của việc lưu trữ json blob dưới dạng chuỗi json hoặc dưới dạng mảng byte trong Redis?
Vinit89

422

Bài viết này có thể cung cấp rất nhiều cái nhìn sâu sắc ở đây: http://redis.io/topics/memory-optimization

Có nhiều cách để lưu trữ một mảng các Đối tượng trong Redis ( spoiler : Tôi thích tùy chọn 1 cho hầu hết các trường hợp sử dụng):

  1. Lưu trữ toàn bộ đối tượng dưới dạng chuỗi được mã hóa JSON trong một khóa duy nhất và theo dõi tất cả các Đối tượng bằng cách sử dụng một bộ (hoặc danh sách, nếu phù hợp hơn). Ví dụ:

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

    Nói chung, đây có lẽ là phương pháp tốt nhất trong hầu hết các trường hợp. Nếu có nhiều trường trong Đối tượng, các Đối tượng của bạn không được lồng với các Đối tượng khác và bạn có xu hướng chỉ truy cập vào một tập hợp con nhỏ của các trường tại một thời điểm, có thể tốt hơn là đi với tùy chọn 2.

    Ưu điểm : được coi là một "thực hành tốt." Mỗi đối tượng là một phím Redis đầy đủ. Phân tích cú pháp JSON rất nhanh, đặc biệt là khi bạn cần truy cập nhiều trường cho Đối tượng này cùng một lúc. Nhược điểm : chậm hơn khi bạn chỉ cần truy cập vào một trường duy nhất.

  2. Lưu trữ từng thuộc tính của Object trong hàm băm Redis.

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

    Ưu điểm : được coi là một "thực hành tốt." Mỗi đối tượng là một phím Redis đầy đủ. Không cần phân tích các chuỗi JSON. Nhược điểm : có thể chậm hơn khi bạn cần truy cập tất cả / hầu hết các trường trong Đối tượng. Ngoài ra, các đối tượng lồng nhau (Đối tượng trong đối tượng) không thể được lưu trữ dễ dàng.

  3. Lưu trữ mỗi Đối tượng dưới dạng chuỗi JSON trong hàm băm Redis.

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

    Điều này cho phép bạn hợp nhất một chút và chỉ sử dụng hai phím thay vì nhiều phím. Nhược điểm rõ ràng là bạn không thể đặt TTL (và các nội dung khác) trên mỗi Đối tượng người dùng, vì đó chỉ là một trường trong hàm băm Redis chứ không phải là khóa Redis toàn diện.

    Ưu điểm : Phân tích cú pháp JSON nhanh, đặc biệt là khi bạn cần truy cập nhiều trường cho Đối tượng này cùng một lúc. Ít "gây ô nhiễm" không gian tên khóa chính. Nhược điểm : Về việc sử dụng bộ nhớ tương tự như số 1 khi bạn có nhiều Đối tượng. Chậm hơn # 2 khi bạn chỉ cần truy cập vào một trường duy nhất. Có lẽ không được coi là một "thực hành tốt."

  4. Lưu trữ từng thuộc tính của từng Đối tượng trong một khóa chuyên dụng.

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

    Theo bài viết trên, tùy chọn này hầu như không bao giờ được ưa thích (trừ khi thuộc tính của Đối tượng cần phải có TTL cụ thể hoặc một cái gì đó).

    Ưu điểm : Thuộc tính đối tượng là các phím Redis đầy đủ, có thể không quá mức cho ứng dụng của bạn. Nhược điểm : chậm, sử dụng nhiều bộ nhớ hơn và không được coi là "thực hành tốt nhất". Rất nhiều ô nhiễm của không gian tên chính.

Tóm tắt chung

Tùy chọn 4 thường không được ưa thích. Tùy chọn 1 và 2 rất giống nhau và cả hai đều khá phổ biến. Tôi thích tùy chọn 1 (nói chung) vì nó cho phép bạn lưu trữ các Đối tượng phức tạp hơn (với nhiều lớp lồng nhau, v.v.) Tùy chọn 3 được sử dụng khi bạn thực sự quan tâm đến việc không làm ô nhiễm không gian tên khóa chính (tức là bạn không muốn ở đó có rất nhiều khóa trong cơ sở dữ liệu của bạn và bạn không quan tâm đến những thứ như TTL, khóa phím hoặc bất cứ thứ gì).

Nếu tôi có điều gì đó sai ở đây, vui lòng xem xét để lại nhận xét và cho phép tôi xem lại câu trả lời trước khi bỏ qua. Cảm ơn! :)


4
Đối với Tùy chọn # 2, bạn nói "có thể chậm hơn khi bạn cần truy cập tất cả / hầu hết các trường trong Đối tượng". Điều này đã được thử nghiệm?
mikegreiling

4
hmget là O (n) cho n trường nhận với tùy chọn 1 vẫn là O (1). Về mặt lý thuyết, vâng, nó nhanh hơn.
Aruna Herath 6/2/2015

4
Làm thế nào về việc kết hợp các tùy chọn 1 và 2 với một hàm băm? Sử dụng tùy chọn 1 cho dữ liệu được cập nhật không thường xuyên và tùy chọn 2 cho dữ liệu được cập nhật thường xuyên? Giả sử, chúng tôi đang lưu trữ các bài viết và chúng tôi lưu trữ các trường như tiêu đề, tác giả và url trong chuỗi JSON có khóa chung như objvà lưu trữ các trường như lượt xem, phiếu bầu và cử tri bằng các khóa riêng biệt? Bằng cách này với một truy vấn READ duy nhất bạn có được toàn bộ đối tượng và vẫn có thể cập nhật các phần động của đối tượng một cách nhanh chóng? Các bản cập nhật tương đối không thường xuyên cho các trường trong chuỗi JSON có thể được thực hiện bằng cách đọc và ghi lại toàn bộ đối tượng trong một giao dịch.
arun

2
Theo đó: ( instagram-engineering.tumblr.com/post/12202313862/ế ) nên lưu trữ trong nhiều giá trị băm về mức tiêu thụ bộ nhớ. Vì vậy, sau khi tối ưu hóa arun, chúng ta có thể thực hiện: 1- tạo nhiều giá trị băm lưu trữ tải trọng json dưới dạng chuỗi cho dữ liệu được cập nhật không thường xuyên và 2- tạo nhiều giá trị băm lưu trữ các trường json cho dữ liệu được cập nhật thường xuyên
Aboelnour

2
Trong trường hợp tùy chọn1, tại sao chúng ta thêm nó vào một tập hợp? Tại sao chúng ta không thể đơn giản sử dụng lệnh Get và kiểm tra nếu trở về không.
Thực dụng

8

Một số bổ sung cho một bộ câu trả lời nhất định:

Trước hết, nếu bạn sử dụng Redis hash một cách hiệu quả, bạn phải biết một khóa đếm số lượng tối đa và giá trị kích thước tối đa - nếu không, nếu chúng phá vỡ hàm băm-max-ziplist-value hoặc hash-max-ziplist-Redis sẽ chuyển đổi nó thành thực tế cặp khóa / giá trị thông thường dưới mui xe. (xem băm-max-ziplist-value, hash-max-ziplist-entry) Và phá vỡ một mui xe từ một tùy chọn băm là THỰC SỰ BAD, bởi vì mỗi cặp khóa / giá trị thông thường trong Redis sử dụng +90 byte mỗi cặp.

Điều đó có nghĩa là nếu bạn bắt đầu với tùy chọn hai và vô tình thoát ra khỏi giá trị max-hash-ziplist-value, bạn sẽ nhận được +90 byte mỗi MACHI ATTRIBUTE mà bạn có trong mô hình người dùng! (thực tế không phải là +90 mà là +70 xem đầu ra giao diện điều khiển bên dưới)

 # 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    
           ....
    }

Đối với câu trả lời của TheHippo, các nhận xét về Tùy chọn một là sai lệch:

hgetall / hmset / hmget để giải cứu nếu bạn cần tất cả các trường hoặc nhiều thao tác get / set.

Đối với câu trả lời BMiner.

Tùy chọn thứ ba thực sự rất thú vị, đối với tập dữ liệu có max (id) <has-max-ziplist-value giải pháp này có độ phức tạp O (N), bởi vì, thật bất ngờ, Reddis lưu trữ các giá trị băm nhỏ dưới dạng bộ chứa độ dài / khóa / giá trị giống như mảng các đối tượng!

Nhưng nhiều lần băm chỉ chứa một vài trường. Thay vào đó, khi băm nhỏ, chúng ta có thể chỉ mã hóa chúng trong cấu trúc dữ liệu O (N), giống như một mảng tuyến tính với các cặp giá trị khóa có tiền tố dài. Vì chúng ta chỉ làm điều này khi N nhỏ, thời gian khấu hao cho các lệnh HGET và HSET vẫn là O (1): hàm băm sẽ được chuyển đổi thành bảng băm thực sự ngay khi số phần tử mà nó chứa sẽ tăng quá nhiều

Nhưng bạn không nên lo lắng, bạn sẽ phá vỡ các mục nhập hash-max-ziplist rất nhanh và bạn sẽ thực sự ở giải pháp số 1.

Tùy chọn thứ hai rất có thể sẽ đi đến giải pháp thứ tư trong một mui xe vì như câu hỏi nêu:

Hãy nhớ rằng nếu tôi sử dụng hàm băm, độ dài giá trị không thể dự đoán được. Chúng không phải là tất cả ngắn như ví dụ sinh học ở trên.

Và như bạn đã nói: giải pháp thứ tư là +70 byte đắt nhất cho mỗi thuộc tính chắc chắn.

Đề nghị của tôi làm thế nào để tối ưu hóa tập dữ liệu đó:

Bạn có hai lựa chọn:

  1. Nếu bạn không thể đảm bảo kích thước tối đa của một số thuộc tính người dùng so với giải pháp đầu tiên và nếu vấn đề bộ nhớ là quan trọng hơn nén json người dùng trước khi lưu trữ trong redis.

  2. Nếu bạn có thể buộc kích thước tối đa của tất cả các thuộc tính. Hơn bạn có thể đặt băm-max-ziplist-entry / value và sử dụng băm dưới dạng một hàm băm cho mỗi đại diện người dùng HOẶC làm tối ưu hóa bộ nhớ băm từ chủ đề này của hướng dẫn Redis: https://redis.io/topics/memory-optimization và lưu trữ người dùng như chuỗi json. Dù bằng cách nào bạn cũng có thể nén các thuộc tính người dùng dài.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.