# Redis — מטמון ותבנית Cache-Aside📖 שיעור

## מה זה?

שאילתה שחוזרת על עצמה מאות פעמים בדקה (למשל "הבא לי את פרטי המוצר הפופולרי ביותר") פוגעת ב-DB פעם אחר פעם עם **אותה** תוצאה בדיוק. Redis פותר את זה: מסד נתונים בזיכרון (RAM), מהיר בסדרי גודל מ-SQL/MongoDB (שעל דיסק), שמתפקד כמטמון (Cache) — שומרים תוצאות שכבר חושבו, ובפעם הבאה שמבקשים אותו דבר, קוראים מהזיכרון במקום לחשב/לשלוף מהדיסק שוב.

## איך מכניסים את זה לפרויקט שלכם — צעד אחר צעד

לפני שמשתמשים ב-Redis בקוד (כמו בדוגמאות בהמשך הדף), צריך להכניס אותו לפרויקט בפועל. ארבעה שלבים, בסדר הזה:

**1. מריצים Redis מקומית** — הכי מהיר עם Docker (אם מותקן):

```bash
docker run -d -p 6379:6379 --name redis redis
```

בלי Docker על Windows — אפשרות חלופית היא [Memurai](https://www.memurai.com/) (Redis תואם ל-Windows) או הרצה דרך WSL.

**2. מתקינים את החבילה בפרויקט ה-.NET:**

```bash
dotnet add package StackExchange.Redis
```

**3. רושמים את החיבור פעם אחת, ב-`Program.cs`** — לא בכל קונטרולר מחדש:

```csharp
// Program.cs — register once, reused everywhere via DI
builder.Services.AddSingleton<IConnectionMultiplexer>(
    ConnectionMultiplexer.Connect("localhost:6379"));
```

`AddSingleton` חשוב כאן — `ConnectionMultiplexer` מיועד להיווצר **פעם אחת** ולהישאר פתוח לכל חיי האפליקציה, לא להיפתח מחדש בכל בקשה.

**4. מזריקים (Dependency Injection) את החיבור לכל שירות שצריך אותו:**

```csharp
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 **ואז** שומרים במטמון לפעם הבאה

```csharp
// 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")
});
```

```csharp
// 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](https://redis.io/docs/latest/)

[Microsoft Learn — Cache-Aside pattern](https://learn.microsoft.com/azure/architecture/patterns/cache-aside)

---

## תרגילים

### תרגיל 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` מחזירה נתונים עדכניים, לא קאש מיושן.

<script type="application/json" id="lesson-quiz-data">
[
  {
    "question": "מה קורה קודם בתבנית Cache-Aside כשמבקשים נתון?",
    "options": [
      "בודקים אם הוא כבר קיים בקאש (Redis)",
      "שולפים אותו ישירות מה-DB",
      "מוחקים אותו מהקאש",
      "יוצרים TTL חדש"
    ],
    "correctIndex": 0
  },
  {
    "question": "למה Redis מהיר בהרבה מ-DB רגיל כמו SQL Server?",
    "options": [
      "הוא שומר את הנתונים בזיכרון RAM ולא על דיסק",
      "הוא כותב תמיד ל-DB הראשי במקביל",
      "הוא לא תומך בשאילתות מורכבות",
      "הוא רץ תמיד על שרת חזק יותר"
    ],
    "correctIndex": 0
  },
  {
    "question": "מה קורה כש-TTL של ערך בקאש פג?",
    "options": [
      "הערך נמחק אוטומטית מהקאש — הקריאה הבאה תהיה Cache Miss",
      "הערך הופך לקבוע לצמיתות בקאש",
      "ה-DB המקורי נמחק גם הוא",
      "TTL לא משפיע על התנהגות הקאש בפועל"
    ],
    "correctIndex": 0
  }
]
</script>
<div id="lesson-quiz"></div>

## מה בפרק הבא

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