یه دیس Redis


چیزی که از redis میشه به عنوان یه معرفی مختصر گفت اینه که یه تکنولوژی (دیتابیس) برای ذخیره سازی دیتا توی مموری هست. با توجه به in memory بودنش و هسته قدرتمند و انواع دیتایی که برای خوندن و نوشتن در اختیارمون قرار میده، گزینه ی مناسب ( و تقریبا همه گیری ) برای cache کردن دیتاست. اگر به مبحث cache علاقمند هستین، شما رو ارجاع میدم که بعد از خوندن این مقاله (اگه خوندین البته D:) ، مقاله ی دیگه ی منو در همین رابطه مطالعه بفرمایین :‌

https://vrgl.ir/MRHTe


ارائه دیتا تایپ های مختلف برای خوندن و نوشتن دیتا، مساله ی حائز اهمیتیه. از این جهت که اگر ساختار دیتای خاصی، غیر از دیتا تایپ های بیسیک رو برای ذخیره کردن مد نظر داشته باشیم، چنانچه دیتا تایپ معادلش توی دیتابیسمون وجود نداشته باشه، ما به ناچار دوبار عمل cast دیتا یا encoding و decoding (یا شایدم marshal/unmarshal خدا عالمه ) خواهیم داشت. قاعدتا یه با ر وقتی دیتا رو مینویسیم و یه بارهم وقتی میخوایم بخونیم و استفاده کنیم. این ممکنه در نظر اول هزینه پردازش و زمانی چشمگیری نداشته باشه، اما امان از وقتی که تعدادش بالا بره... . حالا اینو بزاریم کنار امکان خیلی کوچیک بروز خطا در پروسه تبدیل. خلاصه اینکه وجود این دیتا تایپ ها، دستمونو باز تر و سرمونو ( و سر سرور مونو) خلوت تر میکنه. به علاوه این دیتا تایپ ها هر کدوم در کیس های خاصی از کاربرد، خودشون میتونن با تکیه بر توانایی redis ، قسمتی از راه حل مساله باشن. مثل وقتی که به یه صف یا stream نیاز داریم تا رشته ای یا مجموعه ای . از دیتا ها رو مدیریت کنیم.
و اما دیتا تایپ های ردیس:‌


۱- دیتای رشته ای یا همون string
برای redis توی ذخیره سازی string یه حالت جفت key/value وجود داره. کلید ها (key) به صورت باینری نگهداشته میشن و خیلی مهمه که بدونیم یونیک هستن یعنی میشه به عنوان یه مپ هم بهش نگاه کرد (میشه که چه عرض کنم... مپه دیگه). از طرف دیگه value ها مقادیری با ظرفیت حداکثری 512 مگابایتی هستن که میتونن تکست، آرایه های باینری یا مستقیما object ها (در ادبیات دیگه struct ) رو در خودشون نگه دارن(این تعریف از value ها یه خورده کلیه و خیلی ارتباطی به قضیه string نداره). تو خود کامند لاینش، خوندن و نوشتن string ها به این صورته:

SET my_key = my value
GET my_key 
Output : my value 


۲- دیتای hash
مفهوم hash توی redis رو باید از تعریف خودش ببینیم که درواقع شامل یه لیست از key/value ها میشه. همین خاصیت از redis هست که به ما اجازه میده مستقیما structure های دیتا رو توش بنویسیم و بخونیم. جایی خوندم که هر hash از redis میتونه تا 4 میلیارد key/value تو خودش نگهداره! رسما تو بعضی از work scale ها میشه کل یه دیتابیس رو تو یکی از hash هاش نوشت ( به درست و غلطی و کاربرد این کار فکر نکردم البته).
برای ذخیره hash ها توی redis ، دستوراتی با پیشوند H به نمایندگی از hash وجود دارن. فرض کنین من ساختار کاربری به این شکل دارم:‌

{
name: hamzah,
family : moazedy
}

برای وارد کردن این ساختار به redis به این شکل عمل میکنم:‌

HSET user:1 name hamzah family moazedy

حالا برای خوندنش به صورت یکجا میتونم از HGETALL استفاده کنم، یا اینکه برای خوندن یکی از فیلداش با استفاده از اسم اون فیلد، دیتا رو بخونم:‌

HGETALL user:1
output : 
1) name
2) hamzah
3) family
4) moazedy

HGET user:1 name
output:
hamzah

و یه سری فیچر دیگه که پیشنهاد میکنم یه سرچی راجع بهشون بکنین.


۳- دیتاهای list
این نوع دیتا توی redis رو میشه یه سری linked list از string ها درنظر گرفت که باهشون میشه ساختار stack یا صف ها (queue) رو پیاده سازی کرد. هر لیست یه کلید داره و دیتاهایی بهش وارد میشن، به همون ترتیبی که وارد شدن قرار خواهند گرفت. به این ترتیب با کامندهایی که redis در اختیارمون میزاره (LPUSH / RPUSH ) میتونیم تصمیم بگیریم که این ترتیب چطوری باشه، first in last out یا first in first out . مجموعه این ویژگی ها بهمون این امکان رو میده که با redis نه تنها بتونیم stack و queue پیاده کنیم، بلکه تو بعضی موارد میشه ازش به عنوان message broker هم استفاده کرد.
مثال زیر یه نمونه از صف first in last out از من و برادرامه :)

127.0.0.1:6379> LPUSH users hamzah
(integer) 1
127.0.0.1:6379> LPUSH users khaled
(integer) 2
127.0.0.1:6379> LPUSH users saeed
(integer) 3
127.0.0.1:6379> LPOP users
saeed
127.0.0.1:6379> LPOP users
khaled
127.0.0.1:6379> LPOP users
hamzah

یه سری دستورات دیگه هم رو redis برای queue هستن که پیشنهاد میکنم یه سرچی راجع بهشون بکنین. نکته ی آخر درمورد صفهای redis اینه که تا 4,294,967,295 عدد میتونن ظرفیت داشته باشن، پس با خیال راحت توش دیتا بریزین.


۴- مجموعه ها در redis
مجموعه ها (set) در redis شامل دسته ای از دیتاهای یونیک هستن که ترتیب خاصی هم ندارن. به این صورت که اعضای مجموعه نمیتونن تکراری باشن و عملیات روی مجموعه ها (اجتماع، اشتراک و ... ) روی اونها هم اجرا میشه. نوشتن یه عضو تکراری توی یه مجموعه، تاثیر و تغییری روی مجموعه نمیده. با امکاناتی که مجموعه ها میدن، میشه اعضا رو اضافه یا حذف کرد ویا عضویت یکیشون رو تو مجموعه بررسی کرد

127.0.0.1:6379> SADD brothers khaled hamzah saeed
(integer) 3
127.0.0.1:6379> SMEMBERS brothers
1) khaled
2) hamzah
3) saeed
127.0.0.1:6379> SISMEMBER users hamzah
(integer) 1


۵- مجموعه های منظم در redis
این نوع از دیتا رو میشه ترکیب مجموعه ها و hash توی redis درنظر گرفت (البته نه به صورت کامل). توی مجموعه های منظم شما میتونین جفت هایی از دیتا ذخیره کنین که شامل یه امتیاز و یه عنوان برای اون امتیاز هست. مثل مجموعه ها ، اینجا هم نمیشه عینا عضو مجموعه منظم یعنی جفت امتیاز و عنوان تکراری باشن، اما میشه با امتیاز های یکسان، عناوین مختلفی ذخیره کرد. مثالش میتونه ذخیره کردن نمرات دانش آموزای یه کلاس توی یه در به خصوص باشه. منطقا یه دانش آموز نمیتونه توی اون درس دوتا نمره داشته باشه ولی دوتا دانش آموز میتونن نمره ی یکسانی داشته باشن.

127.0.0.1:6379> zadd class_score  20 khaled 19 hamzah 18 saeed
(integer) 3
127.0.0.1:6379> zrange class_score 0 -1
1) saeed
2) hamzah
3) khaled
127.0.0.1:6379> zrange class_score 0 -1 withscores
1) saeed
2) 18
3) hamzah
4) 19
5) khaled
6) 20
127.0.0.1:6379> zadd class_score  20 gholy
(integer) 1
127.0.0.1:6379> zrange class_score 0 -1 withscores
1) saeed
2) 18
 3) hamzah
4) 19
5) gholy
6) 20
 9) khaled
10) 20


۶- دیتای stream در redis
دیتای stream در redis یک دیتا استراکچر جدیده که redis باتوجه به نیاز فراوان امروز برای هندل کردن دیتای استریم ایجاد کرده. باید بدونیم که استریم هم در اصل به صورت key/value ذخیره میشه که همین امر باعث میشه بتونیم کامندهای این دسته دیتا رو روی استریم ها هم داشته باشیم (مثل DEL و EXPIRE ). استریم ها در واقع یک فلو مداوم از دیتاست که ترتیبشون ثابته و قابل تغییر نیست و شبیه یه صفه که دیتای جدید مدام به آخرش اضافه میشه. هر عضو از یه استریم یه جفت از key/value هست که با دیتای مرتبط با قطعه های دیگه ای از دیتاست. دیتایی که به بخشهای کوچکتر شکسته شده و هر بخشش کلید منحصر به فرد خودش رو داره. هر عضو از این استریم با یه id منحصر به فرد شناخته میشه که باتوجه به اهمیت ترتیب دیتا در استریم، این id ها با یه timestamp همراه هستن.

برای پردازش دیتای استریم، redis دوتا حالت رو اجازه میده، یکی اینکه دیتای استریم شده، هر عضوش تنها توسط یه مصرف کننده دیتا دیده و پردازش بشه، یا اینکه مصرف کننده های مختلفی بتونن همزمان یه دیتا رو ببینین و پردازش کنن. ایم امر توسط consumer group ها هندل میشه. consumer group کمک میکنه که کار بین مصرف کننده ها توزیع بشه و لود بین اونها تقسیم بشه. به این ترتیب هر replica از یه برنامه یکسان، تکه ی جدایی از دیتا رو هندل خواهد کرد.
برای اضافه کردن دیتای جدید به استریم، نقش پابلیشر در سیستم از دستور XADD استفاده میکنه. با این کار یه key/value جدید به یه استریم خاص اضافه میکنه. در این حالت اگه استریم خواسته شده وجود نداشته باشه،redis اونو ایجاد میکنه و دیتا رو توش میریزه. در هنگام اضافه شدن دیتا به استریم، این دستور یه id برای دیتا جنریت میکنه که همونطور که عرض کردم یه timestamp تو خودش داره و شماره ایکه نشون دهنده ی جایگاه دیتا توی توالی اون استریم هست.

127.0.0.1:6379> XADD brothers_stream * name khaled score 100
1719220675816-0
127.0.0.1:6379> XADD brothers_stream * name hamzah score 98
1719220697600-0

حالا اگر دقت کنین مقدار بازگشتی رو که ترکیب timestamp با sequence number هست برای id میبینین. اما چرا sequence number برای هردو دیتا یکیه (صفره درواقع) ؟ ببینین اساس کار id یونیک بودنه و زمانی که insert های شما در timestamp های متفاوتی انجام بشه، چون مقدار timestamp مختص همون لحظه ی نوشتن دیتاست، در نتیجه id یونیک میشه. اما اگه وارد کردن دیتا به استریم در یه میلی ثانیه مشترک انجام بشه، اون موقع است که redis میاد و با افزایش sequence number یونیک بودن id رو تظمین میکنه. مقدار * هم جانشین id هست و به redis میگه که خودش بر اساس ترکیبی که ازش صحبت کردیم، برای دیتا id جنریت کنه.
سمت دیگه اما مصرف کننده ی دیتاست. برای خوندن دیتا از یه استریم، از یه دستوری شبیه دستور پایین استفاده میکنیم

127.0.0.1:6379> XREAD COUNT 2 BLOCK 1000 STREAMS brothers_stream 0-0
1) 1) brothers_stream
   2) 1) 1) 1719220675816-0
         2) 1) name
           2) khaled
            3) score
            4) 100
      2) 1) 1719220697600-0
         2) 1) name
            2) hamzah
            3) score
            4) 98

خب بریم سراغ شکستن دستور. XREAD که کلمه کلیدی برای خوندن از استریمه. بعدش COUNT (اختیاری) و مقداری که بهش پاس میدیم، به redis میگه که چه تعداد رکورد از داده رو میخوایم. BLOCK و مقدارش (اختیاری) مقدار زمانی رو در واحد میلی ثانیه نشن میدن که ما برای صبر کردن تعیین کردیم و اگه بیش از اون خوندن دیتا طول بکشه، تایم اوت خواهیم شد. بعد STREAMS و مقدارش که بهمون میگه از رو کدوم استریم داریم میخونیم و پارامتر آخر که اریم میگیم از کدوم دیتا به بعد که این حالت به خصوص داره میگه همه رو بخون (این فرمت یعنی timestamp و sequence number هردو صفر باشن که قاعدتا میشه از اول).
اما اگه ما بخوایم فقط دیتاهای جدید رو بخونیم چی؟ برای این کار از فرمت زیر استفاده میکنیم:

XREAD COUNT 2 BLOCK 1000 STREAMS brothers_stream $

فقط نکته ای که باید توجه کرد اینه که این کامند فقط زمانی دیتا برمیگردونه که دیتا موقع بلاک بودن این دستور وارد بشه.
حالابریم سراغ مبحث consumer group . هدف از ایجاد این گروه ها برای زمانی هست که لود بالا باشه و ما بخوایم چندتا مصرف کننده همزمان روی یه استریم کار کنن اما نه روی دیتای تکراری. برای ایجاد یه consumer group از این کامند استفاده میکنیم

127.0.0.1:6379> XGROUP CREATE brothers_stream consumer_group1 0
OK

خب با این کار ما برای استریم brothers_stream یه گروه مصرف کننده ایجاد کردیم به نام consumer_group1 و پارامتر آخر یعنی 0 داره میگه که این گروه دیتای روی استریم رو از ابتدا میتونه بخونه. چنانچه میخواستیم که گروه به دیتاهای استریم فقط بعد از ایجاد گروه دسترسی داشته باشه، باید به جای پارامتر 0 از $ استفاده کنیم. حالا وقتی که گروه ایجاد شد، کاری که باید بکنیم، اینه که یه consumer داشته باشیم که تو این گروه دیتا رو بخونه

127.0.0.1:6379> XREADGROUP GROUP consumer_group1 consumer1 COUNT 2 BLOCK 1000 STREAMS brothers_stream >
1) 1) brothers_stream
   2) 1) 1) 1719220675816-0
         2) 1) name
            2) khaled
            3) score
            4) 100
      2) 1) 1719220697600-0
         2) 1) name
            2) hamzah
            3) score
            4) 98

کلمه کلیدی برای اینکار XREADGROUP هستش که یه GROUP میگیره و یه consumer . پارامتر های بعدی رو هم که تو کامندهای قبلی دیدیم. فقط میمونه پارامتر آخر ( < ) . این پارامتر همون id رو میگیره که قبلا باهاش مواجه شدیم یعنی میتونیم به جای این مقدار یه id پاس بدیم، اما این نماد به این معنیه که به من دیتایی رو برگردون که هنوز هیچ consumer دیگه ای توی گروه دریافتش نکرده و اصطلاحا تا حالا به کسی deliver نشده.
حالا با این توضیحات دوتا تعریفی هست که باید بهش توجه کنیم. یه لیستی وجود داره به نام PEL یا Pending Entry List . این لیست شامل پیامها یا دیتاهاییه که به یه consumer تحویل شدن ولی consumer هنوز acknowledge ی مبنی بر پروسس کردنشون به استریم برنگردونده. پس با این اوصاف consumer هاهم باید بعد از پروسس کردن دیتا، یه سیگنال به استریم برگردونن ، به این معنی که کار پروسس اون دیتای دریافتی تموم شده. تو این لیست میشه حتی گزارش گیری هم انجام داد ( XPENDING ) که باهاش میشه وضعیت پیامهای معلق و زمان تعلیقشون رو دید تا در صورت نیاز درموردشون تصمیم گرفت.
اما برای اینکه consumer اعلام کنه که کار پردازش دیتا و پیام دریافتیش تموم شده، به این شکل acknowledge میده

127.0.0.1:6379> XACK brothers_stream consumer_group1 1719220675816-0
(integer) 1

با این دستور به استریم گفته میشه که مسیج 1719220675816-0 پردازش شد.
اگر یه consumer نتونه پیامی که بهش تحویل شده رو در یه بازه زمانی خاص پردازش کنه، اینطور در نظر گرفته میشه که این پروسه fail شده. همونطور که عرض کردم لیست پام های معلق رو به اینصورت دریافت میکنیم

127.0.0.1:6379> XPENDING brothers_stream consumer_group1 IDLE 20000 - + 2 consumer1
1) 1) 1719220697600-0
   2) consumer1
   3) (integer) 1680065
   4) (integer) 1
2) 1) 1719223217132-0
   2) consumer1
   3) (integer) 137401
   4) (integer) 1

با درنظر گرفتن یه محدودیت زمانی برای انجام پردازش پیامها، میتونیم توی برنامه مون ست کنیم که اگه این زمان سپی شده برای پردازش دیتا از حدی گذشت، مصرف کننده ( consumer ) دیگه ای وظیفه ی پردازش این پیام خاص رو بر عهده بگیره و این کار با دستور XCLAIM انجام میشه. به این صورت که consumer درخواست میده تا پردازش یه پیام خاص به اون سپرده بشه

XCLAIM brohters_stream consumer_group1 consumer2 300001719223217132-0

توی این دستور consumer2 داره میگه میگه من به عنوان یه مصرف کننده از consumer_group1 درخواست میکنم که اگر دیتا با id برابر 1719223217132-0 بیش از 30 ثانیه معلق بود، پیام (دیتا ) رو به من بدین که پردازشش رو برعهده بگیرم.