blog.dopana

Back

Thiết kế URL shortener là bài toán kinh điển trong phỏng vấn system design. Nó tưởng đơn giản — nhưng ẩn chứa nhiều vấn đề thú vị: làm sao sinh short code duy nhất? Làm sao handle 10 tỷ URL? Cache thế nào? Redirect loại nào?

Bài này thiết kế một URL shortener từ yêu cầu đến implementation.

Yêu Cầu#

Functional#

  • Tạo short URL từ long URL
  • Redirect short URL → long URL
  • Optional: custom alias, expiry, analytics

Non-Functional#

  • Highly available: Không downtime
  • Low latency: Redirect < 10ms
  • Scale: 100M URLs/month, 10K writes/sec, 100K reads/sec

Estimations#

MetricValue
Writes100M / tháng ≈ 38 / sec
Reads100× writes ≈ 3800 / sec → peak 10K / sec
Storage100M × 500 bytes ≈ 50 GB / năm
BandwidthWrite: 38 × 500B ≈ 19 KB/s, Read: 3800 × 500B ≈ 1.9 MB/s

Thiết Kế#

Base62 — Short Code#

Short code cần: duy nhất, ngắn, URL-safe. Giải pháp: Base62 (a-z, A-Z, 0-9).

Số ký tựSố lượng URL
662⁶ ≈ 56 tỷ
762⁷ ≈ 3,500 tỷ
862⁸ ≈ 218,000 tỷ

7 ký tự đủ cho hầu hết hệ thống.

Cách 1: Hash + Collision#

Hash MD5 long URL, lấy 7 ký tự đầu, check collision.

import hashlib, base62

def generate_short_code(long_url: str) -> str:
    hash_bytes = hashlib.md5(long_url.encode()).digest()
    return base62.encode(int.from_bytes(hash_bytes[:7], 'big'))
python

Ưu: deterministic (cùng URL → cùng code), không cần DB để sinh. Nhược: collision rate cao khi scale.

Cách 2: Distributed ID + Base62#

Sinh unique ID từ distributed ID generator (Snowflake, Redis incr, PostgreSQL sequence), chuyển sang Base62.

import base62

def generate_short_code(db):
    # Redis INCR hoặc database sequence
    next_id = db.incr('url_counter')
    return base62.encode(next_id)
python
# Chuyển ID → Base62
BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

def encode_base62(num: int) -> str:
    if num == 0: return BASE62[0]
    chars = []
    while num > 0:
        num, rem = divmod(num, 62)
        chars.append(BASE62[rem])
    return ''.join(reversed(chars))

# encode_base62(123456789) → "8m0Kx" (6 ký tự)
python

Ưu: không collision, đơn giản. Nhược: cần central ID generator.

Database Schema#

API Design#

Create short URL#

POST /api/shorten
Content-Type: application/json

{
  "long_url": "https://example.com/very/long/url",
  "custom_alias": "",      // optional
  "expires_in_days": 30    // optional
}

Response: 201
{
  "short_url": "https://short.ly/abc1234",
  "short_code": "abc1234",
  "expires_at": "2026-07-31T10:00:00Z"
}
http

Redirect#

GET /abc1234

Response: 301 (permanent) hoặc 302 (temporary)
Location: https://example.com/very/long/url
http

301 vs 302#

StatusÝ nghĩaCacheKhi nào dùng
301Moved PermanentlyBrowser cacheLong URL cố định, không cần analytics
302FoundKhông cacheCần analytics (bitly pattern)

Bitly dùng 302 để track mỗi lần click. Nếu dùng 301, browser cache redirect → không gọi server → mất analytics.

Caching#

Cache Strategy#

Read:   GET short_code → check Redis → nếu miss → query DB → set Redis → redirect
Write:  POST long_url → sinh code → insert DB → set Redis
plaintext

Cache TTL#

  • Hot URLs: TTL dài (7 ngày), refresh mỗi lần access
  • Cold URLs: TTL ngắn (1 giờ)
  • LRU eviction khi đầy

Cache Estimations#

10K reads/sec, 80% cache hit → 8000 từ cache, 2000 từ DB. Với Redis cluster 3 node, mỗi node ~10GB RAM, chịu được ~50M entries.

Database Sharding#

Khi URLs > 1 tỷ, cần shard.

Shard key: short_code (hash modulo N shards)

def get_shard(short_code: str) -> int:
    return murmurhash(short_code) % NUM_SHARDS
python

Vấn đề: thêm shard cần rebalance → consistent hashing.

Rate Limiting#

Prevent abuse — giới hạn tạo URL mới:

# Token bucket: 100 URLs/hour/user
def can_create_url(user_id: str) -> bool:
    key = f"rate:{user_id}:{datetime.now().hour}"
    count = r.incr(key)
    if count == 1:
        r.expire(key, 3600)  # TTL 1 giờ
    return count <= 100
python

Implementation Đơn Giản (Flask + Redis + PostgreSQL)#

Evolution#

V1: PostgreSQL + Redis (1M URLs/month)
    └── Đơn giản, đủ dùng cho MVP

V2: Add sharding + read replicas (100M URLs/month)
    └── Scale DB, cache cluster

V3: Add Kafka + ClickHouse (1B URLs/month)
    └── Analytics riêng, không ảnh hưởng redirect path

V4: Global load balancer + multi-region (10B+ URLs/month)
    └── Geo-routing, global Redis, active-active DB
plaintext

Bắt đầu đơn giản, scale dần. Không cần Kafka và ClickHouse từ ngày đầu.

Tổng Kết#

URL shortener tưởng đơn giản nhưng dạy nhiều concept quan trọng:

  • Base62 encoding — sinh short code
  • Caching — Redis, TTL, LRU
  • Database sharding — consistent hashing
  • HTTP redirect — 301 vs 302
  • Rate limiting — token bucket
  • Analytics — async processing
  • Evolution — từ simple đến global scale

Tài liệu tham khảo#