Codoloper

آموزش برنامه‌نویسی به فارسی

یاد بگیر، پیشرفت کن، بساز.

منبع آموزشی برنامه‌نویسی به زبان فارسی - مستندات، دوره‌ها و مطالب کاربردی برای همه سطوح.

داکیومنت ها:

JavascriptHTMLCSSReact JSبیشتر
1// برنامه‌نویسی به فارسی
2import { learn } from 'codoloper'
3
4const developer = learn({
5"lang": "فارسی"
6"level": "همه سطوح"
7free: true
8})
بلاگ

مطالب جدید

تازه‌ترین مطالب آموزشی و خبرهای مرتبط.

Grok حالا برات اپلیکیشن کامل میسازه؛ آشنایی با Build Mode و grok.me نوشته شده توسط عرفان دهقانی

Grok حالا برات اپلیکیشن کامل میسازه؛ آشنایی با Build Mode و grok.me

خیلی وقت‌ها یک ایده‌ی ساده توی ذهن آدم شکل میگیره — یک ماشین‌حساب مخصوص یک کار خاص، یک صفحه‌ی landing برای یک پروژه‌ی جانبی، یک داشبورد کوچیک برای دنبال کردن یک سری عدد — ولی چون کدنویسی بلد نیستی یا وقت راه‌اندازی محیط توسعه رو نداری، ایده همون‌جا میمونه. این دقیقاً همون شکافیه که SpaceXAI (همون شرکتی که قبلاً به اسم xAI میشناختیمش) با قابلیت جدید Grok به اسم Build Mode میخواد پرش کنه.

Build Mode چیه

Build Mode یک قابلیت تازه‌ست که همین چند روز پیش، ۲۸ ژوئیه ۲۰۲۶، برای مشترکین SuperGrok Heavy به‌صورت early beta فعال شد. ایده‌ش اینه که به‌جای این‌که Grok فقط یک تکه کد بهت بده که خودت باید کپی کنی و جایی اجرا کنی، مستقیم توی همون چت یک پیش‌نمایش زنده و کاملاً کاربردی از چیزی که خواستی میسازه. بعد میتونی با زبان طبیعی — نه با نوشتن کد — بگی چیدمان عوض بشه، رنگ‌ها تغییر کنه، یک ویژگی اضافه بشه، یا منطق برنامه فرق کنه.

این قابلیت روی وب (grok.com)، اپ iOS و اپ اندروید در دسترسه، یعنی محدود به یک پلتفرم خاص نیست.

چی میشه باهاش ساخت

طبق اعلام رسمی SpaceXAI، دامنه‌ی چیزهایی که Build Mode ازش پشتیبانی میکنه نسبتاً گسترده‌ست:

وب‌سایت و پورتفولیو — لندینگ‌پیج و صفحات معرفی شخصی. اپلیکیشن‌های کاربردی — ماشین‌حساب، برنامه‌ریز کار، ابزارهای رهگیری، و یوتیلیتی‌های سفارشی. بازی — از بازی‌های آرکید و پازل گرفته تا محیط‌های سه‌بعدی ساده. داشبورد زنده — نمایش داده‌ی real-time از طریق کانکتورهای سفارشی.

انتشار: لینک grok.me یا دامنه‌ی خودت

نکته‌ای که این قابلیت رو از خیلی از ابزارهای مشابه متمایز میکنه، بخش انتشاره. وقتی چیزی که ساختی آماده شد، میتونی با یک لینک به فرمت grok.me منتشرش کنی و مستقیم به هرکسی بفرستی، یا اگه دامنه‌ی اختصاصی داری، همون پروژه رو به دامنه‌ی خودت وصل کنی. یعنی مسیر از «ایده‌ی توی ذهن» تا «چیزی که یک آدرس واقعی داره و قابل اشتراک‌گذاریه» بدون نیاز به هاستینگ جداگانه یا تنظیمات دیپلوی طی میشه.

رقابتی که Grok تازه واردش شده

این حرکت SpaceXAI رو مستقیم وارد بازاری میکنه که این چند وقته اسمش شده «vibe coding» — رویکردی که در اون به‌جای نوشتن کد، با پرامپت و زبان طبیعی نرم‌افزار میسازی. رقبای این میدون کم نیستن: Google AI Studio که امکان تبدیل پرامپت به اپلیکیشن‌های native اندروید رو داره، Claude Artifacts خود آنتروپیک، و ChatGPT Codex از OpenAI. تفاوتی که Grok روش تأکید کرده اینه که میخواد این تجربه رو تا حد امکان غیرتخصصی نگه داره — بدون نصب محلی، بدون تنظیمات پیچیده، صرفاً پرامپت، پیش‌نمایش، و ویرایش.

نکته‌ای که نباید نادیده گرفت

با همه‌ی این هیجان، این ابزار فعلاً پشت یک پولیوار جدیه: فقط مشترکین SuperGrok Heavy بهش دسترسی دارن، که رده‌ی بالای اشتراک Grok محسوب میشه. یعنی برخلاف تصوری که ممکنه از عبارت «بدون کدنویسی» به ذهن برسه، هنوز یک مانع (البته این‌بار مالی، نه فنی) سر راهه. همچنین چون این قابلیت هنوز در فاز early beta است، طبیعیه که محدودیت‌ها و باگ‌های اولیه‌ای هم داشته باشه که با گذر زمان رفع میشن.

جمع‌بندی

Build Mode نشون میده مسیر ابزارهای هوش مصنوعی داره از «چت‌کردن درباره‌ی کد» به‌سمت «تولید مستقیم محصول قابل‌استفاده» حرکت میکنه. برای کسی که ایده داره ولی مهارت فنی نداره، این یعنی فاصله‌ی بین فکر و محصول واقعی کوتاه‌تر شده. ولی برای کسی که دنبال یک ابزار توسعه‌ی جدی و قابل اتکاست، هنوز باید صبر کرد ببینه این نسخه‌ی beta، وقتی از پشت پولیوار بیرون بیاد و در دسترس عموم قرار بگیره، چقدر توی سناریوهای واقعی و پیچیده‌تر جواب میده.

شاردینگ دیتابیس: وقتی یک سرور دیگه جایی برای داده نداره نوشته شده توسط عرفان دهقانی

شاردینگ دیتابیس: وقتی یک سرور دیگه جایی برای داده نداره

ایندکس و کش و replica خواندنی، همه تا یک جایی جواب میدن. اگه حجم داده‌ات آنقدر بزرگ بشه که حتی قوی‌ترین سرور دیتابیس هم دیگه جا نداشته باشه نگهش داره، یا نرخ نوشتن آنقدر بالا بره که یک سرور تنها نتونه جوابگو باشه، اون‌وقت با یک مسئله‌ی متفاوت روبه‌رو میشی. ایندکس کمکت میکنه سریع‌تر پیدا کنی، replica کمکت میکنه بار خواندن رو پخش کنی، ولی هیچ‌کدوم مشکل «داده آنقدر زیاده که روی یک ماشین جا نمیشه» رو حل نمیکنن. شاردینگ دقیقاً برای همین مسئله ساخته شده.

شاردینگ یعنی چی

شاردینگ یعنی داده‌ی یک جدول بزرگ رو به چند بخش (shard) تقسیم کنی و هر بخش رو روی یک سرور جدا نگه داری. برخلاف replication که هر سرور یک کپی کامل از کل داده رو داره، توی شاردینگ هر سرور فقط بخشی از داده رو داره. مثلاً اگه یک جدول کاربر با ۱۰۰ میلیون ردیف داری، میتونی کاربرهای با ID زوج رو روی یک سرور بذاری و کاربرهای با ID فرد رو روی سرور دیگه. حالا هر سرور فقط نصف داده رو مدیریت میکنه، و هم فضا و هم بار پردازشی بین دو ماشین تقسیم شده.

نکته‌ی مهم اینه که شاردینگ و replication دو مسئله‌ی متفاوت رو حل میکنن و معمولاً با هم ترکیب میشن: هر شارد خودش میتونه چند replica داشته باشه برای availability، و همزمان داده هم بین شاردهای مختلف تقسیم شده برای مقیاس‌پذیری.

چطور تصمیم بگیریم کدوم رکورد بره کدوم شارد

قلب هر سیستم شاردینگ، یک چیز به اسم shard key یا partition key است — ستونی که تعیین میکنه هر ردیف روی کدوم شارد قرار بگیره. چند استراتژی رایج برای این تصمیم وجود داره.

اولین و ساده‌ترین، range-based sharding است: بازه‌های مقدار رو بین شاردها تقسیم میکنی، مثلاً کاربرهایی با ID بین ۱ تا ۱ میلیون روی شارد اول، بین ۱ تا ۲ میلیون روی شارد دوم. مزیتش اینه که query هایی که روی یک بازه کار میکنن (مثل «همه‌ی سفارش‌های هفته‌ی گذشته») ممکنه فقط به یک شارد نیاز داشته باشن. مشکلش اینه که اگه داده به‌طور نامتوازن رشد کنه (مثلاً کاربرهای جدید همیشه ID بزرگ‌تر میگیرن)، شارد آخر همیشه شلوغ‌تر از بقیه میشه — به این پدیده hot shard میگن.

دومی hash-based sharding است: روی مقدار shard key یک تابع هش اجرا میکنی و نتیجه تعیین میکنه کدوم شارد. این روش توزیع خیلی یکنواخت‌تری میده و مشکل hot shard رو تا حد زیادی حل میکنه، ولی query هایی که روی بازه کار میکنن رو سخت‌تر میکنه، چون داده‌ی مرتبط ممکنه پخش بشه روی همه‌ی شاردها.

سومی directory-based sharding است، جایی که یک جدول جدا (یا سرویس جدا) نگه میداری که میگه هر کلید دقیقاً کدوم شارده. انعطاف بیشتری میده — میتونی هر موقع خواستی داده رو جابه‌جا کنی بدون این‌که فرمول هش عوض بشه — ولی خودش یک نقطه‌ی اضافه‌ست که باید مدیریتش کنی و مراقب باشی bottleneck نشه.

چیزی که واقعاً سخته: query هایی که چند شارد رو لمس میکنن

توی یک دیتابیس تک‌سروری، یک JOIN بین دو جدول یا یک عملیات تراکنشی روی چند ردیف، کار همیشگی و بی‌دردسریه. توی یک سیستم شارد شده، این همون کاریه که واقعاً سخت میشه. اگه دو ردیفی که میخوای join کنی روی دو شارد مختلف باشن، دیگه نمیتونی از یک JOIN ساده‌ی SQL استفاده کنی؛ باید یا داده رو توی اپلیکیشن خودت از دو شارد بخونی و دستی join کنی، یا از یک لایه‌ی query routing استفاده کنی که این کار رو برات مخفی کنه.

همین مسئله برای تراکنش‌ها هم صدق میکنه. یک تراکنش که باید همزمان روی دو ردیف از دو شارد مختلف atomic عمل کنه (یا هر دو موفق بشن یا هیچ‌کدوم)، به یک پروتکل به اسم two-phase commit یا الگوهای پیچیده‌تری مثل Saga نیاز داره. این دقیقاً همون چیزیه که شاردینگ رو از یک تصمیم ساده‌ی زیرساختی به یک تصمیم معماری تبدیل میکنه؛ وقتی شارد میکنی، عملاً داری از دنیای تراکنش‌های ساده‌ی ACID فاصله میگیری، مگر این‌که خیلی هوشمندانه shard key رو طوری انتخاب کنی که داده‌های مرتبط همیشه کنار هم بمونن.

re-sharding: کابوس واقعی

فرض کن با hash-based sharding شروع کردی و چهار شارد داری. حالا نیاز داری شاردهای بیشتری اضافه کنی. مشکل اینجاست که اگه فرمول هش‌ت چیزی ساده مثل hash(key) % 4 باشه، وقتی این عدد رو به ۵ تغییر بدی، تقریباً تمام mapping ها به‌هم میریزه — چون باقیمانده‌ی تقسیم برای اکثر کلیدها عوض میشه. یعنی باید تقریباً کل داده رو جابه‌جا کنی، درست همون چیزی که موقع طراحی سیستم میخواستی ازش فرار کنی.

راه‌حل رایج برای این مشکل consistent hashing است — همون تکنیکی که توی مقاله‌ی لود بالانسینگ درباره‌ی Maglev گوگل ازش گفتیم. با consistent hashing، اضافه‌کردن یک شارد جدید فقط باعث میشه بخش کوچیکی از داده جابه‌جا بشه، نه همه‌چیز. برای همین سیستم‌های بزرگ‌مقیاسی مثل Cassandra و DynamoDB این تکنیک رو به‌عنوان پایه‌ی مکانیزم شاردینگشون انتخاب کردن.

مثال‌های واقعی

Vitess، که اصلش رو یوتیوب برای مقیاس‌دادن MySQL ساخت (و بعداً Slack و GitHub هم ازش استفاده کردن)، دقیقاً یک لایه‌ی میانی روی MySQL میذاره که شاردینگ رو تا حد زیادی از دید اپلیکیشن مخفی میکنه. Citus هم همین کار رو برای PostgreSQL انجام میده — جدول‌ها رو به‌صورت شفاف بین چند نود پخش میکنه و کوئری‌ها رو به‌صورت خودکار روت میکنه. MongoDB هم شاردینگ رو به‌عنوان یک قابلیت داخلی و نه یک ابزار جانبی ارائه میده، که همین یکی از دلایلیه که خیلی از تیم‌ها برای داده‌ی خیلی بزرگ سراغش میرن.

جمع‌بندی

شاردینگ آخرین ابزاریه که باید بهش فکر کنی، نه اولین. قبل از رسیدن به این مرحله معمولاً ایندکس‌گذاری بهتر، کش، و read replica ها میتونن مدت زیادی جوابگو باشن. شاردینگ رو وقتی به سراغش میری که واقعاً داده یا نرخ نوشتن از ظرفیت یک ماشین رد شده، چون همراهش پیچیدگی‌های واقعی میاد: query های چندشارده سخت میشن، تراکنش‌ها پیچیده میشن، و re-sharding میتونه یک پروژه‌ی چندهفته‌ای بشه اگه از اول درست طراحی نشده باشه.

ایندکس دیتابیس: چرا یک کوئری ساده گاهی چند ثانیه طول میکشه نوشته شده توسط عرفان دهقانی

ایندکس دیتابیس: چرا یک کوئری ساده گاهی چند ثانیه طول میکشه

فرض کن یک جدول داری با یک میلیون ردیف، و میخوای کاربری با یک ایمیل مشخص رو پیدا کنی. بدون هیچ کمکی، دیتابیس مجبوره ردیف به ردیف کل جدول رو بگرده تا برسه به همونی که دنبالشی — به این کار میگن full table scan. روی یک میلیون ردیف، این یعنی دیتابیس تا آخرین لحظه هم نمیدونه جواب رو پیدا کرده یا نه، و باید همه‌چیز رو ببینه. حالا اگه همین جدول یک ایندکس روی ستون ایمیل داشته باشه، دیتابیس دیگه لازم نیست همه‌جا رو بگرده؛ مستقیم میره سراغ جایی که احتمال جواب هست. تفاوتش میتونه از چند ثانیه به چند میلی‌ثانیه برسه.

ایندکس چیه واقعاً

ساده‌ترین راه برای فهمیدن ایندکس، مقایسه‌اش با فهرست آخر یک کتابه. اگه بخوای بفهمی کلمه‌ی «recursion» توی کدوم صفحه‌ی کتاب اومده، دو راه داری: یا کتاب رو صفحه به صفحه بخونی تا پیداش کنی (همون full scan)، یا بری سراغ فهرست آخر کتاب که مرتب‌شده‌ست و سریع بگی صفحه‌ی ۲۴۳. ایندکس دیتابیس دقیقاً همین نقشو بازی میکنه: یک ساختار داده‌ی جدا و مرتب‌شده که به ازای هر مقدار توی یک ستون، میگه دقیقاً کجا (کدوم ردیف فیزیکی) باید بری دنبالش.

اکثر دیتابیس‌های رابطه‌ای مثل PostgreSQL و MySQL از یک ساختار داده به اسم B-tree برای ایندکس‌ها استفاده میکنن. B-tree یک درخت متوازنه که جستجو، درج، و حذف توش با پیچیدگی زمانی لگاریتمی انجام میشه — یعنی حتی روی یک جدول با میلیون‌ها ردیف، پیدا کردن یک مقدار فقط چند مرحله طول میکشه، نه میلیون‌ها مقایسه.

چرا رایگان نیست

اگه ایندکس این‌قدر خوبه، چرا همه‌ی ستون‌ها رو ایندکس نمیکنیم؟ چون هزینه داره، و این هزینه دو جا خودش رو نشون میده.

اول، فضا. هر ایندکس یک کپی جداگانه از داده‌ی اون ستون (به‌شکل ساختار درختی) نگه میداره. روی جدول‌های بزرگ، این میتونه چندین گیگابایت فضای اضافی مصرف کنه.

دوم و مهم‌تر، سرعت نوشتن. هر بار که یک ردیف جدید insert میکنی یا یک ردیف موجود رو update میکنی، دیتابیس باید همزمان با آپدیت کردن جدول اصلی، تمام ایندکس‌های مرتبط با اون ستون‌ها رو هم آپدیت کنه. اگه یک جدول پنج تا ایندکس داشته باشه، هر insert عملاً شش تا عملیات نوشتن رو تریگر میکنه، نه یکی. برای همین توصیه‌ی رایج اینه که فقط روی ستون‌هایی ایندکس بذاری که واقعاً توی WHERE، JOIN، یا ORDER BY زیاد ازشون استفاده میکنی — نه هر ستونی که «شاید یک روز» لازم بشه.

clustered در مقابل non-clustered

یک تمایز مهم که خیلی وقت‌ها گیج‌کننده‌ست، فرق ایندکس clustered و non-clustered است. توی ایندکس clustered، ترتیب فیزیکی داده روی دیسک دقیقاً همون ترتیب ایندکسه — یعنی خود جدول، به شکل ایندکس مرتب شده. هر جدول فقط میتونه یک ایندکس clustered داشته باشه، چون داده فقط یک ترتیب فیزیکی روی دیسک میتونه داشته باشه. توی MySQL با InnoDB، این معمولاً همون primary key است.

ایندکس non-clustered یک ساختار جداست که فقط اشاره‌گر به ردیف اصلی نگه میداره، نه خود داده رو. میتونی تعداد دلخواهی از این نوع ایندکس روی یک جدول داشته باشی. تفاوتش توی عمل اینه که خوندن از ایندکس clustered یک مرحله کمتر داره (چون داده همونجاست)، ولی non-clustered برای ستون‌های غیر primary key انعطاف بیشتری میده.

ایندکس ترکیبی و ترتیب ستون‌ها

وقتی کوئری‌هات معمولاً چند شرط با هم دارن — مثلاً WHERE user_id = ? AND status = 'active' — میتونی یک composite index روی هر دو ستون بسازی. نکته‌ی مهمی که خیلی از دولوپرها نادیده میگیرن اینه که ترتیب ستون‌ها توی ایندکس ترکیبی مهمه. یک ایندکس روی (user_id, status) برای کوئری‌هایی که با user_id فیلتر میکنن (با یا بدون status) مفیده، ولی برای کوئری‌هایی که فقط روی status فیلتر میکنن و اصلاً کاری با user_id ندارن، عملاً بی‌فایده‌ست — چون درست مثل فهرست کتابیه که بر اساس فصل مرتب شده، نه بر اساس موضوع؛ اگه فصل رو ندونی، فهرست کمکت نمیکنه.

CREATE INDEX idx_user_status ON orders (user_id, status);

-- این کوئری از ایندکس بالا خوب استفاده میکنه
SELECT * FROM orders WHERE user_id = 42 AND status = 'active';

-- این یکی عملاً از این ایندکس استفاده نمیکنه
SELECT * FROM orders WHERE status = 'active';

چطور بفهمیم دیتابیس واقعاً از ایندکس استفاده میکنه

خیلی وقت‌ها فرض میکنیم چون ایندکس ساختیم، کوئری هم ازش استفاده میکنه، درحالی‌که همیشه این‌طور نیست. ابزار اصلی برای فهمیدن این موضوع دستور EXPLAIN (یا EXPLAIN ANALYZE توی PostgreSQL) است که نشون میده دیتابیس دقیقاً چه plan ای برای اجرای کوئری انتخاب کرده — آیا از ایندکس استفاده کرده یا رفته سراغ full table scan.

EXPLAIN ANALYZE
SELECT * FROM orders WHERE user_id = 42;

اگه توی خروجی این دستور عبارتی مثل Seq Scan ببینی به‌جای Index Scan، یعنی دیتابیس تصمیم گرفته ایندکس رو نادیده بگیره — که میتونه چند دلیل داشته باشه: شاید جدول اونقدر کوچیکه که دیتابیس تشخیص داده full scan سریع‌تره، شاید آمار (statistics) دیتابیس قدیمی شده، یا شاید شرط کوئری طوری نوشته شده که ایندکس اصلاً قابل استفاده نیست (مثلاً استفاده از تابع روی ستون ایندکس‌شده، مثل WHERE LOWER(email) = ...).

وقتی ایندکس ضرر میزنه

جالبه که بدونی ایندکس همیشه سریع‌تر نیست. روی جدول‌های خیلی کوچیک، یا وقتی یک شرط WHERE درصد بزرگی از جدول رو برمیگردونه (مثلاً بیش از ۲۰-۳۰ درصد ردیف‌ها)، دیتابیس معمولاً ترجیح میده مستقیم برگرده به full scan، چون رفتن سراغ ایندکس و بعد پرش بین ردیف‌های پراکنده روی دیسک، از یک خوندن پیوسته و ساده کندتره. این یکی از نکاتی‌یه که خیلی از مهندس‌های تازه‌کار تعجب میکنن وقتی برای اولین بار میبینن دیتابیس با وجود ایندکس، ازش استفاده نکرده — درحالی‌که این تصمیم دیتابیس اتفاقاً درسته، نه باگ.

جمع‌بندی

ایندکس یک معامله‌ست، نه یک ترفند رایگان: سرعت خوندن رو در ازای سرعت نوشتن و فضای اضافی میخری. تصمیم درست این نیست که «همه‌چیز رو ایندکس کن»، بلکه اینه که بفهمی کدوم کوئری‌ها واقعاً توی سیستم پرتکرارن، دقیقاً چه ستون‌هایی رو فیلتر یا مرتب میکنن، و بر همون اساس ایندکس بسازی — و بعد با EXPLAIN چک کنی که فرضت درست بوده.

Cache: کش چیه؟ چرا کش کنیم؟ یک لایه با سرعت بسیار بیشتر نوشته شده توسط عرفان دهقانی

Cache: کش چیه؟ چرا کش کنیم؟ یک لایه با سرعت بسیار بیشتر

فرض کن یک صفحه داری که برای ساختنش باید بری سراغ دیتابیس، چند تا جدول رو join کنی، نتیجه رو پردازش کنی، و بعد HTML بسازی. این کار شاید ۲۰۰ میلی‌ثانیه طول بکشه. حالا اگه این صفحه رو هزار نفر در ثانیه ببینن، و محتواش هر بار عوض نشه، داری همون کار سنگین رو هزار بار در ثانیه تکرار میکنی، درحالی‌که جواب همیشه یکیه. کش دقیقاً همین اتلاف رو حل میکنه: جواب رو یک بار حساب میکنی، جایی نگهش میداری که دسترسی بهش خیلی سریع‌تره، و دفعه‌های بعد به‌جای محاسبه‌ی دوباره، همون جواب آماده رو برمیگردونی.

این ایده به‌قدری پایه‌ایه که توی تقریباً هر لایه‌ای از یک سیستم، از مرورگر گرفته تا CDN تا سرور تا دیتابیس، یک نسخه‌ی متفاوت از همین ترفند رو میبینی.

کش توی مرورگر و سمت کاربر

اولین لایه‌ای که هر کاربر باهاش برخورد میکنه، کش مرورگره. وقتی یک فایل CSS یا تصویر یا فونت رو دانلود میکنی، مرورگر بر اساس هدرهای HTTP مثل Cache-Control و ETag تصمیم میگیره که آیا لازمه دوباره از سرور بگیرتش یا همون نسخه‌ی محلی رو نشون بده. برای همین دفعه‌ی دوم که یک سایت رو باز میکنی، خیلی سریع‌تر لود میشه — چون مرورگر اصلاً درخواست جدیدی نمیفرسته، یا اگه بفرسته، سرور فقط میگه «چیزی عوض نشده» (کد وضعیت ۳۰۴) و دوباره کل فایل رو نمیفرسته.

کش توی CDN

لایه‌ی بعدی که خیلی وقت‌ها دیده نمیشه، CDN است — سرویس‌هایی مثل Cloudflare یا ArvanCloud که یک نسخه از فایل‌های استاتیک سایتت (تصویر، CSS، JS، و حتی گاهی HTML) رو روی سرورهای پخش‌شده در نقاط مختلف جغرافیایی نگه میدارن. وقتی کاربری از یک شهر دیگه به سایتت وصل میشه، به‌جای این‌که درخواستش تا سرور اصلی بره، نزدیک‌ترین edge server جواب رو از کشش برمیگردونه. این هم سرعت رو بالا میبره و هم فشار روی سرور اصلی رو کم میکنه.

کش سمت سرور (Application Cache)

توی خود اپلیکیشن، معمولاً از ابزارهایی مثل Redis یا Memcached استفاده میشه تا نتیجه‌ی محاسبات سنگین یا کوئری‌های تکراری رو نگه دارن. مثلاً اگه یک API داری که لیست محصولات پرفروش رو برمیگردونه، به‌جای این‌که هر بار این لیست رو از دیتابیس محاسبه کنی، یک بار حسابش میکنی و توی Redis با یک TTL (زمان انقضا) ذخیره‌اش میکنی:

async function getTopProducts() {
  const cached = await redis.get('top_products');
  if (cached) return JSON.parse(cached);

  const products = await db.query('SELECT ... ORDER BY sales DESC LIMIT 10');
  await redis.set('top_products', JSON.stringify(products), 'EX', 300); // 5 دقیقه
  return products;
}

این الگو رو cache-aside میگن: اول کش رو چک میکنی، اگه خالی بود از منبع اصلی میگیری و توی کش میذاری. رایج‌ترین الگوی کش توی دنیای بک‌اند همینه، چون منطق ساده‌ست و کنترل کامل دستته.

کش توی دیتابیس

خود دیتابیس‌ها هم لایه‌های کش داخلی دارن که معمولاً کمتر بهشون فکر میکنیم. مثلاً PostgreSQL یک بخش به اسم shared buffers داره که صفحات پرکاربرد دیتابیس رو توی رم نگه میداره تا لازم نباشه هر بار از دیسک بخونه. MySQL هم مکانیزم‌های مشابهی داره. این یعنی حتی وقتی فکر میکنی داری «مستقیم از دیتابیس» میخونی، احتمالاً بخشی از اون داده از قبل توی حافظه‌ی خود دیتابیس کش شده و دیسک اصلاً درگیر نشده.

کش توی اپلیکیشن‌های موبایل

توی اپ‌های موبایل هم همین منطق تکرار میشه، فقط با اسم‌های دیگه. مثلاً یک اپلیکیشن ممکنه لیست محصولات یا فید خبری رو یک بار از سرور بگیره و توی حافظه‌ی محلی گوشی (یا یک دیتابیس سبک مثل SQLite) نگه داره، تا کاربر بدون اینترنت هم بتونه محتوای قبلی رو ببینه، و تا وقتی داده‌ی جدیدی نیومده، دوباره از سرور درخواست نده. این هم برای تجربه‌ی کاربری بهتره (لود سریع‌تر) و هم برای مصرف داده و باتری صرفه‌جویی میکنه.

چیزی که واقعاً برای یک مهندس نرم‌افزار جذابه: invalidation

جمله‌ی معروفی توی دنیای برنامه‌نویسی هست که میگه فقط دو چیز سخت توی علوم کامپیوتر وجود داره: invalidate کردن کش، نام‌گذاری متغیرها، و off-by-one errors. این جمله شوخیه، ولی نکته‌ی جدی پشتش اینه: نگه‌داشتن داده توی کش کار سختی نیست؛ سخت اینه که بفهمی دقیقاً کِی اون داده دیگه معتبر نیست و باید پاکش کنی یا آپدیتش کنی.

فرض کن قیمت یک محصول توی دیتابیس عوض بشه، ولی نسخه‌ی کش‌شده‌اش هنوز قیمت قدیمی رو نشون بده. اگه TTL طولانی گذاشته باشی، این عدم‌تطابق میتونه دقیقه‌ها یا حتی ساعت‌ها طول بکشه. راه‌حل‌های رایج برای این مشکل چندتاست: یکی این‌که هر وقت داده توی دیتابیس آپدیت شد، مستقیماً همون کلید رو از کش پاک کنی (invalidate on write). یکی دیگه اینه که از TTL کوتاه‌تر استفاده کنی تا حداکثر «قدیمی‌بودن» داده محدود باشه. روش سوم که پیچیده‌تره ولی قوی‌تره، استفاده از event-based invalidation است: وقتی یک تغییر توی سیستم اتفاق میفته، یک event منتشر میشه و هر سرویسی که کش مرتبط داره، خودش رو آپدیت میکنه.

یک مشکل کمتر شناخته‌شده: cache stampede

این یکی از اون چیزهاییه که وقتی برای اولین بار باهاش برخورد میکنی، واقعاً غافلگیرت میکنه. فرض کن یک کلید کش با TTL پنج دقیقه‌ای منقضی بشه، و دقیقاً همون لحظه هزار درخواست هم‌زمان برسه. چون کش خالیه، هر هزار درخواست میرن سراغ منبع اصلی (مثلاً دیتابیس) تا همون مقدار رو دوباره محاسبه کنن — درحالی‌که فقط یکی از این محاسبات کافی بود. این پدیده رو cache stampede یا thundering herd میگن، و میتونه یک دیتابیس سالم رو در عرض چند ثانیه از پا دربیاره.

راه‌حل رایج برای این مشکل یک قفل موقته: اولین درخواستی که میبینه کش خالیه، یک lock میگیره و شروع میکنه به محاسبه؛ بقیه‌ی درخواست‌ها یا منتظر میمونن تا اون قفل آزاد بشه، یا موقتاً همون مقدار قدیمی (حتی اگه منقضی شده) رو برمیگردونن تا محاسبه‌ی جدید تموم بشه. به این تکنیک دوم گاهی stale-while-revalidate میگن، و توی خیلی از CDN ها و کتابخانه‌های کش هم پیاده‌سازی شده.

سیاست‌های حذف: چه چیزی رو نگه داریم، چه چیزی رو دور بریزیم

چون حافظه‌ای که کش توش نگه‌داری میشه (چه RAM چه هر چیز دیگه) محدوده، وقتی کش پر میشه باید یک چیزی رو حذف کنی تا جا برای داده‌ی جدید باز بشه. رایج‌ترین سیاست LRU (Least Recently Used) است: چیزی که مدت طولانی‌تری استفاده نشده، اول حذف میشه. یک سیاست دیگه LFU (Least Frequently Used) است که به‌جای «آخرین بار کِی استفاده شده» به «چند بار استفاده شده» نگاه میکنه. انتخاب بین این دو به رفتار واقعی ترافیکت بستگی داره — مثلاً برای یک فید خبری که محتوای پرطرفدار مدام عوض میشه، LRU معمولاً منطقی‌تره؛ برای داده‌هایی که یک زیرمجموعه‌ی ثابت همیشه پرتکرارن، LFU میتونه بهتر عمل کنه.

جمع‌بندی

نکته‌ی جالب کش اینه که هیچ‌وقت یک تصمیم یک‌باره نیست. هر لایه‌ای از سیستم —از مرورگر تا CDN تا اپلیکیشن تا دیتابیس— نسخه‌ی خودش رو از همین ایده‌ی ساده پیاده میکنه: یک بار محاسبه کن، سریع نگه دار، و مراقب باش که کِی این نسخه‌ی سریع دیگه با واقعیت هم‌خونی نداره. بخش سخت کش هیچ‌وقت «نگه‌داشتن» نبوده؛ همیشه «دونستن کِی باید فراموشش کنی» بوده.