Redis Praktik Terbaik, Bagian 2

Bagian kedua dari siklus terjemahan Redis Best Practices dari Redis Labs, dan membahas pola interaksi dan pola penyimpanan data.

Bagian pertama ada di sini .

Pola interaksi


Redis dapat berfungsi tidak hanya sebagai DBMS tradisional, tetapi juga struktur dan perintahnya dapat digunakan untuk bertukar pesan antara layanan microser atau proses. Penggunaan klien Redis yang tersebar luas, kecepatan dan efisiensi server dan protokol, serta struktur klasik bawaan memungkinkan Anda untuk membuat alur kerja Anda sendiri dan mekanisme acara. Dalam bab ini, kita akan membahas topik-topik berikut:

  • antrian acara;
  • memblokir dengan Redlock;
  • Pub / Sub;
  • acara terdistribusi.

Antrian acara


Daftar di Redis adalah daftar baris berurutan, sangat mirip dengan daftar tertaut yang mungkin Anda kenal. Menambahkan nilai ke daftar (push) dan menghapus nilai dari daftar (pop) adalah operasi yang sangat ringan. Seperti yang dapat Anda bayangkan, ini adalah struktur yang sangat baik untuk mengelola antrian: tambahkan elemen ke awal dan bacalah dari awal (FIFO). Redis juga menyediakan fitur tambahan yang membuat pola ini lebih efisien, andal, dan mudah digunakan.

Daftar memiliki subset perintah yang memungkinkan Anda menjalankan perilaku "pemblokiran". Istilah "pemblokiran" mengacu pada koneksi dengan hanya satu klien. Bahkan, perintah ini tidak memungkinkan klien untuk melakukan apa pun hingga nilai muncul di daftar atau hingga batas waktu berakhir. Ini menghilangkan kebutuhan untuk polling Redis, menunggu hasilnya. Karena klien tidak dapat melakukan apa pun saat mengharapkan nilai, kami akan membutuhkan dua klien terbuka untuk menggambarkan hal ini:
#Pelanggan 1Klien 2
1
> BRPOP my-q 0
[nilai harapan]
2
> LPUSH my-q hello
(integer) 1
1) "my-q"
2) "hello"
[klien tidak dikunci, siap menerima perintah]
3
> BRPOP my-q 0
[nilai harapan]

Dalam contoh ini, pada langkah 1, kita melihat bahwa klien yang diblokir tidak segera mengembalikan apa pun, karena tidak mengandung apa pun. Argumen terakhir adalah waktu tunggu. Di sini 0 berarti harapan abadi. Pada baris kedua , nilai dimasukkan dalam my-q , dan klien pertama segera keluar dari status pemblokiran. Pada baris ketiga, BRPOP dipanggil lagi (Anda dapat melakukan ini dalam satu loop dalam aplikasi), dan klien juga menunggu nilai berikutnya. Dengan menekan "Ctrl + C" Anda dapat memecahkan kunci dan keluar dari klien.

Mari kita membalikkan contoh dan melihat bagaimana BRPOP bekerja dengan daftar yang tidak kosong:
#Pelanggan 1Klien 2
1
> LPUSH my-q hello
(integer) 1
2
> LPUSH my-q hej
(integer) 2
3
> LPUSH my-q bonjour
(integer) 3
4
> BRPOP my-q 0
1) "my-q"
2) "hello"
5
> BRPOP my-q 0
1) "my-q"
2) "hej"
6
> BRPOP my-q 0
1) "my-q"
2) "bonjour"
7
> BRPOP my-q 0
[nilai harapan]

Pada langkah 1-3, kami menambahkan 3 nilai ke daftar dan melihat bahwa jawabannya tumbuh, menunjukkan jumlah elemen dalam daftar. Langkah 4, meskipun memanggil BRPOP, segera mengembalikan nilainya. Ini karena perilaku pemblokiran hanya terjadi ketika tidak ada nilai dalam antrian. Kita dapat melihat respons instan yang sama di langkah 5-6 karena ini dilakukan untuk setiap item dalam antrian. Pada langkah 7, BRPOP tidak menemukan apa pun dalam antrian dan memblokir klien sampai ada sesuatu yang ditambahkan.

Seringkali antrian mewakili beberapa pekerjaan yang perlu dilakukan dalam proses lain (pekerja). Dalam jenis beban kerja ini, penting agar pekerjaan tidak hilang jika pekerja jatuh karena suatu alasan selama eksekusi. Redis mendukung jenis antrian ini. Untuk melakukan ini, gunakan perintah BRPOPLPUSH alih-alih BRPOP. Dia mengharapkan nilai dalam satu daftar, dan segera setelah muncul di sana, memasukkannya ke daftar lain. Ini dilakukan secara atom, sehingga tidak mungkin bagi dua pekerja untuk mengubah nilai yang sama. Mari kita lihat cara kerjanya:
#Pelanggan 1Klien 2
1
> LINDEX worker-q 0
(nil)
2[Jika hasilnya tidak nol, bagaimanapun prosesnya dan lanjutkan ke langkah 4]
3
> LREM worker-q -1 [   1]
(integer) 1
[kembali ke langkah 1]
4
> BRPOPLPUSH my-q worker-q 0
[nilai harapan]
5
> LPUSH my-q hello
"hello"
[klien tidak dikunci, siap menerima perintah]
6[menangani halo]
7
> LREM worker-q -1 hello
(integer) 1
8[kembali ke langkah 1]

Pada langkah 1-2, kita tidak melakukan apa-apa, karena pekerja-q kosong. Jika sesuatu telah kembali, maka kami memprosesnya dan menghapusnya, dan kembali lagi ke langkah 1 untuk memeriksa apakah ada sesuatu yang masuk ke antrian. Jadi, pertama-tama kita menghapus antrian pekerja dan melakukan pekerjaan yang ada. Pada langkah 4, kita menunggu sampai nilainya muncul di my-q , dan ketika itu terjadi, secara atomik ditransfer ke pekerja-q . Lalu kami entah bagaimana memproses "halo" , setelah itu kami menghapusnya dari pekerja-q dan kembali ke langkah 1. Jika proses mati di langkah 6, nilainya masih tetap di pekerja-q . Setelah memulai kembali proses, kami akan segera menghapus semua yang tidak terhapus di langkah 7.

Pola ini sangat mengurangi kemungkinan kehilangan pekerjaan, tetapi hanya jika pekerja meninggal antara langkah 2 dan 3 atau 5 dan 6, yang tidak mungkin, tetapi praktik terbaik akan mempertimbangkan hal ini dalam logika pekerja.

Kunci dengan redlock


Terkadang dalam sistem itu perlu untuk memblokir beberapa sumber daya. Ini mungkin diperlukan untuk menerapkan perubahan penting yang tidak dapat diselesaikan dalam lingkungan yang kompetitif. Tujuan Pemblokiran:

  • memungkinkan satu dan hanya satu pekerja untuk menangkap sumber daya;
  • dapat melepaskan objek kunci secara andal;
  • Jangan mengunci sumber daya dengan erat (harus dibuka kunci setelah periode waktu tertentu).

Redis adalah pilihan yang baik untuk mengimplementasikan pemblokiran, karena memiliki model data berbasis kunci yang sederhana, dan setiap beling berulir tunggal dan cukup cepat. Ada implementasi kunci yang sangat baik menggunakan Redis yang disebut Redlock.
Klien Redlock tersedia untuk hampir setiap bahasa, namun, penting untuk mengetahui cara kerja Redlock agar dapat menggunakannya dengan aman dan efektif.

Pertama, Anda perlu memahami bahwa Redlock dirancang untuk berjalan di setidaknya 3 mesin dengan instance Redis yang independen. Ini menghilangkan satu titik kegagalan dalam mekanisme penguncian Anda, yang dapat menyebabkan kebuntuan semua sumber daya. Hal lain yang perlu dipahami adalah bahwa meskipun jam pada mesin tidak harus disinkronkan 100%, mereka harus berfungsi dengan cara yang sama - waktu bergerak dengan kecepatan yang sama: satu detik pada mesin Dan sama seperti satu detik pada mesin B.

Mengatur objek kunci dengan Redlock dimulai dengan memperoleh cap waktu dengan presisi milidetik. Anda juga harus menunjukkan terlebih dahulu waktu pemblokiran. Kemudian objek pemblokiran diatur dengan mengatur (SET) kunci dengan nilai acak (hanya jika kunci ini belum ada) dan mengatur batas waktu untuk kunci. Ini diulangi untuk setiap instance independen. Jika instance jatuh, maka itu segera dilewati. Jika objek kunci berhasil diinstal pada sebagian besar kasus sebelum batas waktu berakhir, maka itu dianggap ditangkap. Waktu untuk menginstal atau memperbarui objek kunci adalah jumlah waktu yang diperlukan untuk mencapai status kunci, dikurangi waktu kunci yang telah ditentukan. Jika terjadi kesalahan atau batas waktu, buka kunci semua contoh dan coba lagi.

Untuk membebaskan objek kunci, lebih baik menggunakan skrip Lua yang akan memeriksa apakah nilai acak yang diharapkan ada dalam set kunci. Jika ada, maka Anda dapat menghapusnya, jika tidak lebih baik meninggalkan kunci, karena ini mungkin objek kunci yang lebih baru.

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

Proses Redlock memberikan jaminan yang baik dan tidak adanya satu titik kegagalan, sehingga Anda dapat sepenuhnya yakin bahwa objek kunci tunggal akan didistribusikan dan bahwa tidak ada kunci timbal balik yang akan terjadi.

Pub / Sub


Selain penyimpanan data, Redis juga dapat digunakan sebagai platform Pub / Sub (penerbit / pelanggan). Dalam pola ini, penerbit dapat mengeluarkan pesan ke sejumlah pelanggan saluran. Ini adalah pesan berdasarkan prinsip โ€œtembak dan lupakanโ€, yaitu, jika pesan tersebut dilepaskan dan pelanggan tidak ada, maka pesan tersebut hilang tanpa kemungkinan pemulihan.
Dengan berlangganan ke saluran, klien memasuki mode pelanggan dan tidak bisa lagi memanggil perintah - itu menjadi hanya dibaca. Penerbit tidak memiliki batasan seperti itu.

Anda dapat berlangganan lebih dari satu saluran. Kami mulai dengan berlangganan dua saluran cuaca dan olahraga menggunakan perintah SUBSCRIBE:

> SUBSCRIBE weather sports
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "weather"
3) (integer) 1
1) "subscribe"
2) "sports"
3) (integer) 2

Di klien terpisah (jendela terminal lain, misalnya) kami dapat mempublikasikan pesan di salah satu saluran ini menggunakan perintah PUBLISH:

> PUBLISH sports oilers/7:leafs/1
(integer) 1

Argumen pertama adalah nama saluran, yang kedua adalah pesan. Pesan dapat berupa apa saja, dalam hal ini adalah akun berkode dalam game. Perintah mengembalikan jumlah klien kepada siapa pesan akan dikirimkan. Di klien pelanggan, kami segera melihat pesan:

1) "message"
2) "sports"
3) "oilers/7:leafs/1"

Responsnya berisi tiga elemen: indikasi bahwa ini adalah pesan, saluran berlangganan, dan, pada kenyataannya, sebuah pesan. Klien segera setelah menerima kembali ke mendengarkan saluran.

Kembali ke penerbit, kami dapat memposting pesan lain:

> PUBLISH weather snow/-4c
(integer) 1

Dalam pelanggan kita akan melihat format yang sama, tetapi dengan saluran yang berbeda dengan pesan:

1) "message"
2) "weather"
3) "snow/-4c"

Mari memposting pesan ke saluran di mana tidak ada pelanggan:

> PUBLISH currency CADUSD/0.787
(integer) 0

Karena tidak ada yang mendengarkan saluran mata uang , jawabannya adalah 0. Pesan ini hilang, dan klien yang setelah mereka berlangganan saluran ini tidak akan menerima pemberitahuan tentang pesan ini - pesan telah dikirim dan dilupakan.

Selain berlangganan ke saluran tunggal, Redis memungkinkan berlangganan saluran dengan mask. Topeng gaya glob dilewatkan ke perintah PSUBSCRIBE:

> PSUBSCRIBE sports:*

Klien akan menerima pesan dari semua saluran, dimulai dengan olahraga: . Di klien lain, panggil perintah berikut:

> PUBLISH sports:hockey oilers/7:leafs/1
(integer) 1
> PUBLISH sports:basketball raptors/33:pacers/7
(integer) 1
> PUBLISH weather:edmonton snow/-4c
(integer) 0

Harap dicatat bahwa dua tim pertama mengembalikan 1, sedangkan yang terakhir mengembalikan 0. Dan meskipun kami tidak secara langsung berlangganan olahraga: hoki atau olahraga: bola basket , klien menerima pesan melalui berlangganan dengan topeng. Di jendela pelanggan-klien, kita dapat melihat bahwa ada hasil hanya untuk saluran yang cocok dengan mask.

1) "pmessage"
2) "sports:*"
3) "sports:hockey"
4) "oilers/7:leafs/1"
1) "pmessage"
2) "sports:*"
3) "sports:basketball"
4) "raptors/33:pacers/7"

Output ini sedikit berbeda dari output perintah SUBSCRIBE karena berisi mask itu sendiri, serta nama asli saluran.

Acara Terdistribusi


Skema perpesanan Pub / Sub Redis dapat diperluas untuk membuat acara yang didistribusikan dengan menarik. Katakanlah kita memiliki struktur yang disimpan dalam tabel hash, tetapi kami ingin memperbarui klien hanya ketika satu bidang melebihi nilai numerik yang ditetapkan oleh pelanggan. Kami akan mendengarkan saluran dengan mask dan mengekstrak hash dalam status . Dalam contoh ini, kami tertarik pada update_status dengan nilai 5-9.

> PSUBSCRIBE update_status:[5-9]
1) "psubscribe"
2) "update_status:[5-9]"
3) (integer) 1
...

Untuk mengubah nilai status / error_level , kita memerlukan dua perintah yang dapat dieksekusi secara berurutan atau di blok MULTI / EXEC. Perintah pertama mengatur level, dan yang kedua menerbitkan pemberitahuan dengan nilai yang dikodekan dalam saluran itu sendiri.

> HSET status error_level 5
(integer) 1
> PUBLISH update_status:5 0
(integer) 1

Di jendela pertama, kita melihat bahwa pesan telah diterima, dan setelah itu Anda dapat beralih ke klien lain dan memanggil perintah HGETALL:

...
1) "pmessage"
2) "update_status:[5-9]"
3) "update_status:5"
4) "0"

> HGETALL status
1) "error_level"
2) "5"

Kita juga dapat menggunakan metode ini untuk memperbarui variabel lokal dari beberapa proses yang panjang. Ini dapat memungkinkan banyak contoh dari proses yang sama untuk bertukar data secara real time.

Mengapa pola ini lebih baik daripada menggunakan Pub / Sub? Ketika proses restart, itu hanya bisa mendapatkan seluruh negara dan mulai mendengarkan. Perubahan akan disinkronkan antara sejumlah proses.

Pola penyimpanan data


Ada beberapa pola untuk menyimpan data terstruktur di Redis. Dalam bab ini kita akan mempertimbangkan hal berikut:

  • penyimpanan data di JSON;
  • fasilitas penyimpanan.

Penyimpanan data JSON


Ada beberapa opsi untuk menyimpan data JSON di Redis. Bentuk yang paling umum adalah mengelompokkan objek terlebih dahulu dan menyimpannya di bawah kunci khusus:

> SET car "{\"colour\":\"blue\",\"make\":\"saab\",\"model\":93,\"features\":[\"powerlocks\",\"moonroof\"]}"
OK
> GET car
"{\"colour\":\"blue\",\"make\":\"saab\",\"model\":93,\"features\":[\"powerlocks\",\"moonroof\"]}"

Tampaknya terlihat sederhana, tetapi memiliki beberapa kelemahan yang sangat serius:

  • serialisasi membutuhkan sumber daya komputasi klien untuk membaca dan menulis;
  • Format JSON meningkatkan ukuran data;
  • Redis hanya memiliki cara tidak langsung dalam menangani data di JSON.

Beberapa poin pertama dapat diabaikan pada sejumlah kecil data, tetapi biayanya akan meningkat seiring pertumbuhan data. Namun, poin ketiga adalah yang paling kritis.

Sebelum Redis 4.0, satu-satunya cara untuk bekerja dengan JSON di dalam Redis adalah dengan menggunakan skrip Lua dalam modul cjson. Ini sebagian menyelesaikan masalah, meskipun masih tetap menjadi hambatan dan menciptakan kerumitan tambahan dengan belajar Lua. Selain itu, banyak aplikasi hanya menerima seluruh string JSON, deserialized, bekerja dengan data, serial kembali, dan menyimpannya lagi. Ini adalah antipattern. Ada risiko besar kehilangan data dengan cara ini.

#Mesin Virtual Aplikasi # 1Mesin Virtual Aplikasi # 2
1
> GET my-car
2[deserialize, ubah warna mesin dan serialkan lagi]
> GET my-car
3
> SET my-car

[nilai baru dari instance # 1]
[deserialize, ubah model mesin dan serialkan lagi]
4
> SET my-car

[nilai baru dari instance # 2]
5
> GET my-car

Hasil pada baris 5 akan menunjukkan perubahan hanya untuk instance 2, dan perubahan warna dengan instance 1 akan hilang.

Redis versi 4.0 dan lebih tinggi memiliki kemampuan untuk menggunakan modul. ReJSON adalah modul yang menyediakan tipe data khusus dan perintah untuk interaksi langsung dengannya. ReJSON menyimpan data dalam format biner, yang mengurangi ukuran data yang disimpan, menyediakan akses yang lebih cepat ke elemen-elemen tanpa menghabiskan waktu untuk de / serialisasi.

Untuk menggunakan ReJSON, Anda harus menginstalnya di server Redis atau mengaktifkannya di Redis Enterprise.

Contoh sebelumnya menggunakan ReJSON akan terlihat seperti ini:

#Mesin Virtual Aplikasi # 1Mesin Virtual Aplikasi # 2
1
> JSON.SET car2 . '{"colour": "blue",  "make":"saab", "model":93,  "features": ["powerlocks",  "moonroof"]}โ€˜
OK
2
> JSON.SET car2 colour '"red"'
OK
3
> JSON.SET car2 model '95'
OK
> JSON.GET car2 .
"{\"colour\":\"red",\"make\":\"saab\",\"model\":95,\"features\":[\"powerlocks\",\"moonroof\"]}"

ReJSON menyediakan cara yang lebih aman, lebih cepat dan lebih intuitif untuk bekerja dengan data JSON di Redis, terutama dalam kasus-kasus di mana perubahan atom menjadi elemen bersarang diperlukan.

Penyimpanan Objek


Pada pandangan pertama, tipe data "hash table" Redis standar mungkin tampak sangat mirip dengan objek JSON atau tipe lainnya. Jauh lebih mudah untuk membuat bidang baik string atau angka dan mencegah struktur bersarang. Namun, setelah menghitung "jalan" ke setiap bidang, Anda bisa "meratakan" objek dan menyimpannya di tabel hash Redis.

{
    "colour": "blue",
    "make": "saab",
    "model": {
        "trim": "aero",
        "name": 93
    },
    "features": ["powerlocks", "moonroof"]
}

Menggunakan JSONPath (XPath untuk JSON), kita bisa mewakili setiap elemen pada level yang sama dari tabel hash:

> HSET car3 colour blue
> HSET car3 make saab
> HSET car3 model.trim aero
> HSET car3 model.name 93
> HSET car3 features[0] powerlocks
> HSET car3 features[1] moonroof

Untuk kejelasan, perintah terdaftar secara terpisah, tetapi banyak parameter dapat diteruskan ke HSET.

Sekarang Anda dapat meminta seluruh objek atau bidang individualnya:

> HGETALL car3
 1) "colour"
 2) "blue"
 3) "make"
 4) "saab"
 5) "model.trim"
 6) "aero"
 7) "model.name"
 8) "93"
 9) "features[0]"
10) "powerlocks"
11) "features[1]"
12) "moonroof"

> HGET car3 model.trim
"aero"

Meskipun ini menyediakan cara cepat dan berguna untuk mengambil objek yang disimpan di Redis, ada kekurangannya:

  • dalam bahasa dan pustaka yang berbeda, implementasi JSONPath mungkin berbeda, menyebabkan ketidakcocokan. Dalam hal ini, perlu membuat serialisasi dan deserialisasi data dengan satu alat;
  • dukungan array:
    • array jarang bisa bermasalah;
    • tidak mungkin untuk melakukan banyak operasi, seperti memasukkan elemen di tengah array.

  • Konsumsi sumber daya yang tidak perlu di kunci JSONPath.

Pola ini hampir sama dengan ReJSON. Jika ReJSON tersedia, maka dalam banyak kasus lebih baik menggunakannya. Namun, menyimpan objek dengan cara di atas memiliki satu keunggulan dibandingkan ReJSON: integrasi dengan tim Redis SORT. Namun, perintah ini kompleks secara komputasional dan merupakan topik kompleks yang terpisah di luar cakupan pola ini.

Bagian penutup selanjutnya akan mencakup pola deret waktu, pola batas kecepatan, pola saringan Bloom, penghitung, dan penggunaan Lua di Redis.

PS Saya mencoba mengadaptasi teks artikel-artikel ini dalam bahasa Inggris "barbar" sebanyak mungkin ke dalam bahasa Rusia, tetapi jika Anda berpikir bahwa di suatu tempat idenya tidak dapat dipahami atau salah, perbaiki saya di komentar.

Source: https://habr.com/ru/post/undefined/


All Articles