Invalidasi Cache Rails tingkat lapangan: Solusi DSL
Diterbitkan: 2022-03-11Dalam pengembangan web modern, caching adalah cara cepat dan ampuh untuk mempercepat segalanya. Jika dilakukan dengan benar, caching dapat membawa peningkatan signifikan pada kinerja aplikasi Anda secara keseluruhan. Ketika dilakukan salah, itu pasti akan berakhir dengan bencana.
Pembatalan cache, seperti yang mungkin Anda ketahui, adalah salah satu dari tiga masalah tersulit dalam ilmu komputer—dua lainnya adalah penamaan sesuatu dan kesalahan satu per satu. Salah satu jalan keluar yang mudah adalah membatalkan segalanya, kiri dan kanan, setiap kali ada perubahan. Tapi itu mengalahkan tujuan caching. Anda ingin membatalkan cache hanya jika benar-benar diperlukan.
Jika Anda ingin memaksimalkan caching, Anda harus sangat spesifik tentang apa yang Anda batalkan dan menyelamatkan aplikasi Anda dari pemborosan sumber daya berharga pada pekerjaan berulang.
Dalam posting blog ini, Anda akan mempelajari teknik untuk memiliki kontrol yang lebih baik atas perilaku cache Rails: khususnya, menerapkan pembatalan cache tingkat bidang. Teknik ini bergantung pada Rails ActiveRecord dan ActiveSupport::Concern
serta manipulasi perilaku metode touch
.
Posting blog ini didasarkan pada pengalaman saya baru-baru ini dalam sebuah proyek di mana kami melihat peningkatan kinerja yang signifikan setelah menerapkan pembatalan cache tingkat bidang. Ini membantu mengurangi pembatalan cache yang tidak perlu dan rendering template yang berulang.
Rel, Ruby, dan Performa
Ruby bukan bahasa tercepat, tetapi secara keseluruhan, ini adalah opsi yang cocok untuk kecepatan pengembangan. Selain itu, kemampuan metaprogramming dan domain-specific language (DSL) bawaannya memberi pengembang fleksibilitas yang luar biasa.
Ada penelitian di luar sana seperti penelitian Jakob Nielsen yang menunjukkan kepada kita bahwa jika suatu tugas memakan waktu lebih dari 10 detik, kita akan kehilangan fokus. Dan mendapatkan kembali fokus kita membutuhkan waktu. Jadi ini bisa menjadi mahal secara tak terduga.
Sayangnya, di Ruby on Rails, sangat mudah untuk melampaui ambang batas 10 detik itu dengan pembuatan template. Anda tidak akan melihat itu terjadi di aplikasi "hello world" atau proyek hewan peliharaan skala kecil, tetapi dalam proyek dunia nyata di mana banyak hal dimuat ke satu halaman, percayalah, pembuatan template dapat dengan mudah mulai menyeret.
Dan, itulah yang harus saya selesaikan dalam proyek saya.
Optimasi Sederhana
Tapi bagaimana tepatnya Anda mempercepat?
Jawabannya: Tolok ukur dan optimalkan.
Dalam proyek saya, dua langkah yang sangat efektif dalam pengoptimalan adalah:
- Menghilangkan N+1 kueri
- Memperkenalkan teknik caching yang baik untuk template
N+1 Pertanyaan
Memperbaiki kueri N+1 itu mudah. Yang dapat Anda lakukan adalah memeriksa file log Anda—setiap kali Anda melihat beberapa kueri SQL seperti di bawah ini di log Anda, hilangkan dengan menggantinya dengan pemuatan yang bersemangat:
Learning Load (0.4ms) SELECT 'learnings'.* FROM 'learnings' WHERE 'project'.'id' = ? Learning Load (0.3ms) SELECT 'learnings'.* FROM 'learnings' WHERE 'project'.'id' = ? Learning Load (0.4ms) SELECT 'learnings'.* FROM 'learnings' WHERE 'project'.'id' = ?
Ada permata untuk ini yang disebut peluru untuk membantu mendeteksi inefisiensi ini. Anda juga dapat menelusuri setiap kasus penggunaan dan, sementara itu, memeriksa log dengan memeriksanya berdasarkan pola di atas. Dengan menghilangkan semua inefisiensi N+1, Anda dapat cukup yakin bahwa Anda tidak akan membebani database Anda dan waktu yang Anda habiskan untuk ActiveRecord akan turun secara signifikan.
Setelah melakukan perubahan ini, proyek saya sudah berjalan lebih cepat. Tapi saya memutuskan untuk membawanya ke tingkat berikutnya dan melihat apakah saya bisa menurunkan waktu muat itu lebih jauh. Masih ada sedikit rendering yang tidak perlu yang terjadi di template, dan pada akhirnya, di situlah caching fragmen membantu.
Cache Fragmen
Caching fragmen umumnya membantu mengurangi waktu pembuatan template secara signifikan. Tetapi perilaku cache Rails default tidak memotongnya untuk proyek saya.
Ide di balik caching fragmen Rails sangat brilian. Ini menyediakan mekanisme caching yang sangat sederhana dan efektif.
Penulis Ruby On Rails telah menulis artikel yang sangat bagus di Signal v. Noise tentang cara kerja caching fragmen.
Katakanlah Anda memiliki sedikit antarmuka pengguna yang menunjukkan beberapa bidang entitas.
- Saat memuat halaman, Rails menghitung
cache_key
berdasarkan kelas entitas dan bidangupdated_at
. - Menggunakan
cache_key
itu, ia memeriksa untuk melihat apakah ada sesuatu di cache yang terkait dengan kunci itu. - Jika tidak ada apa pun di cache, maka kode HTML untuk fragmen tersebut akan dirender untuk tampilan (dan konten yang baru dirender disimpan dalam cache).
- Jika ada konten yang ada di cache dengan kunci itu, maka tampilan akan dirender dengan konten cache.
Ini menyiratkan bahwa cache tidak perlu dibatalkan secara eksplisit. Setiap kali kami mengubah entitas dan memuat ulang halaman, konten cache baru dirender untuk entitas.
Rails, secara default, juga menawarkan kemampuan untuk membatalkan cache entitas induk jika anak berubah:
belongs_to :parent_entity, touch: true
Ini, ketika dimasukkan dalam model, akan secara otomatis menyentuh orang tua ketika anak disentuh . Anda dapat mempelajari lebih lanjut tentang touch
di sini. Dengan ini, Rails memberi kita cara sederhana dan efisien untuk membatalkan cache entitas induk secara bersamaan dengan cache entitas turunan.
Caching di Rails
Namun, caching di Rails dibuat untuk melayani antarmuka pengguna di mana fragmen HTML yang mewakili entitas induk berisi fragmen HTML yang hanya mewakili entitas anak dari induknya. Dengan kata lain, fragmen HTML yang mewakili entitas anak dalam paradigma ini tidak dapat berisi bidang dari entitas induk.
Tapi bukan itu yang terjadi di dunia nyata. Anda mungkin perlu melakukan hal-hal di aplikasi Rails Anda yang melanggar kondisi ini.
Bagaimana Anda menangani situasi di mana antarmuka pengguna menunjukkan bidang entitas induk di dalam fragmen HTML yang mewakili entitas anak?
Jika anak berisi bidang dari entitas induk, maka Anda bermasalah dengan perilaku pembatalan cache default Rails.

Setiap kali bidang yang disajikan dari entitas induk diubah, Anda harus menyentuh semua entitas anak milik induk tersebut. Misalnya, jika Parent1
dimodifikasi, Anda perlu memastikan bahwa cache untuk tampilan Child1
dan Child2
keduanya tidak valid.
Jelas, ini dapat menyebabkan kemacetan kinerja yang sangat besar. Menyentuh setiap entitas anak setiap kali induk telah berubah akan menghasilkan banyak kueri basis data tanpa alasan yang jelas.
Skenario serupa lainnya adalah ketika entitas yang terkait dengan asosiasi has_and_belongs_to
disajikan dalam daftar, dan memodifikasi entitas tersebut memulai kaskade pembatalan cache melalui rantai asosiasi.
class Event < ActiveRecord::Base has_many :participants has_many :users, through: :participants end class Participant < ActiveRecord::Base belongs_to :event belongs_to :user end class User < ActiveRecord::Base has_many :participants has_many :events, through :participants end
Jadi, untuk antarmuka pengguna di atas, tidak logis untuk menyentuh peserta atau acara ketika lokasi pengguna berubah. Tapi kita harus menyentuh acara dan peserta saat nama pengguna berubah, bukan?
Jadi teknik dalam artikel Signal v. Noise tidak efisien untuk instance UI/UX tertentu, seperti yang dijelaskan di atas.
Meskipun Rails sangat efektif untuk hal-hal sederhana, proyek nyata memiliki komplikasinya sendiri.
Invalidasi Cache Rel Level Bidang
Dalam proyek saya, saya telah menggunakan Ruby DSL kecil untuk menangani situasi seperti di atas. Ini memungkinkan Anda untuk menentukan secara deklaratif bidang yang akan memicu pembatalan cache melalui asosiasi.
Mari kita lihat beberapa contoh di mana itu benar-benar membantu:
Contoh 1:
class Event < ActiveRecord::Base include Touchable ... has_many :tasks ... touch :tasks, in_case_of_modified_fields: [:name] ... end class Task < ActiveRecord::Base belongs_to :event end
Cuplikan ini memanfaatkan kemampuan metaprogramming dan kemampuan DSL dalam Ruby.
Untuk lebih spesifik, hanya perubahan nama dalam acara yang akan membatalkan cache fragmen dari tugas terkaitnya. Mengubah bidang acara lainnya—seperti tujuan atau lokasi—tidak akan membatalkan cache fragmen tugas. Saya akan menyebut ini sebagai kontrol pembatalan tembolok berbutir halus tingkat bidang .
Contoh 2:
Mari kita lihat contoh yang menunjukkan pembatalan cache melalui rantai asosiasi has_many
.
Fragmen antarmuka pengguna yang ditunjukkan di bawah ini menunjukkan tugas dan pemiliknya:
Untuk antarmuka pengguna ini, fragmen HTML yang mewakili tugas harus dibatalkan hanya ketika tugas berubah atau ketika nama pemilik berubah. Jika semua bidang lain dari pemilik (seperti zona waktu atau preferensi) berubah, maka cache tugas harus dibiarkan utuh.
Ini dicapai dengan menggunakan DSL yang ditunjukkan di sini:
class User < ActiveRecord::Base include Touchable touch :tasks, in_case_of_modified_fields: [:first_name, :last_name] ... end class Task < ActiveRecord::Base has_one owner, class_name: :User end
Implementasi DSL
Esensi utama DSL adalah metode touch
. Argumen pertamanya adalah asosiasi, dan argumen berikutnya adalah daftar bidang yang memicu touch
pada asosiasi itu:
touch :tasks, in_case_of_modified_fields: [:first_name, :last_name]
Metode ini disediakan oleh modul Touchable
:
module Touchable extend ActiveSupport::Concern included do before_save :check_touchable_entities after_save :touch_marked_entities end module ClassMethods def touch association, options @touchable_associations ||= {} @touchable_associations[association] = options end end end
Dalam kode ini, poin utamanya adalah kita menyimpan argumen dari panggilan touch
. Kemudian, sebelum menyimpan entitas, kami menandai asosiasi tersebut sebagai kotor jika bidang yang ditentukan telah diubah. Kami menyentuh entitas dalam asosiasi itu setelah menyimpan jika asosiasi itu kotor.
Kemudian, bagian pribadi yang menjadi perhatian adalah:
... private def klass_level_meta_info self.class.instance_variable_get('@touchable_associations') end def meta_info @meta_info ||= {} end def check_touchable_entities return unless klass_level_meta_info.present? klass_level_meta_info.each_pair do |association, change_triggering_fields| if any_of_the_declared_field_changed?(change_triggering_fields) meta_info[association] = true end end end def any_of_the_declared_field_changed?(options) (options[:in_case_of_modified_fields] & changes.keys.map{|x|x.to_sym}).present? end …
Dalam metode check_touchable_entities
, kami memeriksa apakah bidang yang dideklarasikan berubah . Jika demikian, kami menandai asosiasi sebagai kotor dengan menyetel meta_info[association]
menjadi true
.
Kemudian, setelah menyimpan entitas, kami memeriksa asosiasi kotor kami dan menyentuh entitas di dalamnya jika perlu:
… def touch_marked_entities return unless klass_level_meta_info.present? klass_level_meta_info.each_key do |association_key| if meta_info[association_key] association = send(association_key) association.update_all(updated_at: Time.zone.now) meta_info[association_key] = false end end end …
Dan, itu dia! Sekarang Anda dapat melakukan pembatalan cache tingkat bidang di Rails dengan DSL sederhana.
Kesimpulan
Rails caching menjanjikan peningkatan kinerja dalam aplikasi Anda dengan relatif mudah. Namun, aplikasi dunia nyata bisa rumit dan sering menimbulkan tantangan unik. Perilaku cache Rails default berfungsi dengan baik untuk sebagian besar skenario, tetapi ada skenario tertentu di mana sedikit lebih banyak pengoptimalan dalam pembatalan cache dapat sangat membantu.
Sekarang setelah Anda mengetahui cara menerapkan pembatalan cache tingkat bidang di Rails, Anda dapat mencegah pembatalan cache yang tidak perlu di aplikasi Anda.