String Redis vs Redis hash untuk mewakili JSON: efisiensi?

287

Saya ingin menyimpan muatan JSON ke redis. Sebenarnya ada 2 cara yang bisa saya lakukan ini:

  1. Satu menggunakan kunci kunci dan nilai.
    kunci: pengguna, nilai: payload (seluruh gumpalan JSON yang bisa 100-200 KB)

    SET user:1 payload

  2. Menggunakan hash

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

Perlu diingat bahwa jika saya menggunakan hash, panjang nilainya tidak dapat diprediksi. Mereka tidak semuanya pendek seperti contoh bio di atas.

Mana yang lebih hemat memori? Menggunakan kunci dan nilai string, atau menggunakan hash?

Henley Chiu
sumber
37
Juga perlu diingat bahwa Anda tidak dapat (dengan mudah) menyimpan objek JSON bersarang dalam hash set.
Jonatan Hedborg
3
ReJSON dapat membantu di sini juga: redislabs.com/blog/redis-as-a-json-store
Cihan B.
2
apakah ada yang menggunakan ReJSON di sini?
Swamy

Jawaban:

168

Itu tergantung pada bagaimana Anda mengakses data:

Pilih Opsi 1:

  • Jika Anda menggunakan sebagian besar bidang pada sebagian besar akses Anda.
  • Jika ada perbedaan pada kunci yang mungkin

Pilih Opsi 2:

  • Jika Anda hanya menggunakan satu bidang pada sebagian besar akses Anda.
  • Jika Anda selalu tahu bidang mana yang tersedia

PS: Sebagai aturan praktis, pilih opsi yang membutuhkan lebih sedikit pertanyaan pada sebagian besar kasus penggunaan Anda.

TheHippo
sumber
28
Opsi 1 adalah bukan ide yang baik jika modifikasi bersamaan dari JSONpayload diharapkan (masalah klasik non-atom read-modify-write ).
Samveen
1
Mana yang lebih efisien di antara opsi yang tersedia untuk menyimpan json blob sebagai string json atau sebagai array byte di Redis?
Vinit89
422

Artikel ini dapat memberikan banyak wawasan di sini: http://redis.io/topics/memory-optimization

Ada banyak cara untuk menyimpan array Objek di Redis ( spoiler : Saya suka opsi 1 untuk kebanyakan kasus penggunaan):

  1. Menyimpan seluruh objek sebagai string bersandi JSON dalam satu kunci dan melacak semua Objek menggunakan set (atau daftar, jika lebih tepat). Sebagai contoh:

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

    Secara umum, ini mungkin metode terbaik dalam banyak kasus. Jika ada banyak bidang dalam Objek, Objek Anda tidak bersarang dengan Objek lain, dan Anda cenderung hanya mengakses sebagian kecil bidang pada satu waktu, mungkin lebih baik untuk pergi dengan opsi 2.

    Keuntungan : dianggap sebagai "praktik yang baik." Setiap Objek adalah kunci Redis yang lengkap. Penguraian JSON cepat, terutama ketika Anda perlu mengakses banyak bidang untuk Obyek ini sekaligus. Kekurangan : lebih lambat saat Anda hanya perlu mengakses satu bidang.

  2. Simpan setiap properti Object dalam hash Redis.

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

    Keuntungan : dianggap sebagai "praktik yang baik." Setiap Objek adalah kunci Redis yang lengkap. Tidak perlu mengurai string JSON. Kekurangan : mungkin lebih lambat ketika Anda perlu mengakses semua / sebagian besar bidang dalam suatu Objek. Juga, Objek bersarang (Objek dalam Objek) tidak dapat dengan mudah disimpan.

  3. Simpan setiap Objek sebagai string JSON dalam hash Redis.

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

    Ini memungkinkan Anda untuk menggabungkan sedikit dan hanya menggunakan dua tombol, bukan banyak tombol. Kerugian yang jelas adalah bahwa Anda tidak dapat mengatur TTL (dan hal-hal lain) pada setiap objek pengguna, karena itu hanya bidang di hash Redis dan bukan kunci Redis yang penuh.

    Keuntungan : Penguraian JSON cepat, terutama ketika Anda perlu mengakses banyak bidang untuk Obyek ini sekaligus. Kurang "mencemari" ruang nama kunci utama. Kekurangan : Tentang penggunaan memori yang sama dengan # 1 saat Anda memiliki banyak Objek. Lebih lambat dari # 2 saat Anda hanya perlu mengakses satu bidang. Mungkin tidak dianggap sebagai "praktik yang baik."

  4. Simpan setiap properti dari setiap Objek dalam kunci khusus.

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

    Menurut artikel di atas, opsi ini hampir tidak pernah disukai (kecuali jika properti Object perlu memiliki TTL tertentu atau sesuatu).

    Keuntungan : Properti objek adalah kunci Redis yang penuh, yang mungkin tidak berlebihan bagi aplikasi Anda. Kekurangan : lambat, menggunakan lebih banyak memori, dan tidak dianggap "praktik terbaik." Banyak polusi dari namespace kunci utama.

Ringkasan Keseluruhan

Opsi 4 umumnya tidak disukai. Opsi 1 dan 2 sangat mirip, dan keduanya cukup umum. Saya lebih suka opsi 1 (secara umum) karena memungkinkan Anda untuk menyimpan Objek yang lebih rumit (dengan banyak lapisan sarang, dll.) Opsi 3 digunakan ketika Anda benar-benar peduli untuk tidak mencemari namespace kunci utama (yaitu Anda tidak ingin ada untuk menjadi banyak kunci dalam database Anda dan Anda tidak peduli tentang hal-hal seperti TTL, key sharding, atau apa pun).

Jika ada yang salah di sini, silakan tinggalkan komentar dan izinkan saya merevisi jawabannya sebelum membatalkan. Terima kasih! :)

BMiner
sumber
4
Untuk Opsi # 2 Anda mengatakan "mungkin lebih lambat ketika Anda perlu mengakses semua / sebagian besar bidang dalam Objek". Apakah ini sudah diuji?
mikegreiling
4
hmget adalah O (n) untuk n bidang dapatkan dengan opsi 1 masih akan menjadi O (1). Secara teoritis, ya, ini lebih cepat.
Aruna Herath
4
Bagaimana dengan menggabungkan opsi 1 dan 2 dengan hash? Gunakan opsi 1 untuk data yang jarang diperbarui dan opsi 2 untuk data yang sering diperbarui? Katakanlah, kami menyimpan artikel dan kami menyimpan bidang seperti judul, penulis, dan url dalam string JSON dengan kunci generik seperti objdan menyimpan bidang seperti tampilan, suara, dan pemilih dengan kunci terpisah? Dengan cara ini dengan satu permintaan BACA Anda mendapatkan seluruh objek dan masih dapat memperbarui bagian dinamis dari objek Anda dengan cepat? Pembaruan yang relatif jarang ke bidang dalam string JSON dapat dilakukan dengan membaca dan menulis seluruh objek kembali dalam transaksi.
arun
2
Menurut ini: ( instagram-engineering.tumblr.com/post/12202313862/… ) disarankan untuk menyimpan dalam banyak hash dalam hal konsumsi memori. Jadi setelah optimasi arun, kita dapat melakukan: 1- membuat banyak hash menyimpan json payload sebagai string untuk data yang jarang diperbarui, dan 2- membuat banyak hash menyimpan bidang json untuk data yang sering diperbarui
Aboelnour
2
Dalam hal option1, mengapa kita menambahkannya ke set? Mengapa kita tidak bisa menggunakan perintah Get dan periksa kembali jika tidak.
Pragmatis
8

Beberapa tambahan pada serangkaian jawaban yang diberikan:

Pertama-tama, jika Anda akan menggunakan hash Redis secara efisien, Anda harus tahu kunci menghitung jumlah maksimum dan nilai ukuran maksimum - jika tidak, jika hash-max-ziplist-value atau hash-max-ziplist-entries Redis akan mengubahnya menjadi praktis pasangan kunci / nilai biasa di bawah tenda. (lihat hash-max-ziplist-value, hash-max-ziplist-entri) Dan melanggar di bawah tenda dari opsi hash BENAR-BENAR BURUK, karena setiap pasangan kunci / nilai biasa di dalam Redis menggunakan +90 byte per pasang.

Ini berarti bahwa jika Anda mulai dengan opsi dua dan tanpa sengaja keluar dari nilai max-hash-ziplist-Anda akan mendapatkan +90 byte per SETIAP ATTRIBUTE yang Anda miliki di dalam model pengguna! (sebenarnya bukan +90 tetapi +70 lihat output konsol di bawah)

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

Untuk jawaban TheHippo, komentar pada Opsi satu menyesatkan:

hgetall / hmset / hmget untuk menyelamatkan jika Anda membutuhkan semua bidang atau operasi beberapa get / set.

Untuk jawaban BMiner.

Pilihan ketiga sebenarnya sangat menyenangkan, untuk dataset dengan max (id) <memiliki-max-ziplist-value solusi ini memiliki kompleksitas O (N), karena, yang mengejutkan, Reddis menyimpan hash kecil sebagai wadah array seperti panjang / kunci / nilai benda!

Tetapi berkali-kali hash hanya berisi beberapa bidang. Ketika hash berukuran kecil, kita bisa mengkodekannya dalam struktur data O (N), seperti array linier dengan pasangan nilai kunci yang diawali dengan panjang. Karena kita melakukan ini hanya ketika N kecil, waktu diamortisasi untuk perintah HGET dan HSET masih O (1): hash akan dikonversi menjadi tabel hash nyata segera setelah jumlah elemen yang dikandungnya akan tumbuh terlalu banyak

Tetapi Anda tidak perlu khawatir, Anda akan memecahkan entri hash-max-ziplist-sangat cepat dan di sana Anda sekarang Anda benar-benar di solusi nomor 1.

Opsi kedua kemungkinan besar akan menuju solusi keempat di bawah tudung karena sebagai pertanyaan menyatakan:

Perlu diingat bahwa jika saya menggunakan hash, panjang nilainya tidak dapat diprediksi. Mereka tidak semuanya pendek seperti contoh bio di atas.

Dan seperti yang sudah Anda katakan: solusi keempat adalah +70 byte paling mahal untuk setiap atribut.

Saran saya bagaimana mengoptimalkan dataset tersebut:

Anda punya dua opsi:

  1. Jika Anda tidak dapat menjamin ukuran maksimal beberapa atribut pengguna daripada Anda mencari solusi pertama dan jika masalah memori sangat penting daripada kompres pengguna json sebelum disimpan dalam redis.

  2. Jika Anda dapat memaksa ukuran maksimum semua atribut. Daripada Anda dapat mengatur hash-max-ziplist-entri / nilai dan menggunakan hash baik sebagai satu hash per representasi pengguna ATAU sebagai optimasi memori hash dari topik ini panduan Redis: https://redis.io/topics/memory-optimization dan menyimpan pengguna sebagai string json. Bagaimanapun Anda juga dapat mengompres atribut pengguna yang panjang.

Алексей Лещук
sumber