Redis — מטמון ותבנית Cache-Aside
מה זה?
שאילתה שחוזרת על עצמה מאות פעמים בדקה (למשל “הבא לי את פרטי המוצר הפופולרי ביותר”) פוגעת ב-DB פעם אחר פעם עם אותה תוצאה בדיוק. Redis פותר את זה: מסד נתונים בזיכרון (RAM), מהיר בסדרי גודל מ-SQL/MongoDB (שעל דיסק), שמתפקד כמטמון (Cache) — שומרים תוצאות שכבר חושבו, ובפעם הבאה שמבקשים אותו דבר, קוראים מהזיכרון במקום לחשב/לשלוף מהדיסק שוב.
איך מכניסים את זה לפרויקט שלכם — צעד אחר צעד
לפני שמשתמשים ב-Redis בקוד (כמו בדוגמאות בהמשך הדף), צריך להכניס אותו לפרויקט בפועל. ארבעה שלבים, בסדר הזה:
1. מריצים Redis מקומית — הכי מהיר עם Docker (אם מותקן):
docker run -d -p 6379:6379 --name redis redisבלי Docker על Windows — אפשרות חלופית היא Memurai (Redis תואם ל-Windows) או הרצה דרך WSL.
2. מתקינים את החבילה בפרויקט ה-.NET:
dotnet add package StackExchange.Redis3. רושמים את החיבור פעם אחת, ב-Program.cs — לא בכל קונטרולר מחדש:
// Program.cs — register once, reused everywhere via DI
builder.Services.AddSingleton<IConnectionMultiplexer>(
ConnectionMultiplexer.Connect("localhost:6379"));AddSingleton חשוב כאן — ConnectionMultiplexer מיועד להיווצר פעם אחת ולהישאר פתוח לכל חיי האפליקציה, לא להיפתח מחדש בכל בקשה.
4. מזריקים (Dependency Injection) את החיבור לכל שירות שצריך אותו:
public class ProductService
{
private readonly IDatabase _cache;
public ProductService(IConnectionMultiplexer redis)
{
_cache = redis.GetDatabase(); // this is the `db` used below
}
}מכאן, _cache הוא בדיוק אותו db שמופיע בכל שאר הדף — לא צריך ליצור חיבור חדש בכל מקום, רק להזריק את IConnectionMultiplexer ולקרוא GetDatabase().
מילות מפתח שחשוב לזכור
• Cache — אחסון זמני של תוצאות שכבר חושבו, לגישה מהירה יותר בפעם הבאה
• Redis Data Types — String (ערך פשוט), Hash (כמו Dictionary), List (רשימה מסודרת), Set (קבוצה ללא כפילויות)
• TTL (Time To Live) — כמה זמן ערך נשאר במטמון לפני שפג ומוסר אוטומטית — דומה ל-TTL Index ב-MongoDB
• Cache-Aside Pattern — תבנית: קודם בודקים במטמון; אם לא שם (Cache Miss), שולפים מה-DB ואז שומרים במטמון לפעם הבאה
// standalone version (script/console) — in a real app, get `db` via DI as shown above
var redis = ConnectionMultiplexer.Connect("localhost:6379");
var db = redis.GetDatabase();
// String — a simple value with TTL
await db.StringSetAsync("product:5:name", "Laptop", TimeSpan.FromMinutes(10));
string name = await db.StringGetAsync("product:5:name");
// Hash — like a Dictionary, multiple fields under one key
await db.HashSetAsync("product:5", new[]
{
new HashEntry("Name", "Laptop"),
new HashEntry("Price", "4500")
});// Cache-Aside Pattern — the core of how Redis is used in practice
async Task<Product> GetProductAsync(int id)
{
string cacheKey = $"product:{id}";
string cached = await db.StringGetAsync(cacheKey);
if (cached != null)
{
return JsonSerializer.Deserialize<Product>(cached); // Cache Hit — fast!
}
// Cache Miss — fetch from the real DB (EF Core, from the previous unit)
Product product = await context.Products.FindAsync(id);
await db.StringSetAsync(cacheKey, JsonSerializer.Serialize(product), TimeSpan.FromMinutes(10));
return product; // next time — Cache Hit
}הסבר עיקרי
למה Redis מהיר כל כך — הוא שומר הכל בזיכרון RAM, לא על דיסק. גישה ל-RAM מהירה בסדרי גודל מגישה לדיסק (גם SSD מהיר) — זו הסיבה ש-Redis משמש כמעט תמיד כ"שכבה" מעל DB איטי יותר, לא כתחליף מלא לו. הנתונים ב-Redis זמניים (במיוחד עם TTL) — אובדן הזיכרון לא אמור “לאבד” נתונים אמיתיים, כי הם עדיין קיימים ב-DB המקורי.
Cache-Aside הוא דפוס, לא רק “פונקציה” — קודם בודקים Redis (StringGetAsync); אם ריק (Cache Miss) — שולפים מה-DB האמיתי, ורק אז כותבים לקאש לפעם הבאה. הפעם הראשונה תמיד “איטית” (Cache Miss), כל הפעמים הבאות (עד שה-TTL פג) מהירות (Cache Hit). זה בדיוק אותו רעיון כמו ??= ב-Singleton מיחידת Design Patterns — “אם אין, צור/שלוף; אם יש, החזר את מה שכבר יש”.
TTL מונע נתונים מיושנים לצמיתות — בלי TTL, קאש היה יכול להישאר “נכון” רק ברגע שנוצר, ולהזדקן בלי שאף אחד ידע. TimeSpan.FromMinutes(10) אומר “אחרי 10 דקות, מחק אוטומטית” — הפעם הבאה תהיה Cache Miss מחדש, ותשלוף נתונים טריים מה-DB. בחירת TTL היא Trade-off: TTL קצר = נתונים עדכניים יותר אך פחות יעילות קאש; TTL ארוך = יעילות רבה יותר אך סיכון לנתונים מיושנים.
יתרונות
Redis מאיץ משמעותית שאילתות חוזרות, מפחית עומס על ה-DB הראשי; Data Types מגוונים (String/Hash/List/Set) מתאימים לתרחישים שונים בלי לבנות מבנה מותאם אישית; Cache-Aside פשוט ליישום מעל כל DB קיים, בלי לשנות אותו.
חסרונות
קאש “מיושן” (stale) אפשרי בין עדכון ה-DB לפקיעת ה-TTL — trade-off מודע, לא באג; Redis דורש ניהול נוסף (שרת נפרד, זיכרון מוקצה) מעבר ל-DB הראשי.
נקודות חשובות
• Redis שומר בזיכרון RAM — מהיר בהרבה מ-DB על דיסק, אך זמני (עם TTL)
• Cache-Aside: בדוק קאש → אם ריק, שלוף מ-DB ושמור בקאש → אם קיים, החזר ישירות
• TTL קובע כמה זמן ערך נשאר תקף לפני שפג אוטומטית
• Redis Data Types: String/Hash/List/Set — לא רק “מפתח-ערך” פשוט
טעויות נפוצות
• לשכוח TTL ולתת לקאש להישאר “נכון לנצח” גם כשה-DB המקורי כבר השתנה
• לעדכן את ה-DB בלי לעדכן/למחוק את הערך המתאים בקאש (Cache Invalidation) — נתונים סותרים
• להשתמש ב-Redis כמקור אמת יחיד במקום כמטמון מעל DB אמיתי — מסוכן, כי הנתונים בזיכרון יכולים “להיעלם”
סיכום
Redis הוא מסד נתונים בזיכרון, מהיר בהרבה מ-DB על דיסק, שמשמש בעיקר כמטמון (Cache) מעל DB איטי יותר. תבנית Cache-Aside — בדיקת קאש קודם, שליפה מה-DB רק ב-Cache Miss, ואז שמירה בקאש לפעם הבאה — היא הדפוס המרכזי לשימוש בו. TTL מבטיח שנתונים לא נשארים מיושנים לצמיתות. Redis Data Types (String/Hash/List/Set) מתאימים למגוון תרחישים מעבר למפתח-ערך פשוט.
דוקומנטציה רשמית
Microsoft Learn — Cache-Aside pattern
תרגילים
תרגיל 1 — String עם TTL
המשימה: שמרו ערך ב-Redis עם TTL של 10 שניות, קראו אותו מיד (אמור להופיע), והמתינו 11 שניות ונסו שוב (אמור להיעלם).
בדיקה: הקריאה הראשונה מחזירה את הערך; הקריאה השנייה (אחרי הפקיעה) מחזירה ריק/null.
תרגיל 2 — Cache-Aside ידני
המשימה: ממשו GetProductAsync (כמו בדוגמה) על נתונים מדומים (בלי DB אמיתי — פונקציה שמחזירה מוצר “לאט” בכוונה עם Task.Delay). מדדו זמן תגובה בקריאה ראשונה מול שנייה.
בדיקה: הקריאה הראשונה איטית (Cache Miss); הקריאה השנייה מהירה משמעותית (Cache Hit).
פרויקט מסכם
המשימה: הוסיפו שכבת Cache-Aside אמיתית ל-TasksController (מהפרויקטים המסכמים הקודמים), על שליפת רשימת המשימות.
דרישות:
GetAllבודק קודם ב-Redis; אם קיים — מחזיר משם; אם לא — שולף מה-SqlTaskRepository(מיחידת EF Core) ושומר לקאש עם TTL של דקה- פעולת
Create/Update/Deleteמוחקת את מפתח הקאש הרלוונטי (Cache Invalidation) כדי למנוע נתונים מיושנים - מדידת זמן תגובה (
Stopwatch) שמדפיסה אם הבקשה הייתה Cache Hit או Miss
בדיקה: קריאה ראשונה ל-GetAll איטית יותר (Miss); קריאות חוזרות תוך פחות מדקה מהירות (Hit); הוספת משימה חדשה ואז GetAll מחזירה נתונים עדכניים, לא קאש מיושן.
מה בפרק הבא
בפרק הבא — Kafka: איך שולחים ומעבדים הודעות בין שירותים בזמן אמת, עם topics, partitions ו-consumer groups — הבסיס לתקשורת אסינכרונית במיקרוסרביסים.