کش کردن یعنی داده هایی که از قبل درخواست شده اند و امکان درخواست مجدد آن وجود دارد یا داده هایی که می توان تا دسترسی به داده از سرور به کاربر نمایش داده شوند در محلی ذخیره شوند تا سرعت افزایش یابد و سیستم بهینه تر شود. به طور خلاصه caching تکنیکی برای ذخیره کردن موقت داده ها در حافظه است. به صورت کلی به دو صورت سخت افزاری و نرم افزاری انجام می شود. انواع کش کردن نرم افزاری در back-end با ذخیره اطلاعات در حافظه کش (معمولا RAM) و در front-end به صورت ذخیره اطلاعات در حافظه مرورگر امکان پذیر است، و کش کردن سخت افراری کش DNS سرور ها و Cache server ها.
کاربرد اصلی کش کردن زمانی است که برنامه درگیر محاسبات سنگینی است یا بنا به دلایل دیگر دریافت پاسخ زمان بر باشد، در این گونه مواقع تا زمان دریافت داده های جدید، داده های کش شده را در اختیار کاربر قرار می دهیم. همچنین در بعضی مواقع ریکوئست های تکراری در سیستم زیاد است یا ریکوئست هایی وجود دارد که پاسخ آن ها معمولا با تغییرات زیادی همراه نیستند، در این مواقع caching موجب می شود تا تنها برای داده هایی که نیاز است request ها به سمت سرور ارسال گردند و سایر داده ها از حافظه کش در اختیار کاربر قرار می گیرد. البته داده های کش با استفاده از روش های مختلف دائما به روز می شوند. در عملیات caching داده ها یا با موفقیت cache می شوند (اصطلاحا heat اتفاق افتاده) و یا با شکست مواجه می شوند (miss).
هدف اصلی caching پایین آوردن Response Time درخواستهای کاربران و پایین آوردن بار ترافیکی روی سیستم و دیتابیس است.
تا اینجا به بررسی caching و مزایای آن در سیستم پرداختیم ولی caching می تواند مشکلاتی هم ایجاد کند: حتی در صورتی که caching به صورت صحیح پیاده سازی شده باشد مراحل اضافه ای جهت کش کردن اطلاعات به سیستم افزوده می شود. در صورت پیاده سازی غیر صحیح این امر کاملا مشهود خواهد بود.
از دیگر مشکلاتی که در caching وجود دارد این است که ممکن است اطلاعات کش اطلاعات قدیمی باشد زیرا ممکن است اطلاعات با موفقیت کش نشده باشد یا در هنگام برگرداندن پاسخ از کش داده در دیتابیس تغییر کرده باشد.
از آنجایی که فضای cache محدود است و ما نیاز دائمی به داده های کش شده نداریم، روش هایی برای حذف داده های قدیمی تر و جایگزین کردنشان با داده های جدید و مدیریت داده ها وجود دارد:
در این روش داده های بیشتر استفاده شده به بالای حافظه منتقل شده و داده های کمتر استفاده شده حذف می شود.
این روش از رایج ترین روش ها در caching است. در این روش هرگاه داده ای درخواست شود به بالای کش منتقل می شود، به این ترتیب الگوریتم می تواند داده هایی که کمتر از آن ها استفاده شده است را با مشاهده انتهای کش شناسایی کند. این الگوریتم فرض بر این است که هرچه تعداد درخواست ها برای یک داده بیشتر باشد باید بیشتر در فضای کش باقی بماند.
در این روش یک شمارنده تعداد استفاده از داده ها را محاسبه کرده و داده هایی که کمترین استفاده از آن ها شده باشند حذف می شوند.
در این روش داده هایی که بیشتر استفاده شده باشند حذف می شوند. این روش زمانی که داده های قدیمی تر با اهمیت تر هستند کاربرد دارد.
این روش را می توان یکی از متداولترین روشهای caching دانست. در مواقعی که درخواست کش کردن بیشتر از درخواست خواندن اطلاعات است. در این روش اگر داده ها در cache موجود باشند در اختیار کاربر قرار داده می شود و در صورت که موجود نباشد داده از سرور درخواست می شود و برای کاربر ارسال می شود، سپس داده در cache ذخیره خواهد شد.
در این روش دیتابیس و کش به صورت in-line هستند و درخواستها به cache ارسال می شود. کش با داده های دیتابیس sync می شود، اگر داده در کش وجود نداشته باشد، از دیتابیس درخواست می شود و کش آپدیت می کنیم، سپس داده در اختیار کاربر قرار می گیرد. تفاوت این روش با روش قبل در این است که برنامه همیشه با cache در ارتباط است.
مانند روش قبل برنامه همیشه با cache در ارتباط است. در این حالت اطلاعات ابتدا در cache ذخیره می شوند و دیتابیس با کش sync می شود. در این حالت احتمال cache miss به حداقل می رسد.
این روش مشابه روش قبل است با این تفاوت که ذخیره اطلاعات از cache به database در لحظه صورت نمی گیرد و مثلا در بازه های زمانی خاص صورت می گیرد. در این روش اگر داده های cache از بین برود دیگر به آنها دسترسی نداریم بنابراین این روش پر ریسک محسوب می شود.
در این روش اطلاعات روی دیتابیس ذخیره می شود و در زمان درخواست، فقط اطلاعات درخواست شده روی کش قرار می گیرد.
در پایتون با استفاده از دکوراتور lru_cache@ که در پکیج functools وجود دارد می توان عملیات caching را انجام داد. پکیج functools پکیجی higher-order است، به این معنی که یا ورودی آن تابع است و یا یک تابع را به عنوان خروجی باز می گرداند.
این دکوراتور از یک دیکشنری ذخیره می کند که شامل argument ها و نتیجه تابع است. بنا براین argument ها باید قابلیت hash شدن را داشته باشند تا دکوراتور بتواند کار خود را انجام دهد.
مثال زیر را بدون در نظر گرفتن کار که انجام می دهد در نظر بگیرید:
from timeit import repeat def steps_to(stair): if stair == 1: return 1 elif stair == 2: return 2 elif stair == 3: return 4 else: return ( steps_to(stair - 3) + steps_to(stair - 2) + steps_to(stair - 1)
)
setup_code = "from __main__ import steps_to" stmt = "steps_to(30)" times = repeat(setup=setup_code, stmt=stmt, repeat=3, number=10) print(f"Minimum execution time: {min(times)}")
خروجی کد بالا به صورت زیر خواهد بود:
Minimum execution time: 29.684728438001912
حال دکوراتور lru_cache@ را به منظور کش کردن نتیجه به کد بالا اضافه می کنیم:
from functools import lru_cache from timeit import repeat def steps_to(stair): . . .
و همانطور که مشاهده می کنیم نتیجه به شکل قابل توجهی سریع تر خواهد بود
Minimum execution time: 1.2620002962648869e-06
با استفاده از print(steps_to.cache_info())
می توان جزئیات نربوط به کش کردن کد بالا را مشاهده کرد که ۸۲ بار عملیات کش شدن با موفقیت انجام شده و ۳۰ شکست هم در این عملیات مشاهده می شود:
CacheInfo(hits=82, misses=30, maxsize=128, currsize=30)
ردیس (redis) به عنوان یک message queue یا message broker شناخته می شود که داده ها را به صورت key-value در خود ذخیره می کند. از مزایای redis سرعت، سادگی در ساختار، ttl و کاربردی بودن آن است.
ردیس ابزاری قدرتمند جهت کش کردن یک سیستم به شمار می آید که با در اختیار قرار دادن data type های مختلف بنا به نیازمندی و معماری سیستم می تواند انتخاب مناسبی برای caching باشد.
انواع Data Type ها در Redis:
برای ذخیره داده به صورت list_name: valu1, value2, ... value(n) کاربرد دارد.
برای نصب ردیس در ubuntu دستور زیر را در terminal وارد می کنیم:
sudo apt install redis-server
و برای اتصال به redis در terminal دستور زیر را وارد می کنیم:
redis-server
برای اطمینان از اتصال به ردیس دستور ping را وارد می کنیم و در صورتی که PONG در خروجی برگردانده شود دسترسی به redis به درستی صورت گرفته است.
کلید ها (KEYS)
همانطور که قبلا هم اشاره شد redis پایگاه داده ای مبتنی بر کلید-مقدار (key-value) است. برای تعریف یک key و مقدار دادن به آن از دستور زیر استفاده می کنیم:
SET KEY [KEY_NAME] [VALUE]
برای مقدار دادن به یک کلید در صورتی که وجود نداشته باشد از دستور زیر استفاده می کنیم:
SET KEY [KEY_NAME] [VALUE]
برای مقدار دادن به چند کلید همزمان از دستور زیر استفاده می کنیم:
MSET KEY [KEY_NAME1] [VALUE1] [KEY_NAME2] [VALUE2] ... [KEY_NAMEn] [VALUEn]
نکته: در صورتی که یکی از کلیدها از قبل وجود داشته باشد هیچ کدام از مقادیر ثبت نخواهند شد.
برای دسترسی به همه کلید ها از دستور زیر استفاده می کنیم:
KEYS *
برای دسترسی به مقادیر یک کلید از دستور زیر استفاده می کنیم:
GET [KEY_NAME]
اگر بخواهیم به چند کلید را همزمان GET کنیم از دستور زیر استفاده می کنیم:
MGET [KEY_NAME] [KEY_NAME1] ... [KEY_NAMEn]
برای تغییر نام یک کلید از دستور زیر استفاده می کنیم:
RENAME [KEY_NAME] [NEW_KEY_NAME]
برای حذف یک کلید از دستور زیر استفاده می کنیم:
DEL [KEY_NAME]
برای بررسی وجود داشتن یک کلید در کلید از دستور زیر استفاده می کنیم:
EXISTS [KEY_NAME]
برای ایجاد موقتی یک کلید به صورت ثانیه از دستور زیر استفاده می کنیم (n ثانیه):
SET [KEY_NAME] [VALUE] EXPIRE [KEY_NAME] #n(second)
برای مشاهده میزان باقی مانده از طول عمر رشته از دستور زیر استفاده می کنیم:
TTL [KEY_NAME]
نکته: در صورت expire شدن کلید مقدار 2- برگردانده می شود.
در صورتی که بخواهیم دستور EXPIRE را خنثی کنیم از دستور زیر استفاده می کنیم:
PERSIST [KEY_NAME]
نکته: در صورتی که expire روی کلیدی اعمال نشده باشد و یا دستور persist را بعد از expire روی یک کلید اعمال کنیم مقدار برگشتی از دستور '[TTL '[KEY_NAM برابر با 1- خواهد بود.
برای ایجاد یک مقدار در هش از دستور زیر استفاده می کنیم:
HSET [HASH_NAME] [FIELD] [VALUE]
دریافت مقادیر فیلد یک hash
HGET [HASH_NAME] [FIELD_NAME]
دریافت همه مقادیر یک hash
HGETALL [HASH_NAME]
کلید های یک hash
HKEYS [HASH_NAME]
ذخیره دسته ای در یک hash
HMSET [HASH_NAME1] [FIELD1] [VALUE1] [HASH_NAME2] [FIELD2] [VALUE2] ... [HASH_NAMEn] [FIELDn] [VALUEn]
برای قرار دادن مقادیر در ابتدای لیست از LPUSH و برای قراردادن از انتهای لیست از RPUSH استفاده می کنیم
LPUSH [LIST_NAME] [VALUE1] [VALUE2] ... [VALUEN]
RPUSH [LIST_NAME] [VALUE1] [VALUE2] ... [VALUEN]
برای وارد کردن مقادیر در لیست در صورت وجود لیست از دستو زیر استفاده می کنیم:
LPUSHX [LIST_NAME] [VALUE1] [VALUE2] ... [VALUEN] RPUSHX [LIST_NAME] [VALUE1] [VALUE2] ... [VALUEN]
برای دسترسی به آخرین عنصر لیست از دستور زیر استفاده می کنیم.
LPOP [LIST_NAME]
[VALUE2] ... [VALUEN]
برای دسترسی به اولین عنصر لیست از دستور زیر استفاده می کنیم.
RPOP [LIST_NAME]
نکته: در صورتی که بخواهیم به آخرین عنصر لیست اشاره کنیم از 1- استفاده می کنیم.
برای حذف چند عنصر لیست از دستور زیر استفاده می کنیم:
LREM [LIST_NAME] counter [VALUE]