לדלג לתוכן

4.4 מקצר URLs הרצאה

מקרה בוחן: עיצוב מקצר URLs

מקצר URLs (כמו bit.ly) הוא שאלת system design קלאסית בראיונות עבודה. נעבור על כל שלבי העיצוב.


שלב 1: הגדרת דרישות

שאלות שצריך לשאול:

דרישות פונקציונליות:
- משתמש מכניס URL ארוך ומקבל URL קצר
- מי שנכנס ל-URL הקצר מועבר (redirect) ל-URL המקורי
- האם URL הקצר הוא אקראי או ניתן לבחירה? (ניתן לבחירה - אופציה)
- מהו תוקף ה-URL? (לצמיתות)
- האם צריך analytics על כמה קליקים?

דרישות לא-פונקציונליות:
- 100 מיליון URLs במערכת
- יחס קריאה/כתיבה: 10:1 (הרבה יותר redirects מהקצרות)
- latency נמוך ל-redirect (< 10ms)
- זמינות גבוהה


שלב 2: הערכת נפח

כתיבות:
- 100 מיליון URLs
- נניח 10 מיליון כתיבות חדשות בחודש
- זה ~4 כתיבות בשנייה

קריאות:
- יחס 10:1 → 40 redirects בשנייה
- בשיא: בוא נניח 1000 redirects בשנייה

אחסון:
- URL ארוך: 2KB ממוצע
- 100 מיליון × 2KB = 200GB

לא מסיבי - שרת אחד יכול להכיל, אבל נצטרך scaling לביצועים.


שלב 3: עיצוב ה-API

POST /api/shorten
Request:  { "long_url": "https://www.example.com/very/long/path/..." }
Response: { "short_url": "https://short.ly/abc123", "short_code": "abc123" }

GET /{short_code}
Response: HTTP 301/302 Redirect → long_url

301 לעומת 302:
- 301 Permanent Redirect: דפדפן שומר ולא שולח בקשה לשרת שלנו פעמיים - פחות עומס אבל אין analytics
- 302 Temporary Redirect: כל קליק מגיע לשרת שלנו - יש analytics


שלב 4: מבנה הנתונים

CREATE TABLE urls (
    id          BIGINT PRIMARY KEY AUTO_INCREMENT,
    short_code  VARCHAR(8) UNIQUE NOT NULL,
    long_url    TEXT NOT NULL,
    created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    user_id     BIGINT,  -- אם מחוברים
    click_count BIGINT DEFAULT 0
);

CREATE INDEX idx_short_code ON urls(short_code);

שלב 5: יצירת short code

אפשרות 1: Hash + Truncation

import hashlib
import base64


def generate_short_code(long_url: str) -> str:
    hash_bytes = hashlib.md5(long_url.encode()).digest()
    encoded = base64.urlsafe_b64encode(hash_bytes).decode()
    return encoded[:7]  # 7 תווים = 62^7 = 3.5 טריליון קומבינציות

בעיה: התנגשויות (collisions) - שתי URLs שונות יכולות לקבל אותו code.

אפשרות 2: ID + Base62 Encoding

BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"


def encode_base62(num: int) -> str:
    if num == 0:
        return BASE62[0]
    result = []
    while num:
        result.append(BASE62[num % 62])
        num //= 62
    return "".join(reversed(result))


def decode_base62(code: str) -> int:
    result = 0
    for char in code:
        result = result * 62 + BASE62.index(char)
    return result


# דוגמה
print(encode_base62(12345678))  # "4qbEW"
print(decode_base62("4qbEW"))   # 12345678

יתרון: אין התנגשויות. ID מהDB → code ייחודי.


שלב 6: ארכיטקטורה

Client → [Load Balancer]
         [API Servers] (stateless, ניתן לscale אופקי)
    ┌─────────┼─────────┐
    ↓         ↓         ↓
[Redis     [Primary  [Read
 Cache]      DB]     Replicas]

Redirect Flow - מהיר

async def redirect(short_code: str):
    # שלב 1: בדוק cache ראשון
    long_url = await redis.get(f"url:{short_code}")

    if not long_url:
        # שלב 2: DB
        row = db_replica.execute(
            "SELECT long_url FROM urls WHERE short_code=?", (short_code,)
        ).fetchone()
        if not row:
            raise HTTPException(404)
        long_url = row[0]
        # שלב 3: שמור בcache ל-24 שעות
        await redis.setex(f"url:{short_code}", 86400, long_url)

    # שלב 4: עדכן counter (async, לא blocking)
    background_tasks.add_task(increment_click_count, short_code)

    return RedirectResponse(url=long_url, status_code=302)

Shorten Flow

def shorten(long_url: str) -> str:
    # בדוק אם כבר קיים
    existing = db_primary.execute(
        "SELECT short_code FROM urls WHERE long_url=?", (long_url,)
    ).fetchone()
    if existing:
        return existing[0]

    # הכנס לDB - קבל ID
    cursor = db_primary.execute(
        "INSERT INTO urls (long_url) VALUES (?)", (long_url,)
    )
    url_id = cursor.lastrowid

    # המר ID לshort code
    short_code = encode_base62(url_id)

    # עדכן שדה short_code
    db_primary.execute(
        "UPDATE urls SET short_code=? WHERE id=?", (short_code, url_id)
    )

    return short_code

שלב 7: Bottlenecks ופתרונות

הכי חשוב: redirects מהירים. Cache hit rate גבוה = מהיר.

מה לאחסן בcache: URLs פופולריות מאוד (20% מה-URLs = 80% מהקליקים, Zipf's Law).

Cache eviction: LRU (Least Recently Used) - URLs שלא נגישו הכי הרבה - יוצאות ראשונות.

Rate Limiting: מניעת spam - לא יותר מ-100 קיצורים לדקה למשתמש.


סיכום הארכיטקטורה

רכיב תפקיד
Load Balancer פיזור תעבורה
API Servers (×3) stateless, ניתן לscale
Redis cache ל-redirects
PostgreSQL Primary כתיבות
PostgreSQL Replicas קריאות
Base62 Encoder short code generation