מה זה?

שאילתה שחוזרת על עצמה מאות פעמים בדקה (למשל “הבא לי את פרטי המוצר הפופולרי ביותר”) פוגעת ב-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.Redis

3. רושמים את החיבור פעם אחת, ב-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) מתאימים למגוון תרחישים מעבר למפתח-ערך פשוט.

דוקומנטציה רשמית

Redis — Documentation

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 (מהפרויקטים המסכמים הקודמים), על שליפת רשימת המשימות.

דרישות:

  1. GetAll בודק קודם ב-Redis; אם קיים — מחזיר משם; אם לא — שולף מה-SqlTaskRepository (מיחידת EF Core) ושומר לקאש עם TTL של דקה
  2. פעולת Create/Update/Delete מוחקת את מפתח הקאש הרלוונטי (Cache Invalidation) כדי למנוע נתונים מיושנים
  3. מדידת זמן תגובה (Stopwatch) שמדפיסה אם הבקשה הייתה Cache Hit או Miss

בדיקה: קריאה ראשונה ל-GetAll איטית יותר (Miss); קריאות חוזרות תוך פחות מדקה מהירות (Hit); הוספת משימה חדשה ואז GetAll מחזירה נתונים עדכניים, לא קאש מיושן.

מה בפרק הבא

בפרק הבא — Kafka: איך שולחים ומעבדים הודעות בין שירותים בזמן אמת, עם topics, partitions ו-consumer groups — הבסיס לתקשורת אסינכרונית במיקרוסרביסים.