Redis Best Practices ، الجزء الثاني

الجزء الثاني من دورة ترجمة Redis Best Practices من Redis Labs ، ويناقش أنماط التفاعل وأنماط تخزين البيانات.

الجزء الأول هنا .

أنماط التفاعل


يمكن أن يعمل Redis ليس فقط كنظام DBMS تقليدي ، ولكن يمكن أيضًا استخدام هياكله وأوامره لتبادل الرسائل بين الخدمات الدقيقة أو العمليات. يتيح لك الاستخدام الواسع النطاق لعملاء Redis ، وسرعة وكفاءة الخادم والبروتوكول ، بالإضافة إلى الهياكل الكلاسيكية المدمجة إنشاء سير العمل وآليات الأحداث الخاصة بك. في هذا الفصل ، سنتناول الموضوعات التالية:

  • طابور الأحداث ؛
  • الحظر مع Redlock ؛
  • حانة / Sub ؛
  • الأحداث الموزعة.

قائمة انتظار الأحداث


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

تحتوي القوائم على مجموعة فرعية من الأوامر التي تتيح لك تنفيذ سلوك "الحظر". يشير مصطلح "المنع" إلى اتصال مع عميل واحد فقط. في الواقع ، لا تسمح هذه الأوامر للعميل بفعل أي شيء حتى تظهر قيمة في القائمة أو حتى انتهاء المهلة. هذا يلغي الحاجة لاستطلاع Redis ، في انتظار النتيجة. نظرًا لأن العميل لا يمكنه فعل أي شيء بينما يتوقع قيمة ، سنحتاج إلى عميلين مفتوحين لتوضيح هذا:
#العميل 1العميل 2
1
> BRPOP my-q 0
[توقع القيمة]
2
> LPUSH my-q hello
(integer) 1
1) "my-q"
2) "hello"
[العميل غير مقفل ، جاهز لقبول الأوامر]
3
> BRPOP my-q 0
[توقع القيمة]

في هذا المثال ، في الخطوة 1 ، نرى أن العميل المحظور لا يعيد أي شيء على الفور ، لأنه لا يحتوي على أي شيء. الحجة الأخيرة هي وقت الانتظار. هنا 0 يعني التوقع الأبدي. في السطر الثاني ، يتم إدخال قيمة في my-q ، ويخرج العميل الأول فورًا من حالة الحظر. في السطر الثالث ، يتم استدعاء BRPOP مرة أخرى (يمكنك القيام بذلك في حلقة في التطبيق) ، وينتظر العميل أيضًا القيمة التالية. بالضغط على "Ctrl + C" يمكنك كسر القفل والخروج من العميل.

دعنا نعكس المثال ونرى كيف يعمل BRPOP مع قائمة غير فارغة:
#العميل 1العميل 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
[توقع القيمة]

في الخطوات 1-3 ، نضيف 3 قيم إلى القائمة ونرى أن الجواب ينمو ، مع الإشارة إلى عدد العناصر في القائمة. الخطوة 4 ، على الرغم من استدعاء BRPOP ، تقوم بإرجاع القيمة على الفور. وذلك لأن سلوك الحظر يحدث فقط عندما لا تكون هناك قيم في قائمة الانتظار. يمكننا رؤية نفس الاستجابة الفورية في الخطوات 5-6 لأنه يتم ذلك لكل عنصر في قائمة الانتظار. في الخطوة 7 ، لا يجد BRPOP أي شيء في قائمة الانتظار ويحظر العميل حتى تتم إضافة شيء.

غالبًا ما تمثل قوائم الانتظار بعض العمل الذي يجب القيام به في عملية أخرى (عامل). في هذا النوع من عبء العمل ، من المهم ألا يختفي العمل إذا سقط العامل لسبب ما أثناء التنفيذ. يدعم Redis هذا النوع من قائمة الانتظار. للقيام بذلك ، استخدم الأمر BRPOPLPUSH بدلاً من BRPOP. تتوقع قيمة في قائمة ، وبمجرد ظهورها هناك ، تضعها في قائمة أخرى. يتم ذلك بشكل ذري ، لذلك من المستحيل على عاملين تغيير نفس القيمة. دعونا نرى كيف يعمل:
#العميل 1العميل 2
1
> LINDEX worker-q 0
(nil)
2[إذا كانت النتيجة ليست صفرية ، فقم بمعالجتها بطريقة ما وانتقل إلى الخطوة 4]
3
> LREM worker-q -1 [   1]
(integer) 1
[العودة إلى الخطوة 1]
4
> BRPOPLPUSH my-q worker-q 0
[توقع القيمة]
5
> LPUSH my-q hello
"hello"
[العميل غير مقفل ، جاهز لقبول الأوامر]
6[تعامل مع مرحبا]
7
> LREM worker-q -1 hello
(integer) 1
8[العودة إلى الخطوة 1]

في الخطوتين 1 و 2 ، لا نقوم بأي شيء ، لأن العامل q فارغ. إذا عاد شيء ما ، فإننا نقوم بمعالجته وحذفه ، ونعود مرة أخرى إلى الخطوة 1 للتحقق من وجود شيء ما في قائمة الانتظار. وبالتالي ، نقوم أولاً بمسح قائمة انتظار العامل ونؤدي العمل الحالي. في الخطوة 4 ، ننتظر حتى تظهر القيمة في my-q ، وعندما تظهر ، يتم نقلها تلقائيًا إلى worker-q . ثم نعالج بطريقة ما "مرحبًا" ، وبعد ذلك نحذفها من worker-q ونعود إلى الخطوة 1. إذا ماتت العملية في الخطوة 6 ، فستظل القيمة في worker-q . بعد إعادة تشغيل العملية ، سنحذف على الفور كل شيء لم يتم حذفه في الخطوة 7.

يقلل هذا النمط بشكل كبير من احتمالية فقدان الوظيفة ، ولكن فقط إذا مات العامل بين الخطوتين 2 و 3 أو 5 و 6 ، وهو أمر غير مرجح ، لكن أفضل الممارسات ستأخذ ذلك في الاعتبار في منطق العامل.

قفل مع redlock


في بعض الأحيان في النظام ، من الضروري حظر بعض الموارد. قد يكون ذلك ضروريًا لتطبيق التغييرات المهمة التي لا يمكن حلها في بيئة تنافسية. أهداف المنع:

  • السماح لعامل واحد فقط بالاستيلاء على المورد ؛
  • تكون قادرة على تحرير كائن القفل بشكل موثوق ؛
  • لا تقفل المورد بإحكام (يجب فتحه بعد فترة زمنية معينة).

يعد Redis خيارًا جيدًا لتنفيذ الحظر ، نظرًا لأنه يحتوي على نموذج بيانات بسيط قائم على المفتاح ، وكل قطعة صغيرة مترابطة الأحادية وسريعة إلى حد ما. هناك تطبيق قفل ممتاز باستخدام Redis يسمى Redlock.
يتوفر عملاء Redlock لكل اللغات تقريبًا ، ومع ذلك ، من المهم معرفة كيفية عمل Redlock من أجل استخدامه بأمان وفعالية.

أولاً ، عليك أن تفهم أن Redlock مصمم للعمل على 3 أجهزة على الأقل مع حالات Redis مستقلة. هذا يزيل نقطة الفشل الوحيدة في آلية القفل الخاصة بك ، والتي يمكن أن تؤدي إلى طريق مسدود لجميع الموارد. نقطة أخرى يجب فهمها هي أنه على الرغم من أن الساعات الموجودة على الماكينات لا يجب أن تكون متزامنة بنسبة 100 ٪ ، فيجب أن تعمل بنفس الطريقة - يتحرك الوقت بنفس السرعة: ثانية واحدة على الجهاز ونفس ثانية واحدة على الجهاز B.

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

لتحرير كائن قفل ، من الأفضل استخدام برنامج Lua النصي الذي سيتحقق مما إذا كانت القيمة العشوائية المتوقعة في مجموعة المفاتيح. إذا كان هناك ، فيمكنك حذفه ، وإلا فمن الأفضل ترك المفاتيح ، حيث قد تكون هذه كائنات قفل أحدث.

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

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

Pub / Sub


بالإضافة إلى تخزين البيانات ، يمكن أيضًا استخدام Redis كمنصة Pub / Sub (الناشر / المشترك). في هذا النمط ، يمكن للناشر إصدار رسائل لأي عدد من المشتركين في القناة. هذه رسائل تستند إلى مبدأ "اللقطة والنسيان" ، أي إذا تم إصدار الرسالة ولم يكن المشترك موجودًا ، فستختفي الرسالة دون إمكانية الاسترداد.
من خلال الاشتراك في القناة ، يدخل العميل في وضع المشترك ولا يمكنه استدعاء الأوامر - يصبح للقراءة فقط. الناشر ليس لديه مثل هذه القيود.

يمكنك الاشتراك في أكثر من قناة. نبدأ بالاشتراك في قناتي الطقس والرياضة باستخدام أمر الاشتراك:

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

في عميل منفصل (نافذة طرفية أخرى ، على سبيل المثال) يمكننا نشر الرسائل في أي من هذه القنوات باستخدام الأمر PUBLISH:

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

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

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

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

بالعودة إلى الناشر ، قد ننشر رسالة أخرى:

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

في المشترك سنرى نفس التنسيق ، ولكن مع قناة مختلفة مع الرسالة:

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

دعنا ننشر رسالة إلى قناة لا يوجد فيها مشتركون:

> PUBLISH currency CADUSD/0.787
(integer) 0

نظرًا لأن لا أحد يستمع إلى قناة العملة ، فإن الإجابة ستكون 0. هذه الرسالة قد اختفت ، ولن يتلقى العملاء الذين بعد الاشتراك في هذه القناة إشعارًا حول هذه الرسالة - تم إرسالها ونسيانها.

بالإضافة إلى الاشتراك في قناة واحدة ، يسمح Redis بالاشتراك في القنوات حسب القناع. يتم تمرير قناع نمط glob إلى الأمر PSUBSCRIBE:

> PSUBSCRIBE sports:*

سوف يتلقى العميل رسائل من جميع القنوات، بدءا الرياضية: . في عميل آخر ، اتصل بالأوامر التالية:

> 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

يرجى ملاحظة أن أول فريقين يعودان 1 ، بينما يعود آخر 0. وعلى الرغم من أننا غير مشتركين مباشرة في الألعاب الرياضية: الهوكي أو الرياضة: كرة السلة ، يتلقى العميل الرسائل من خلال الاشتراك بواسطة القناع. في نافذة المشترك-العميل ، يمكننا أن نرى أن هناك نتائج للقنوات المطابقة للقناع فقط.

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

يختلف هذا الإخراج قليلاً عن إخراج الأمر SUBSCRIBE لأنه يحتوي على القناع نفسه ، بالإضافة إلى الاسم الحقيقي للقناة.

الأحداث الموزعة


يمكن توسيع نظام رسائل Pub / Sub من Redis لإنشاء أحداث موزعة مثيرة للاهتمام. لنفترض أن لدينا بنية مخزنة في جدول تجزئة ، لكننا لا نريد تحديث العملاء إلا عندما يتجاوز حقل واحد القيمة العددية التي حددها المشترك. سنستمع للقنوات عن طريق القناع ونستخرج التجزئة في الحالة . في هذا المثال ، نحن مهتمون بتحديث الحالة بقيم 5-9.

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

لتغيير قيمة الحالة / error_level ، نحتاج إلى أمرين يمكن تنفيذهما بالتسلسل أو في كتلة MULTI / EXEC. يقوم الأمر الأول بتعيين المستوى ، وينشر الأمر الثاني إشعارًا بالقيمة المشفرة في القناة نفسها.

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

في النافذة الأولى ، نرى أنه تم استلام الرسالة ، وبعد ذلك يمكنك التبديل إلى عميل آخر واستدعاء الأمر HGETALL:

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

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

يمكننا أيضًا استخدام هذه الطريقة لتحديث المتغير المحلي لبعض العمليات الطويلة. يمكن أن يسمح ذلك لمثيلات متعددة من نفس العملية بتبادل البيانات في الوقت الفعلي.

لماذا هذا النمط أفضل من استخدام Pub / Sub؟ عند إعادة تشغيل العملية ، يمكنها فقط الحصول على الحالة الكاملة والبدء في الاستماع. ستتم مزامنة التغييرات بين أي عدد من العمليات.

أنماط تخزين البيانات


هناك العديد من الأنماط لتخزين البيانات المنظمة في Redis. سننظر في هذا الفصل فيما يلي:

  • تخزين البيانات في JSON ؛
  • مرافق التخزين.

تخزين بيانات JSON


هناك العديد من الخيارات لتخزين بيانات JSON في Redis. الشكل الأكثر شيوعًا هو إجراء تسلسل للكائن مقدمًا وحفظه تحت مفتاح خاص:

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

قد يبدو الأمر بسيطًا ، ولكن له بعض العوائق الخطيرة للغاية:

  • يتطلب التسلسل موارد حوسبة العميل للقراءة والكتابة ؛
  • يزيد تنسيق JSON من حجم البيانات ؛
  • لدى Redis طريقة غير مباشرة فقط لمعالجة البيانات في JSON.

قد تكون النقطتان الأوليان مهمشتان في كميات صغيرة من البيانات ، لكن التكاليف ستزداد مع نمو البيانات. ومع ذلك ، فإن النقطة الثالثة هي الأكثر أهمية.

قبل Redis 4.0 ، كانت الطريقة الوحيدة للعمل مع JSON داخل Redis هي استخدام برنامج Lua النصي في وحدة cjson. أدى هذا إلى حل المشكلة جزئيًا ، على الرغم من أنه لا يزال يمثل اختناقًا وخلق متاعب إضافية مع تعلم Lua. بالإضافة إلى ذلك ، استلمت العديد من التطبيقات ببساطة سلسلة JSON بالكامل ، وأزلت سلطتها ، وعملت مع البيانات ، وسلسلتها مرة أخرى ، وحفظتها مرة أخرى. هذا مضاد. هناك خطر كبير من فقدان البيانات بهذه الطريقة.

#مثيل التطبيق # 1مثيل التطبيق رقم 2
1
> GET my-car
2[إلغاء التسلسل وتغيير لون الجهاز والتسلسل مرة أخرى]
> GET my-car
3
> SET my-car

[قيمة جديدة من المثال رقم 1]
[إلغاء التسلسل وتغيير طراز الجهاز والتسلسل مرة أخرى]
4
> SET my-car

[قيمة جديدة من المثال رقم 2]
5
> GET my-car

ستظهر النتيجة في السطر 5 التغييرات على المثال 2 فقط ، وسيتم فقدان تغيير اللون بواسطة المثال 1.

Redis الإصدار 4.0 وأعلى لديه القدرة على استخدام الوحدات. ReJSON هي وحدة نمطية توفر نوع بيانات خاص وأوامر للتفاعل المباشر معها. يحفظ ReJSON البيانات في تنسيق ثنائي ، مما يقلل من حجم البيانات المخزنة ، ويوفر وصولاً أسرع إلى العناصر دون قضاء الوقت في de / التسلسل.

لاستخدام ReJSON ، تحتاج إلى تثبيته على خادم Redis أو تمكينه في Redis Enterprise.

سيبدو المثال السابق باستخدام ReJSON كما يلي:

#مثيل التطبيق # 1مثيل التطبيق رقم 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 طريقة أكثر أمانًا وسرعة وبديهية للعمل مع بيانات JSON في Redis ، خاصة في الحالات التي تكون فيها التغييرات الذرية للعناصر المتداخلة ضرورية.

تخزين الكائن


للوهلة الأولى ، قد يبدو نوع بيانات Redis القياسي "جدول التجزئة" مشابهًا جدًا لكائن JSON أو نوع آخر. من الأسهل إنشاء الحقول إما سلسلة أو رقم ومنع الهياكل المتداخلة. ومع ذلك ، بعد حساب "المسار" لكل حقل ، يمكنك "تسوية" الكائن وحفظه في جدول التجزئة Redis.

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

باستخدام JSONPath (XPath لـ JSON) ، يمكننا تمثيل كل عنصر في نفس مستوى جدول التجزئة:

> 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

للتوضيح ، يتم سرد الأوامر بشكل منفصل ، ولكن يمكن تمرير العديد من المعلمات إلى HSET.

يمكنك الآن طلب الكائن بالكامل أو حقله الفردي:

> 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"

على الرغم من أن هذا يوفر طريقة سريعة ومفيدة لاسترداد كائن مخزَّن في Redis ، إلا أن له عيوبه:

  • في اللغات والمكتبات المختلفة ، قد يكون تنفيذ JSONPath مختلفًا ، مما يتسبب في عدم التوافق. في هذه الحالة ، يجدر إجراء تسلسل البيانات وإلغاء تسلسلها باستخدام أداة واحدة ؛
  • دعم المصفوفة:
    • يمكن أن تكون الصفائف المتفرقة مشكلة ؛
    • من المستحيل إجراء العديد من العمليات ، مثل إدراج عنصر في منتصف صفيف.

  • استهلاك الموارد غير الضرورية في مفاتيح JSONPath.

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

سيغطي القسم الختامي التالي أنماط السلاسل الزمنية وأنماط حدود السرعة وأنماط مرشح بلوم والعدادات واستخدام Lua في Redis.

ملاحظة: حاولت تكييف نص هذه المقالات باللغة الإنجليزية "البربرية" قدر الإمكان إلى اللغة الروسية ، ولكن إذا كنت تعتقد أن الفكرة في مكان ما غير مفهومة أو غير صحيحة ، فصححني في التعليقات.

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


All Articles