الفرق بين المراجعتين لصفحة: «Rails/caching with rails»

من موسوعة حسوب
مراجعة وتدقيق
طلا ملخص تعديل
 
(1 مراجعات متوسطة بواسطة نفس المستخدم غير معروضة)
سطر 348: سطر 348:


== مفاتيح ذاكرة التخزين المؤقت ==
== مفاتيح ذاكرة التخزين المؤقت ==
يمكن أن تكون المفاتيح المستخدمة في ذاكرة التخزين المؤقتة أي كائن يستجيب إلى <code>cache_key</code> أو <code>to_param</code>. يمكنك تنفيذ التابع <code>cache_key</code> في الأصناف الخاصه بك إذا كنت تحتاج إلى إنشاء مفاتيح مخصصة. سيعمل [[Rails/active record basics|السجل الفعال]] على إنشاء مفاتيح استنادًا إلى اسم الصنف ومعرف السجل.
يمكن أن تكون المفاتيح المستخدمة في ذاكرة التخزين المؤقتة أي كائن يستجيب إلى <code>cache_key</code> أو <code>to_param</code>. يمكنك تنفيذ التابع <code>cache_key</code> في الأصناف الخاصه بك إذا كنت تحتاج إلى إنشاء مفاتيح مخصصة. سيعمل [[Rails/active record|Active Record]] على إنشاء مفاتيح استنادًا إلى اسم الصنف ومعرف السجل.


يمكنك استخدام جدول Hash ومصفوفات من القيم كمفاتيح ذاكرة التخزين المؤقت.
يمكنك استخدام جدول Hash ومصفوفات من القيم كمفاتيح ذاكرة التخزين المؤقت.
سطر 435: سطر 435:
ريلز ينشئ ترويسات ETag ضعيفة بشكل افتراضي. تتيح ترويسات ETag الضعيف استجابات ذات دلالة مكافئة، إذ يكون لعدة الاستجابات نفس ترويسات ETag، رغم كون محتواها غير متطابق تمامًا. يفيد ذلك عندما لا نريد إعادة توليد الصفحة لتغييرات طفيفة في محتوى الاستجابة.
ريلز ينشئ ترويسات ETag ضعيفة بشكل افتراضي. تتيح ترويسات ETag الضعيف استجابات ذات دلالة مكافئة، إذ يكون لعدة الاستجابات نفس ترويسات ETag، رغم كون محتواها غير متطابق تمامًا. يفيد ذلك عندما لا نريد إعادة توليد الصفحة لتغييرات طفيفة في محتوى الاستجابة.


تملك الترويسات ETag الضعيفة البادئة W/‎ لتمييزها عن الترويسات ETags القوية.
تملك الترويسات ETag الضعيفة البادئة W/‎ لتمييزها عن الترويسات ETag القوية.
<syntaxhighlight lang="text">
<syntaxhighlight lang="text">
W/"618bbc92e2d35ea1945008b42799b0e7" → Weak ETag
W/"618bbc92e2d35ea1945008b42799b0e7" → Weak ETag

المراجعة الحالية بتاريخ 08:19، 25 مارس 2019

هذا الدليل عبارة عن مدخل يطلعك على كيفية تسريع تطبيق ريلز عبر التخزين المؤقت.

يعني التخزين المؤقت تخزين المحتوى الذي أُنشِئ أثناء دورة الاستجابة للطلب وإعادة استخدامه عند الاستجابة لطلبات مشابهة.

غالبًا ما يكون التخزين المؤقت الطريقة الأكثر فاعلية لتعزيز أداء التطبيق. من خلال التخزين المؤقت، يمكن لمواقع الويب التي تعمل على خادم واحد مع قاعدة بيانات واحدة الحفاظ على تحميل الآلاف من المستخدمين المتزامنين.

يوفر ريلز مجموعة مميزة من ميزات التخزين المؤقت. سوف يعلمك هذا الدليل نطاق وغرض كل واحدة منها. إتقان هذه التقنيات وتطبيقها على تطبيقات ريلز الخاصة بك سيُمكِّن تطبيقك من تخديم ملايين المستخدمين دون أوقات استجابة كبيرة أو فواتير باهظة للخادم.

بعد قراءة هذا الدليل، ستتعلم:

  • أسلوب تخزين الأجزاء (fragment caching) وأسلوب تخزين الدمية الروسية (Russian doll caching)
  • كيفية إدارة اعتماديات التخزين المؤقت.
  • مخازن ذاكرة التخزين المؤقت البديلة.
  • دعم GET الشرطي.

التخزين المؤقت الأساسي

هذه مقدمة إلى ثلاثة أنواع من تقنيات التخزين المؤقت: تخزين الصفحة (page)، وتخزين الإجراء (action)، وتخزين الأجزاء (fragment).

بشكل افتراضي يوفر ريلز أسلوب تخزين الأجزاء. لاستخدام أسلوب التخزين المؤقت للصفحة وللإجراء، ستحتاج إلى إضافة actionpack-page_caching و actionpack-action_caching إلى الملف Gemfile الخاص بك.

بشكل افتراضي، يُفعَّل التخزين المؤقت فقط في بيئة الإنتاج الخاصة بك. للتلاعب بالتخزين المؤقت محليًا، ستحتاج إلى تمكين التخزين المؤقت في البيئة المحلية عن طريق تعيين الضبط config.action_controller.perform_caching إلى القيمة true في الملف config/environments/*.rb:

config.action_controller.perform_caching = true

ملاحظة: سيؤثر تغيير قيمة config.action_controller.perform_caching فقط على التخزين المؤقت الذي يوفره مكون "عنصر وحدة التحكم في الإجراء". على سبيل المثال، لن يؤثر ذلك على التخزين المؤقت ذي المستوى المنخفض، الذي سنتناوله أدناه.

التخزين المؤقت للصفحة

التخزين المؤقت للصفحة هو آلية توفرها ريلز تسمح لطلب صفحة وُلِّدَت مسبقًا بواسطة خادم الويب (أي Apache أو NGINX) دون الحاجة إلى المرور عبر مكدس ريلز بأكمله. في حين أن هذا لا يمكن تطبيقه بسرعة فائقة على كل حالة (مثل الصفحات التي تحتاج إلى المصادقة). أيضًا، لأن خادم الويب يخدم ملفًا مباشرةً من نظام الملفات، إذ ستحتاج إلى تنفيذ انتهاء صلاحية ذاكرة التخزين المؤقت (cache expiration).

تنبيه: تمت إزالة التخزين المؤقت للصفحة من الإصدار 4 من ريلز. راجع الجوهرة actionpack-page_caching.

التخزين المؤقت للإجراء

لا يمكن استخدام التخزين المؤقت للصفحة من أجل تخزين الإجراءات التي تحتاج إلى ترشيح مسبق مثل الصفحات التي تتطلب استيثاق. هذا هو المكان الذي يأتي فيه التخزين المؤقت للإجراء. يعمل التخزين المؤقت للإجراء مثل التخزين المؤقت للصفحة باستثناء أن طلب الويب الوارد ينتقل إلى مكدس ريلز، وبذلك يمكن تشغيل المرشحات المسبقة (before filters) معها قبل أن تُخدَّم ذاكرة التخزين المؤقتة. يسمح ذلك بتشغيل الاستيثاق والقيود الأخرى مع الاستمرار في عرض نتيجة المخرجات من نسخة التخزين المؤقت المخبأة.

تنبيه: تمت إزالة التخزين المؤقت للإجراء من الإصدار 4 من ريلز. راجع الجوهرة actionpack-action_caching. انظر نظرة عامة على انتهاء صلاحية ذاكرة التخزين المؤقت المستندة إلى مفتاح DHH للاطلاع على الطريقة المفضلة للتخزين حاليًا.

التخزين المؤقت للأجزاء

عادة ما تقوم تطبيقات الويب الديناميكية بإنشاء صفحات تحتوي على مجموعة متنوعة من المكونات التي لا تحتوي جميعها على نفس خصائص التخزين المؤقت. عندما تحتاج إلى تخزين أجزاء مختلفة من الصفحة مؤقتًا بشكل تنتهي صلاحيتها بشكل منفصل، يمكنك استخدام أسلوب تخزين الأجزاء مؤقتًا (Fragment Caching).

يسمح التخزين المؤقت للأجزاء بتجزئة وحدة العرض منطقيًّا لتغليفها في كتلة ذاكرة التخزين المؤقت وتُخدَّم خارج مخزن ذاكرة التخزين المؤقت عندما يأتي الطلب التالي.

على سبيل المثال، إذا كنت تريد تخزين كل منتج معروض في صفحة مؤقتًا، يمكنك استخدام هذه الشيفرة:

<% @products.each do |product| %>
  <% cache product do %>
    <%= render product %>
  <% end %>
<% end %>

عندما يتلقى تطبيقك طلبه الأول إلى هذه الصفحة، سيكتب ريلز مدخلة جديدة الى ذاكرة التخزين المؤقت ذات مفتاح فريد. المفتاح يبدو شيئًا كهذا:

views/products/1-201505056193031061005000/bea67108094918eeba42cd4a6e786901

الرقم في المنتصف هو معرف المنتج product_id متبوعًا بقيمة الطابع الزمني في الخاصية updated_at لسجل المنتج. تستخدم ريلز قيمة الطابع الزمني للتأكد من أنها لا تخدم البيانات القديمة. إذا تغيرت قيمة updated_at، ستولد قيمة مفتاح جديد. ثم ستكتب ريلز ذاكرة تخزين مؤقت جديدة إلى هذا المفتاح، ولن تُستخدم ذاكرة التخزين المؤقتة القديمة المكتوبة على المفتاح القديم مرة أخرى. وهذا ما يسمى انتهاء الصلاحية المستندة إلى المفتاح (key-based expiration).

ستنتهي صلاحية أجزاء ذاكرة التخزين المؤقت أيضًا عند تغيير الملخص الرقمي للواجهة (على سبيل المثال عند تغير شيفرة HTML في الواجهة). سلسلة الأحرف في نهاية المفتاح هي ملخص شجرة القالب. وهو عبارة عن ملخص رقمي مشفر محسوب استنادًا إلى محتويات جزء الواجهة الذي تُخزنه مؤقتًا. إذا قمت بتغيير جزء الواجهة، سيتغير الملخص، وتنتهي صلاحية الملف الموجود.

تنبيه: ستحذف مخازن ذاكرة التخزين المؤقت مثل Memcached ملفات التخزين المؤقت القديمة تلقائيًا.

إذا كنت تريد تخزين جزء مؤقتًا تحت شروط معينة، فيمكنك استخدام cache_if أو cache_unless:

<% cache_if admin?, product do %>
  <%= render product %>
<% end %>

التخزين المؤقت للمجموعة

يمكن للمساعد render أيضًا تخزين قوالب فردية مصيَّرة لمجموعة ما. في أحد الأمثلة السابقة التي تطبق each، يمكن تصيير جميع قوالب التخزين المؤقت دفعة واحدة بدلًا من تصييرها واحدة تلو الأخرى. يجرى ذلك بتمرير cached: true عند تصيير المجموعة:

<%= render partial: 'products/product', collection: @products, cached: true %>

ستُجلَب جميع نماذج التخزين المؤقت من العروض السابقة دفعة واحدة بسرعة أكبر بكثير. بالإضافة إلى ذلك، ستُكتب القوالب التي لم تُخزن مؤقتًا حتى الآن إلى ذاكرة التخزين المؤقت وسيُجلبها التطبيق بشكل متعدد في التصيير التالي.

أسلوب الدمية الروسية للتخزين المؤقت

قد ترغب في دمج أجزاء مخزنة مؤقتًا داخل أجزاء أخرى مخزنة مؤقتًا. وهذا ما يسمى أسلوب الدمية الروسية للتخزين المؤقت (Russian doll caching).

ميزة التخزين المؤقت بأسلوب الدمية الروسية (Russian doll) هي أنه في حالة تحديث منتج واحد، يمكن إعادة استخدام جميع الأجزاء الداخلية الأخرى عند إعادة توليد الجزء الخارجي.

كما هو موضح في القسم السابق، ستنتهي صلاحية الملف المخزن مؤقتًا إذا تغيرت القيمة updated_at للسجل الذي يعتمد عليه الملف المخزن مؤقتًا بشكل مباشر. ومع ذلك، لن تنتهي صلاحية أي ذاكرة تخزين مؤقتة تكون الجزئية (fragment) متشعبة فيها.

على سبيل المثال، خذ الواجهة التالية:

<% cache product do %>
  <%= render product.games %>
<% end %>

وهو ما يؤدي بدوره إلى عرض هذه الواجهة:

<% cache game do %>
  <%= render game %>
<% end %>

إذا تغيرت أية خاصية في game، فستُعيَّن القيمة updated_at إلى الوقت الحالي، وبالتالي تنتهي صلاحية ذاكرة التخزين المؤقت. ومع ذلك، نظرًا لأنه لن تتغيير updated_at لكائن المنتج، فلن تنتهي صلاحية ذاكرة التخزين المؤقت هذه وسيقدم تطبيقك بيانات قديمة. لإصلاح ذلك، نقوم بربط النماذج مع التابع touch:

class Product < ApplicationRecord
  has_many :games
end
 
class Game < ApplicationRecord
  belongs_to :product, touch: true
end

مع ضبط touch إلى القيمة true، فإن أي إجراء يغير updated_at لسجل game سيغيره أيضًا للمنتج product المقترن، وبذلك تنتهي صلاحية ذاكرة التخزين المؤقت.

التخزين المؤقت لأجزاء مشتركة

من الممكن مشاركة الجزئيات والتخزين المؤقت المرتبط بين ملفات ذات أنواع mime مختلفة. على سبيل المثال، يسمح التخزين المؤقت لجزئية مشتركة لكتاب النماذج بمشاركة جزء من ملفات HTML و JavaScript. عند تجميع القوالب في مسارات ملفات مستبين القالب، فإنها لا تتضمن سوى لاحقة لغة القالب وليس النوع mime. وذلك بسبب إمكانية استخدام هذه القوالب لأنواع متعددة من mime. ستستجيب كل من طلبات HTML و JavaScript إلى الشيفرة التالية:

render(partial: 'hotels/hotel', collection: @hotels, cached: true)

سيُحمل ملفًا اسمه hotels/hotel.erb.

خيار آخر هو تضمين اسم الملف الكامل للجزئية المراد تصييرها:

render(partial: 'hotels/hotel.html.erb', collection: @hotels, cached: true)

سيُحمل ملفًا باسم hotels/hotel.html.erb في أي نوع من أنواع ملفات mime، على سبيل المثال، يمكنك تضمين هذه الجزئية في ملف JavaScript.

إدارة الاعتماديات

من أجل إبطال ذاكرة التخزين المؤقت بشكل صحيح، تحتاج إلى تعريف اعتماديات التخزين المؤقت (caching dependencies) بشكل صحيح. ريلز ذكية بما فيه الكفاية للتعامل مع الحالات الشائعة بحيث لا تضطر إلى تحديد أي شيء. ومع ذلك، في بعض الأحيان، عندما تتعامل مع مساعدين متخصصين على سبيل المثال، تحتاج إلى تعريفهم بشكل صريح.

الاعتماديات الضمنية

يمكن اشتقاق معظم اعتماديات القالب من الإستدعاءات render في القالب نفسه. فيما يلي بعض الأمثلة على الإستدعاءات التي تجعل ActionView::Digestor تعرف كيفية فك ترميز:

render partial: "comments/comment", collection: commentable.comments
render "comments/comments"
render 'comments/comments'
render('comments/comments')
 
render "header" translates to render("comments/header")
 
render(@topic)         translates to render("topics/topic")
render(topics)         translates to render("topics/topic")
render(message.topics) translates to render("topics/topic")

من ناحية أخرى، يجب تغيير بعض الإستدعاءات لجعل التخزين المؤقت يعمل بشكل صحيح. على سبيل المثال، إذا كنت تُمرر مجموعة مخصصة، فستحتاج إلى تغيير:

render @project.documents.where(published: true)

إلى

render partial: "documents/document", collection: @project.documents.where(published: true)

اعتماديات صريحة

في بعض الأحيان سيكون لديك اعتماديات قالب لا يمكن اشتقاقه على الإطلاق. هذا هو الحال عادة عندما يحدث التصيير في المساعدين. إليك مثال على ذلك:

<%= render_sortable_todolists @project.todolists %>

ستحتاج إلى استخدام تنسيق تعليق خاص لاستدعاءها:

<%# Template Dependency: todolists/todolist %>
<%= render_sortable_todolists @project.todolists %>

في بعض الحالات، مثل إعداد توريث جدول فردي، قد يكون لديك مجموعة من الاعتماديات الصريحة. بدلًا من كتابة كل قالب، يمكنك استخدام محرف بديل (wildcard) لمطابقة أي قالب في المجلد:

<%# Template Dependency: events/* %>
<%= render_categorizable_events @person.events %>

بالنسبة إلى التخزين المؤقت للمجموعة، إذا لم يبدأ قالب الجزئية باستدعاء ذاكرة تخزين مؤقتة نظيفة (clean cache)، فلا يزال بإمكانك الاستفادة من التخزين المؤقت للمجموعة عن طريق إضافة تنسيق خاص للتعليق في أي مكان في النموذج، مثل:

<%# Template Collection: notification %>
<% my_helper_that_calls_cache(some_arg, notification) do %>
  <%= notification.name %>
<% end %>

الاعتماديات الخارجية

إذا كنت تستخدم تابعًا مساعدًا، على سبيل المثال، داخل كتلة مخزنة مؤقتًا ثم قمت بتحديث هذا المساعد، فسيتعين عليك تحذيث ذاكرة التخزين المؤقت أيضًا. لا يهم حقًا كيف تفعل ذلك، ولكن يجب تغيير MD5 من ملف القالب. إحدى التوصيات هي أن تنفذ ذلك بشكل صريح في تعليق، مثل:

<%# Helper Dependency Updated: Jul 28, 2015 at 7pm %>
<%= some_helper_method(person) %>

التخزين المؤقت منخفض المستوى

تحتاج في بعض الأحيان إلى تخزين قيمة استعلام أو نتيجة معينة بدلاً من تخزين أجزاء من ذاكرة التخزين المؤقت. تعمل آلية التخزين المؤقت في ريلز بشكل رائع لتخزين أي نوع من المعلومات.

يستخدم التابع Rails.cache.fetch الطريقة الأكثر فعالية لتطبيق التخزين المؤقت ذي المستوى المنخفض. هذا التابع لا يقرأ ولا يكتب على حد سواء إلى ذاكرة التخزين المؤقت. عند تمرير وسيط واحدة فقط، يجلب المفتاح ويعيد القيمة من ذاكرة التخزين المؤقتة. إذا مُرِّرت كتلة، فسيُنفذ هذه الكتلة في حالة فقدان ذاكرة التخزين المؤقتة. تكتب القيمة المعادة الخاصة بالكتلة إلى ذاكرة التخزين المؤقتة تحت مفتاح التخزين المؤقت المحدد، وستُعاد تلك القيمة المعادة أيضًا. في حالة الوصول إلى ذاكرة التخزين المؤقتة، تعاد القيمة المخزنة مؤقتًا دون تنفيذ الكتلة.

خذ بعين الاعتبار المثال التالي. يحتوي التطبيق على النموذج Product مع نسخة تابع تبحث عن سعر المنتج على موقع ويب منافس. ستكون البيانات التي تعاد بواسطة هذا التابع مثالية للتخزين المؤقت على مستوى منخفض:

class Product < ApplicationRecord
  def competing_price
    Rails.cache.fetch("#{cache_key}/competing_price", expires_in: 12.hours) do
      Competitor::API.find_price(id)
    end
  end
end

ملاحظة: لاحظ أننا في هذا المثال استخدمنا التابع cache_key_with_version، لذا فإن مفتاح ذاكرة التخزين المؤقت الناتج سيكون شيئًا مثل المنتجات products/233-20140225082222765838000/competing_price. يولد cache_key_with_version سلسلة نصية استنادًا إلى معرف النموذج والخاصية updated_at. هذا هو عُرفٌ شائع وله فائدة إبطال ذاكرة التخزين المؤقت كلما تحدَّث المنتج. بشكل عام، عند استخدام التخزين المؤقت ذي المستوى المنخفض لمعلومات على مستوى النسخة، تحتاج إلى توليد مفتاح ذاكرة تخزين مؤقت.

تخزين تعليمات SQL مؤقتًا

التخزين المؤقت لاستعلام هو إحدى ميزات ريلز لتخزين مجموعة النتائج التي أُرجعت بواسطة كل استعلام. إذا واجهت ريلز نفس الاستعلام مرة أخرى لهذا الطلب، ستستخدم مجموعة النتائج المخزنة مؤقتًا مسبقًا بدلًا من تشغيل الاستعلام على قاعدة البيانات مرة أخرى.

فمثلًا:

class ProductsController < ApplicationController
 
  def index
    # نفذ تعليمة
    @products = Product.all
 
    ...
 
    # أعد تنفيذ نفس التعليمة مجددًا
    @products = Product.all
  end
 
end

في المرة الثانية التي يُنفَّذ فيها الاستعلام نفسه على قاعدة البيانات، لن يوصل إلى قاعدة البيانات فعليًا. خُزِّنَت النتيجة في المرة الأولى التي أعيدت فيها من الاستعلام في ذاكرة التخزين المؤقتة للاستعلام (في الذاكرة) وسحبت في المرة الثانية من الذاكرة عند طلب تنفيذها.

ومع ذلك، من المهم ملاحظة أن مخازن الاستعلام المؤقتة تُنشَأ في بداية الإجراء وتدمَّر في نهاية هذا الإجراء وبالتالي تستمر فقط لمدة الإجراء. إذا كنت ترغب في تخزين نتائج طلبات البحث بطريقة أكثر ثباتًا، فيمكنك استخدام التخزين المؤقت ذي المستوى المنخفض.

مخازن ذاكرة التخزين المؤقتة

توفر ريلز مخازن مختلفة للبيانات المخزنة مؤقتًا (بخلاف التخزين المؤقت لاستعلامات SQL والصفحة).

إعدادات الضبط

يمكنك إعداد مخزن ذاكرة تخزين مؤقتة افتراضي للتطبيق من خلال تعيين ضبط الإعداد config.cache_store. يمكن تمرير معاملات أخرى كوسائط إلى مُنشئ مخزن التخزين المؤقت:

config.cache_store = :memory_store, { size: 64.megabytes }

ملاحظة: بدلًا من ذلك، يمكنك استدعاء ActionController::Base.cache_store خارج كتلة الضبط.

يمكنك الوصول إلى ذاكرة التخزين المؤقتة عن طريق استدعاء Rails.cache.

ActiveSupport::Cache::Store

يوفر هذا الصنف الأساس للتفاعل مع ذاكرة التخزين المؤقت في ريلز. هذا هو صنف مجرد (abstract class) ولا يمكنك استخدامه من تلقاء نفسه. بدلًا من ذلك يجب عليك استخدام تنفيذ متماسك للصنف المرتبط بمحرك التخزين. تُشحَن ريلز مع العديد من التطبيقات الموثقة أدناه.

التوابع الرئيسية للإستدعاء هي read، و write، و delete، و ?exist و fetch.

التابع fetch يأخذ كتلة ويعيد قيمة موجودة من ذاكرة التخزين المؤقت أو تقييم الكتلة وكتابة النتيجة إلى ذاكرة التخزين المؤقت في حالة عدم وجود قيمة.

هناك بعض الخيارات الشائعة المستخدمة بواسطة جميع تطبيقات ذاكرة التخزين المؤقت. يمكن تمريرها إلى المنشئ أو التوابع المختلفة للتفاعل مع المدخلات.

  • :namespace - يمكن استخدام هذا الخيار لإنشاء مجال اسم داخل مخزن ذاكرة التخزين المؤقتة. هذا الخيار مفيد بشكل خاص إذا كان التطبيق الخاص بك يشارك ذاكرة تخزين مؤقت مع تطبيقات أخرى.
  • :compress - مفعل افتراضيًا. يضغط مدخلات ذاكرة التخزين المؤقتة بحيث يمكن تخزين المزيد من البيانات في نفس مساحة الذاكرة، مما يؤدي إلى عدد أقل من عمليات الإخلاء في الذاكرة المؤقتة ومعدل أعلى من الاعتماد واللجوء إلى الذاكرة المؤقتة (أي نسبة الطّلبات المُحتفَظ بإجاباتِها في الذاكرة المؤقتة تصبح أعلى).
  • :compress_threshold - افتراضيًا 1 كيلو بايت. تضغط مدخلات ذاكرة التخزين المؤقتة الأكبر من هذا الحد، محدد بالبايت.
  • :expires_in - يحدد هذا الخيار وقت انتهاء الصلاحية بالثواني لمدخلة في ذاكرة التخزين المؤقتة أي متى ستُزال تلقائيًا من ذاكرة التخزين المؤقت.
  • :race_condition_ttl - يستخدم هذا الخيار مع الخيار :expires_in. وسوف يمنع حدوث حالات تسابق (race conditions) عندما تنتهي صلاحية مدخلات ذاكرة التخزين المؤقتة من خلال منع عمليات متعددة من إعادة إنشاء نفس المدخلات في نفس الوقت (المعروف أيضًا باسم تأثير تناثر محتوى الذاكرة المؤقتة [cache stampede]). يحدد هذا الخيار عدد الثواني التي يمكن فيها إعادة استخدام المدخلة منتهية الصلاحية أثناء إعادة توليد قيمة جديدة. من الممارسات الجيدة تعيين هذه القيمة إذا كنت تستخدم الخيار ‎:expires_in.

مخازن ذاكرة التخزين المؤقتة المخصصة

يمكنك إنشاء مخزن مؤقت مخصص خاص بك ببساطة عن طريق توسيع ActiveSupport::Cache::Store وتنفيذ التوابع المناسبة. بهذه الطريقة، يمكنك تبديل أي عدد من تقنيات التخزين المؤقت في تطبيق ريلز.

لاستخدام مخزن مؤقت مخصص، ما عليك سوى تعيين مخزن ذاكرة التخزين المؤقت إلى نسخة جديدة للصنف المخصصة الخاص بك.

config.cache_store = MyCacheStore.new

ActiveSupport::Cache::MemoryStore

يحتفظ مخزن التخزين المؤقت هذا بالمدخلات في الذاكرة في نفس عملية روبي. يحتوي مخزن التخزين المؤقت على حجم محدد يتم تحديده عن طريق إرسال الخيار :size إلى المُهيئ (القيمة الافتراضية هي 32 ميجابايت). عندما تتجاوز ذاكرة التخزين المؤقت الحجم المخصص، ستجرى عملية تنظيف وستزال المدخلات الأقل استخدامًا مؤخرًا.

config.cache_store = :memory_store, { size: 64.megabytes }

إذا كنت تشغِّل عدة عمليات على خادم ريلز (وهي الحالة إذا كنت تستخدم الوضع Phusion Passenger أو الوضع puma clustered)، فلن تتمكن نُسَخ عملية خادم ريلز من مشاركة بيانات ذاكرة التخزين المؤقت مع بعضها بعضًا. مخزن التخزين المؤقت هذا غير مناسب لعمليات نشر التطبيقات الكبيرة. ومع ذلك، يمكن أن تعمل بشكل جيد للمواقع الصغيرة منخفضة الحركة مع اثنين فقط من عمليات الخادم، بالإضافة إلى بيئتي التطوير والاختبار.

تضبط مشاريع ريلز حديثة الإنشاء هذا التطبيق في بيئة التطوير بشكل افتراضي.

ملاحظة: نظرًا لأن العمليات لن تشارك بيانات ذاكرة التخزين المؤقت عند استخدام :memory_store، لن يكون من الممكن قراءة ذاكرة التخزين المؤقت أو الكتابة عليها أو انهاء صلاحيتها يدويًا عبر وحدة تحكم ريلز.

ActiveSupport::Cache::FileStore

يستخدم مخزن التخزين المؤقت هذا نظام الملفات لتخزين المدخلات. يجب تحديد المسار إلى المجلد حيث تُخزن ملفات المخزن عند تهيئة ذاكرة التخزين المؤقت.

config.cache_store = :file_store, "/path/to/cache/directory"

باستخدام هذا المخزن المؤقت، يمكن لعمليات الخادم المتعددة على نفس المضيف مشاركة ذاكرة تخزين مؤقت. هذا المخزن المؤقت مناسب للمواقع ذات الحركة المرورية المنخفضة أو المتوسطة التي تعرض على مضيف واحد أو اثنين. يمكن لعمليات الخادم التي تعمل على مضيفين مختلفين مشاركة ذاكرة تخزين مؤقت باستخدام نظام ملفات مشترك، لكن هذا الإعداد غير مستحسن.

عندما تكبر ذاكرة التخزين المؤقت حتى يمتلئ القرص، يُوصى بمسح المدخلات القديمة بشكل دوري.

هذا هو تنفيذ التخزين المؤقت الافتراضي (في "‎#{root}/tmp/cache/‎") إذا لم يضبط config.cache_store بشكل صريح.

ActiveSupport::Cache::MemCacheStore

يستخدم مخزن التخزين المؤقت هذا الخادم memcached الذي يخص Danga لتوفير ذاكرة تخزين مركزية لتطبيقك. يستخدم ريلز الجوهرة dalli المحزَّمة بشكل افتراضي. هذا هو مخزن ذاكرة التخزين المؤقت الأكثر شيوعًا حاليًا لمواقع الإنتاج. يمكن استخدامه لتوفير مجموعة ذاكرة تخزين مؤقت مشتركة واحدة بأداء وتوافر عالٍ جدًا.

عند تهيئة ذاكرة التخزين المؤقتة، تحتاج إلى تحديد العناوين لكافة خوادم memcached في نظام المجموعة الخاص بك. إذا لم تحدد أي منها، فستفترض أن memcached يعمل على مضيف محلي (localhost) على المنفذ الافتراضي، ولكن هذا ليس إعدادًا مثاليًا للمواقع الكبيرة.

يقبل التابعين write و fetch في ذاكرة التخزين المؤقتة هذين الخيارين الإضافيين اللذين يستفيدان من الميزات الخاصة بخوادم memcached. يمكنك تحديد :raw لإرسال قيمة مباشرة إلى الخادم بدون وجود تسلسل. يجب أن تكون القيمة عبارة عن سلسلة أو رقم. يمكنك استخدام عمليات memcached المباشرة مثل increment و decrement فقط على قيم صرفة (raw values). يمكنك أيضًا تحديد ‎:unless_exist إذا كنت لا تريد من الخادم memcached استبدال مدخلة موجودة.

config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com"

ActiveSupport::Cache::RedisCacheStore

يستفيد مخزن التخزين المؤقت Redis من دعم Redis للإخراج التلقائي عندما يصل إلى الحد الأقصى للذاكرة، مما يسمح له بالتصرف بشكل كبير مثل خادم ذاكرة التخزين المؤقت Memcached.

ملاحظة للنشر: لا تنتهي صلاحية Redis للمفاتيح افتراضيًا، لذا يجب توخي الحذر لاستخدام خادم التخزين المؤقت Redis المخصص. لا تملأ خادم Redis المستمر مع بيانات مؤقتة متطايرة! اقرأ دليل إعداد خادم التخزين المؤقت Redis بالتفصيل.

بالنسبة لخادم Redis الخاص بالذاكرة المؤقتة فقط، قم بتعيين maxmemory-policy إلى أحد المتغيرات الخاصة بكل المفاتيح. يدعم الإصدار 4 وما بعده من Redis الإخلاء الأقل استخدامًا (allkeys-lfu) ، وهو اختيار افتراضي ممتاز. يجب في الإصدار 3 وما دونه استخدام الإخلاء الأقل استخدامًا (allkeys-lru).

اضبط انتهاء مهلة ذاكرة التخزين المؤقتة للقراءة والكتابة إلى قيمة منخفضة نسبيًا. غالبًا ما يكون إعادة إنشاء قيمة مخزنة مؤقتًا أسرع من الانتظار لأكثر من ثانية لاستعادتها. تتوقف مهلة القراءة والكتابة افتراضيًا على ثانية واحدة، ولكن قد تُعين إلى قيمة أقل إذا كانت زمن تأخير الشبكة منخفضًا دومًا.

بشكل افتراضي، لن يحاول مخزن ذاكرة التخزين المؤقت إعادة الاتصال بـ Redis إذا فشل الاتصال أثناء الطلب. إذا واجهت قطع اتصال متكررة، فقد ترغب في تمكين محاولات إعادة الاتصال.

لا ترمي ذاكرة التخزين المؤقت يقرأ ويكتب استثناءات مطلقًا. هي تعيد فقط القيمة nil بدلًا من رمي الاستثناء، وتتصرف كما لو لم يكن هناك شيء في ذاكرة التخزين المؤقتة. لقياس ما إذا كانت ذاكرة التخزين المؤقتة ترمي استثناءً أم لا، يمكنك توفير التابع error_handler للإبلاغ عن خدمة تجميع الاستثناءات. يجب أن يقبل ذلك التابع ثلاث وسائط مسماة هي:

  • method: تابع تخزين ذاكرة التخزين المؤقتة الذي استدعي في الأصل؛
  • returning: القيمة التي أعيدت للمستخدم، وتكون عادةً nil؛
  • exception: الاستثناء الذي أُنقذ.

للبدء، أضف الجوهرة redis إلى الملف Gemfile:

gem 'redis'

يمكنك تمكين الدعم لمكتبة الاتصال hiredis الأسرع بواسطة إضافة مغلف روبي الخاص بها إلى الملف Gemfile الخاص بك:

gem 'hiredis'

سيطلب مخزن التخزين المؤقت Redis المكتبة hiredis تلقائيًّا ويستخدمها إذا كان متاحًا. هناك حاجة إلى مزيد من الضبط.

وأخيرًا، أضف الضبط في الملف config/environments/*.bb:

config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }

أكثر تعقيدًا، قد يظهر مخزن Redis الإنتاجي للتخزين المؤقت كالتالي:

cache_servers = %w(redis://cache-01:6379/0 redis://cache-02:6379/0)
config.cache_store = :redis_cache_store, { url: cache_servers,
 
  connect_timeout: 30,  # Defaults to 20 seconds
  read_timeout:    0.2, # Defaults to 1 second
  write_timeout:   0.2, # Defaults to 1 second
 
  error_handler: -> (method:, returning:, exception:) {
    # Report errors to Sentry as warnings
    Raven.capture_exception exception, level: 'warning',
      tags: { method: method, returning: returning }
  }
}

ActiveSupport::Cache::NullStore

من المفترض استخدام تنفيذ مخزن ذاكرة التخزين المؤقت هذا فقط في بيئات التطوير أو الاختبار ولا يخزن أي شيء أبدًا. قد يكون هذا مفيدًا جدًا في التطوير عندما يكون لديك شيفرة تتفاعل مباشرةً مع Rails.cache ولكن قد يتداخل التخزين المؤقت مع إمكانية رؤية نتائج تغييرات التعليمات البرمجية. باستخدام هذا المخزن المؤقت، ستؤدي جميع عمليات fetch و read إلى فقدانها.

config.cache_store = :null_store

مفاتيح ذاكرة التخزين المؤقت

يمكن أن تكون المفاتيح المستخدمة في ذاكرة التخزين المؤقتة أي كائن يستجيب إلى cache_key أو to_param. يمكنك تنفيذ التابع cache_key في الأصناف الخاصه بك إذا كنت تحتاج إلى إنشاء مفاتيح مخصصة. سيعمل Active Record على إنشاء مفاتيح استنادًا إلى اسم الصنف ومعرف السجل.

يمكنك استخدام جدول Hash ومصفوفات من القيم كمفاتيح ذاكرة التخزين المؤقت.

# هذا مفتاح ذاكرة مؤقتة صالح
Rails.cache.read(site: "mysite", owners: [owner_1, owner_2])

لن تكون المفاتيح التي تستخدمها مع Rails.cache هي نفسها المفاتيح المستخدمة فعليًا مع محرك التخزين. يمكن تعديلها باستخدام مجال اسم أو تعديلها لتلائم القيود الخلفية للتقنية. هذا يعني (technology backend constraints). على سبيل المثال، لا يمكنك حفظ القيم باستخدام Rails.cache وبالتالي تحاول سحبها باستخدام الجوهرة dalli. ومع ذلك، لا داعي للقلق أيضًا بشأن تجاوز حد حجم memcached أو انتهاك قواعد تركيب الصياغة.

دعم GET الشرطي

تعتبر طلبيات GET المشروطة إحدى ميزات مواصفات HTTP التي توفر طريقة لخوادم الويب لإخبار المستعرضات بأن الاستجابة لطلب GET لم يتغير منذ آخر طلب ويمكن سحبها بأمان من ذاكرة التخزين المؤقت للمتصفح.

وهي تعمل باستخدام الترويسة HTTP_IF_NONE_MATCH والترويسة HTTP_IF_MODIFIED_SINCE لتمرير كل من معرف محتوى فريد وطابع زمني لآخر مرة تغير فيها المحتوى. إذا قدم المتصفح طلبًا حيث يطابق معرّف المحتوى (الإصدار) أو آخر تعديل منذ الطابع الزمني مع إصدار الخادم، يحتاج الخادم فقط إلى إرسال رد فارغ بحالة لم تُعدل.

يتحمل الخادم (الخاص بنا) مسؤولية البحث عن آخر طابع زمني معدّل وتحدد الترويسة if-none-match ما إذا كان سيرسل الاستجابة الكاملة أم لا. مع توافر دعم GET الشرطية في ريلز، هذه مهمة سهلة جدًا:

class ProductsController < ApplicationController
 
  def show
    @product = Product.find(params[:id])
 
    # etag إن أصبح الطلب قديمًا وفقًا للطابع الزمني وقيمة
    # أي يحتاج الطلب لمعالجته مجددًّا)، فستنفَّذ هذه الكتلة)
    if stale?(last_modified: @product.updated_at.utc, etag: @product.cache_key_with_version)
      respond_to do |wants|
        # ... معالجة الطلب بشكل طبيعي
      end
    end
 
    # إن كان الطلب حديثًا (أي لم يتغير)، فلن تحتاج إلى فعل أي شيء
    # تتحقق عملية التصيير الافتراضية لهذا باستعمال المعاملات المستعملة
    # :not_modified وسيرسل تلقائيًا stale? في الاستدعاء السابق إلى
    # هذا كل ما في الأمر. انتهينا
  end
end

بدلًا من جدول Hash من الخيارات، يمكنك أيضًا ببساطة تمرير نموذج. سيستخدم ريلز التابعين updated_at و cache_key_with_version لإعداد last_modified و etag:

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])
 
    if stale?(@product)
      respond_to do |wants|
        # ... معالجة الطلب بشكل طبيعي
      end
    end
  end
end

إذا لم تكن لديك أية معالجة خاصة للرد وكنت تستخدم آلية التنفيذ الافتراضية (بمعنى أنك لا تستخدم response_to أو تستدعي render بنفسك)، فحينئذٍ يكون لديك مساعد سهل الاستعمال هو fresh_when:

class ProductsController < ApplicationController
 
  # تلقائيًاإن كان الطلب حديثًا :not_modified هذا سيعيد إرسال
  # إن كان الطلب قديمًا (product.*) وسيصيّر القالب الافتراضي
 
  def show
    @product = Product.find(params[:id])
    fresh_when last_modified: @product.published_at.utc, etag: @product
  end
end

في بعض الأحيان، نريد تخزين الاستجابة مؤقتًا، على سبيل المثال صفحة ثابتة، لا تنتهي صلاحيتها أبدًا. ولتحقيق ذلك، استخدم المساعد http_cache_forever؛ ومن خلال إجراء ذلك، سيعمل المتصفح والوسائط (proxies) على تخزينها مؤقتًا إلى أجل غير مسمى.

ستكون الاستجابات المخزنة مؤقتًا بشكل افتراضي خاصة، و تخزن مؤقتًا فقط على متصفح الويب للمستخدم. للسماح للوسائط (proxies) تخزين الاستجابة مؤقتًا، يمكنك تعيين public: true للإشارة إلى أنه يمكنهم عرض الاستجابة المخزنة مؤقتًا لجميع المستخدمين.

باستخدام هذا المساعد، تُعيَّن الترويسة last_modified إلى Time.new(2011، 1، 1).utc وتضبط الترويسة expires إلى 100 عام.

تحذير: استخدم هذا التابع بعناية نظرًا لأن المتصفح / الوسيط لن يتمكن من إبطال الاستجابة المخزنة مؤقتًا ما لم تُمحى ذاكرة التخزين المؤقتة للمتصفح.

class HomeController < ApplicationController
  def index
    http_cache_forever(public: true) do
      render
    end
  end
end

الترويسات ETag القوية والضعيفة

ريلز ينشئ ترويسات ETag ضعيفة بشكل افتراضي. تتيح ترويسات ETag الضعيف استجابات ذات دلالة مكافئة، إذ يكون لعدة الاستجابات نفس ترويسات ETag، رغم كون محتواها غير متطابق تمامًا. يفيد ذلك عندما لا نريد إعادة توليد الصفحة لتغييرات طفيفة في محتوى الاستجابة.

تملك الترويسات ETag الضعيفة البادئة W/‎ لتمييزها عن الترويسات ETag القوية.

W/"618bbc92e2d35ea1945008b42799b0e7" → Weak ETag
"618bbc92e2d35ea1945008b42799b0e7" → Strong ETag

بعكس الترويسات ETag الضعيف، تشير الترويسات ETag القوية إلى أن الاستجابة يجب أن تكون متماثلة تمامًا ومتطابقة بايتًا ببايت. هذا مفيد عند تنفيذ طلبات النطاق داخل ملف فيديو أو ملف PDF كبير. بعض شبكات توززيع المحتوى (CDN) تدعم فقط الترويسات ETag القوية، مثل Akamai. إذا كنت في حاجة ماسة إلى إنشاء ترويسات ETag قوية، فيمكن القيام بذلك على النحو التالي:

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])
    fresh_when last_modified: @product.published_at.utc, strong_etag: @product
  end
end

يمكنك أيضًا تعيين ترويسات ETag القوية مباشرة على الاستجابة:

response.strong_etag = response.body # => "618bbc92e2d35ea1945008b42799b0e7"

التخزين المؤقت في التطوير

من الشائع أن ترغب في اختبار استراتيجية التخزين المؤقت للتطبيق الخاص بك في وضع التطوير. يوفر ريلز المهمة dev:cache التي تخص rake لتبديل التخزين المؤقت بسهولة بين التشغيل والإيقاف (on/off).

$ bin/rails dev:cache
Development mode is now being cached.
$ bin/rails dev:cache
Development mode is no longer being cached.

المراجع

مصادر