Harbor

Rút gọn link

DễĐọc nhiềuSinh IDCacheRedirectGiống Bitly, TinyURL

Hiểu đề

Rút gọn link là gì? Dịch vụ biến một URL dài thành mã ngắn như sho.rt/aB3x9Kq. Người tạo link là marketer, dev, người dùng mạng xã hội. Người bấm link là bất kỳ ai, và số lượt bấm lớn gấp khoảng 100 lần số link được tạo.

Yêu cầu chức năng

  1. Người dùng có thể dán URL dài và nhận về link ngắn
  2. Người bấm link ngắn được chuyển hướng tới URL gốc
  3. Người dùng có thể tự đặt alias (ví dụ sho.rt/sale-t11) và đặt ngày hết hạn
  4. Người tạo link có thể xem số lượt click theo ngày

Yêu cầu phi chức năng

  • Chuyển hướng p95 < 100 ms tính từ lúc request tới server
  • Đường redirect sẵn sàng 99,99% (tối đa khoảng 52 phút gián đoạn mỗi năm)
  • Mã sinh tự động dài tối đa 7 ký tự và không đoán được link kế tiếp
  • Tỷ lệ đọc:ghi khoảng 100:1, thiết kế phải ưu tiên đường đọc
  • Số liệu click được phép trễ tối đa 5 phút

Ngoài phạm vi: Quét mã độc và phishing chuyên sâu (chỉ kiểm tra blocklist đơn giản); Sửa URL đích sau khi tạo; Tên miền riêng cho từng khách hàng; Đăng ký, đăng nhập và phân quyền chi tiết.

Thực thể và API

ShortUrl
code (khóa chính), longUrl, ownerId, isCustom, createdAt, expiresAt. Khoảng 500 B mỗi bản ghi.
User
Người tạo link: id, email, gói dịch vụ. Dùng để giới hạn số link và xem thống kê.
ClickEvent
code, thời điểm, quốc gia, referrer, user agent. Ghi bất đồng bộ, lưu ở kho phân tích riêng.
IdRange
Một khoảng ID liên tục (ví dụ 5.000.000 đến 5.000.999) đã cấp cho một server. Giúp sinh mã không cần hỏi DB mỗi lần.
MethodEndpointGhi chú
POST/v1/urlsBody: longUrl, customAlias?, expiresAt?. Trả về code và shortUrl. 409 nếu alias đã có người dùng.
GET/{code}Trả 302 kèm header Location. 404 nếu không có, 410 nếu đã hết hạn.
GET/v1/urls/{code}/statsSố click theo ngày, chỉ chủ link được xem.
DELETE/v1/urls/{code}Chủ link xóa link. Xóa cả bản ghi trong cache.

Thiết kế tổng thể

Đi lần lượt từng yêu cầu chức năng. Mục tiêu ở đây là chạy đúng, việc tối ưu để dành cho phần đào sâu.

1) Người dùng có thể dán URL dài và nhận về link ngắn

API kiểm tra URL rồi lấy một ID số từ khoảng ID mà server đang giữ. ID được xáo trộn rồi mã hóa base62 thành chuỗi 7 ký tự. Vì mỗi ID chỉ cấp một lần nên không cần kiểm tra trùng. Grokking dùng base-58 (bỏ 0, O, I, l và ký tự +, /) để dễ đọc; khoá học dùng base62 cho mã ngắn hơn một chút.

  1. 1Client gửi POST /v1/urls với longUrl
  2. 2API kiểm tra định dạng URL và blocklist tên miền
  3. 3API lấy ID kế tiếp trong IdRange đang giữ trong bộ nhớ, hết thì xin khoảng mới
  4. 4Xáo trộn ID bằng một hoán vị cố định rồi mã hóa base62 thành code
  5. 5INSERT bản ghi ShortUrl vào DB và trả shortUrl cho client

Nguồn: Grokking Modern System Design · ch.34 Design TinyURL ↗ · Design and Deployment of TinyURL · mục "Workflow" (Shortening) và "Encoder for TinyURL" (sequencer 64-bit + bộ mã hóa base-58) ; Grokking Modern System Design · ch.12 Sequencer ↗ · mục "Third solution: using a range handler" ; Grokking Modern System Design · ch.34 Design TinyURL ↗ · Evaluation of TinyURL's Design · mục "Unpredictability" ; Chưa có nguồn trong thư viện: hoán vị cố định (xáo trộn ID) và việc dùng base62 thay cho base-58 là lựa chọn của khoá học

2) Người bấm link ngắn được chuyển hướng tới URL gốc

Đây là đường nóng nhất, chiếm khoảng 99% traffic. Server đọc cache trước, chỉ hỏi DB khi cache trượt. Ghi click đi qua queue để không làm chậm redirect.

  1. 1Trình duyệt gọi GET /{code}
  2. 2API đọc Redis theo code, trượt thì đọc DB rồi ghi lại vào cache
  3. 3Kiểm tra expiresAt, đã quá hạn thì trả 410
  4. 4Trả 302 với Location là longUrl
  5. 5Đẩy ClickEvent vào queue sau khi đã trả response

Nguồn: Grokking Modern System Design · ch.34 Design TinyURL ↗ · Design and Deployment of TinyURL · mục "Workflow" (Redirection: kiểm tra cache rồi tới database) ; Grokking Modern System Design · ch.16 Distributed Cache ↗ · Background of Distributed Cache · mục "Writing policies" (write-around: ghi vào cache sau lần trượt) ; Node.js in Practice ↗ (.zip) · ch.7 Networking · Technique 51 "Following redirects" (mã 301/302, tr. 158–159) ; Grokking Modern System Design · ch.17 Distributed Messaging Queue ↗ · mục "Motivation" (Improved performance, Granular scalability)

3) Người dùng có thể tự đặt alias và đặt ngày hết hạn

Alias tự đặt được ghi bằng lệnh insert có điều kiện, DB tự từ chối nếu trùng. Hết hạn được kiểm tra lúc đọc, còn việc xóa hẳn do job chạy nền.

  1. 1Client gửi customAlias và expiresAt
  2. 2API kiểm tra alias hợp lệ (4-30 ký tự, có dấu gạch ngang để không trùng không gian mã tự sinh)
  3. 3INSERT với ràng buộc unique trên code, lỗi trùng thì trả 409
  4. 4Cache đặt TTL không vượt quá expiresAt
  5. 5Job hằng đêm xóa các link đã hết hạn quá 30 ngày

Nguồn: Grokking Modern System Design · ch.34 Design TinyURL ↗ · Requirements of TinyURL's Design · mục "Functional requirements" (Custom short links, Expiry time; không tái sử dụng mã hết hạn) ; Grokking Modern System Design · ch.34 Design TinyURL ↗ · Design and Deployment of TinyURL · mục "Workflow" (Custom short links) ; MongoDB: The Definitive Guide 2e ↗ (.zip) · ch.5 Indexing · mục "Unique Indexes" (tr. 104) ; Chưa có nguồn trong thư viện: độ dài alias 4-30 ký tự và mốc xóa sau 30 ngày là giả định của khoá học

4) Người tạo link có thể xem số lượt click theo ngày

Click được gom theo lô từ queue và ghi vào kho phân tích. API thống kê chỉ đọc bảng đã cộng sẵn theo ngày nên rất nhẹ.

  1. 1Worker đọc ClickEvent từ queue theo lô vài nghìn sự kiện
  2. 2Ghi lô vào kho phân tích và cộng dồn bảng click theo (code, ngày)
  3. 3GET /v1/urls/{code}/stats đọc bảng cộng dồn

Nguồn: Grokking Modern System Design · ch.17 Distributed Messaging Queue ↗ · mục "Motivation" (Improved performance, Granular scalability) ; Grokking Modern System Design · ch.24 Sharded Counters ↗ · mục "Burst of writes requests" ; Chưa có nguồn trong thư viện: kho phân tích (BigQuery/ClickHouse) không có sách trong thư viện, chỉ nêu ở mức khái niệm

Hạ tầng theo quy mô

Cùng một sản phẩm, ba mức người dùng. Chọn từng mức để xem sơ đồ, phép tính và cấu hình server thay đổi thế nào.

Người dùng
10.000 người tạo link/ngày
Tải đỉnh
≈ 120 req/s đỉnh
Chi phí ước lượng
≈ 40–90 USD/tháng

Hai instance Node.js trên Cloud Run và một Postgres managed. Mã sinh từ sequence của Postgres, click đếm ngay trong DB. Traffic nhỏ nên mọi thứ nằm gọn trong một database.

Người dùngNgười dùngCân bằng tảiLoad balancerDịch vụAPI Node.js ×2DatabasePostgreSQLWorkerJob dọn linkHTTPSSQLxóa hết hạn

Ước lượng

  • Tạo link: 10.000 link/ngày / 86.400 ≈ 0,12 req/s
  • Redirect (đọc:ghi 100:1): 10.000 × 100 = 1.000.000 lượt/ngày / 86.400 ≈ 11,6 req/s, đỉnh ×10 ≈ 116 req/s
  • API: ceil(116 / (1.000 × 0,6)) = 1, lấy tối thiểu 2 instance để có dự phòng
  • Lưu trữ: 10.000 × 500 B × 365 ≈ 1,8 GB/năm
  • Không gian mã: 62^7 ≈ 3.500 tỷ; 5 năm × 3,65 triệu link ≈ 18 triệu, dùng chưa tới 0,001%

Nguồn con số: Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations ↗ · mục "Examples of Resource Estimation" (Number of servers required, Storage requirements, Bandwidth requirements) ; Grokking Modern System Design · ch.34 Design TinyURL ↗ · Requirements of TinyURL's Design · mục "Resource estimation" (tỷ lệ 1:100, 500 B mỗi bản ghi, cache theo 80-20) ; Quy tắc ước lượng của khoá học · /hoc/chon-server (1.000 req/s mỗi máy 2 vCPU, chạy tối đa 60% CPU, Redis ×1,3)

Bảng hạ tầng

Thành phầnChọnSố lượngCấu hìnhVì sao
Load balancerLB có sẵn của Cloud Run + TLS managed1 (managed)Mặc địnhKhông phải tự vận hành, đủ cho vài trăm req/s.
APINode.js (Fastify), Docker, Cloud Run22 vCPU · 4 GB RAM116 req/s đỉnh chỉ dùng khoảng 12% sức một instance, cái thứ hai để dự phòng.
DatabasePostgreSQL managed (Cloud SQL / RDS)12 vCPU · 8 GB · 20 GB SSDVài GB dữ liệu và 116 QPS đọc là rất nhẹ với một node có index trên code.
Sinh mãSequence Postgres + xáo trộn + base62Nằm trong DBBIGSERIALGhi 0,12 req/s nên gọi nextval mỗi lần vẫn thoải mái.
Đếm clickBảng click_daily trong Postgres, UPDATE gộp mỗi 10 sNằm trong DBGộp trong bộ nhớ APIGộp lại để không ghi 1 lần cho mỗi lượt bấm.
Job dọn linkCloud Scheduler gọi endpoint nội bộ1 job/đêmXóa theo lô 1.000 dòngLượng link hết hạn mỗi ngày rất nhỏ.
Thay đổi so với mức trước
  • Điểm xuất phát
  • Không có cache vì DB đủ nhanh ở 116 QPS
  • Không có queue, click được gộp trong bộ nhớ rồi UPDATE định kỳ
Nút thắt tiếp theo

Khi redirect lên vài nghìn req/s, mỗi lượt đều đọc DB và UPDATE click nên Postgres là thứ quá tải đầu tiên.

Cấu hình và chi phí là ước lượng để học và trình bày trong phỏng vấn. Xem cách tính ở bài Chọn server.

So sánh nhanh ba mức

Thành phầnMức 1Mức 2Mức 3
Người dùng10.000 người tạo link/ngày1 triệu người tạo link/ngày50 triệu DAU · 10 triệu link mới/ngày
Chi phí≈ 40–90 USD/tháng≈ 1.500–3.500 USD/tháng≈ 50.000–100.000 USD/tháng (CDN và ScyllaDB chiếm phần lớn)
Load balancer
LB có sẵn của Cloud Run + TLS managed
1 (managed) × Mặc định
L7 LB managed (ALB / Cloud LB)
1 (managed) × Mặc định
Chưa cần
API
Node.js (Fastify), Docker, Cloud Run
2 × 2 vCPU · 4 GB RAM
Node.js, container autoscale theo CPU
10 lúc đỉnh × 2 vCPU · 4 GB RAM
Node.js trên Kubernetes, HPA theo CPU
40 pod (20/region) × 2 vCPU · 4 GB
Database
PostgreSQL managed (Cloud SQL / RDS)
1 × 2 vCPU · 8 GB · 20 GB SSD
Chưa cần
ScyllaDB, phân mảnh theo hash(code), TTL theo expiresAt
12 node/region, tổng 24 × 16 vCPU · 128 GB · 3,75 TB NVMe
Sinh mã
Sequence Postgres + xáo trộn + base62
Nằm trong DB × BIGSERIAL
Sequence tăng 1.000 mỗi lần, API giữ khoảng trong bộ nhớ
Nằm trong DB × INCREMENT BY 1000
Chưa cần
Đếm click
Bảng click_daily trong Postgres, UPDATE gộp mỗi 10 s
Nằm trong DB × Gộp trong bộ nhớ API
Chưa cầnChưa cần
Job dọn link
Cloud Scheduler gọi endpoint nội bộ
1 job/đêm × Xóa theo lô 1.000 dòng
Chưa cầnChưa cần
CacheChưa cần
Redis managed (ElastiCache / Memorystore)
1 primary + 1 replica × 13 GB RAM
Redis Cluster
3 shard × 2 = 6 node/region, tổng 12 × 16 GB RAM mỗi node
DB primaryChưa cần
PostgreSQL managed, HA standby
1 (+1 standby) × 4 vCPU · 16 GB · 1 TB SSD
Chưa cần
DB replicaChưa cần
Postgres read replica
1 × 4 vCPU · 16 GB · 1 TB SSD
Chưa cần
Queue clickChưa cần
Pub/Sub hoặc SQS
1 topic × Managed
Chưa cần
WorkerChưa cần
Node.js consumer ghi theo lô
2 × 1 vCPU · 2 GB
Chưa cần
Kho phân tíchChưa cần
BigQuery hoặc ClickHouse Cloud
1 (managed) × ≈ 10 GB/ngày thô
ClickHouse
3 shard × 2 replica = 6 node × 8 vCPU · 32 GB · 2 TB SSD
CDNChưa cầnChưa cần
Cloudflare hoặc CloudFront, cache 302 với max-age 300 s
Toàn cầu (managed) × Log stream bật
DNS + LBChưa cầnChưa cần
GeoDNS + L7 LB mỗi region
2 region × Active-active
Cấp khoảng IDChưa cầnChưa cần
Service nhỏ lưu con trỏ khoảng trong etcd
3 pod/region × 1 vCPU · 1 GB
KafkaChưa cầnChưa cần
Kafka managed (MSK / Confluent)
3 broker/region × 12 partition topic clicks
Stream workerChưa cầnChưa cần
Consumer Node.js hoặc Flink
6 pod × 2 vCPU · 4 GB

Đào sâu

Mỗi câu hỏi đi từ cách đơn giản đến cách người phỏng vấn mong đợi ở cấp senior.

1) Làm sao sinh mã ngắn không trùng khi có nhiều server cùng tạo link?

Cách chưa tốt
Hash URL (MD5) rồi cắt 7 ký tự

Lấy MD5 của longUrl, mã hóa base62 và giữ 7 ký tự đầu. Khi số link lên hàng tỷ, hai URL khác nhau sẽ cho cùng 7 ký tự. Phải đọc DB kiểm tra trùng rồi thử lại với salt.

Đánh đổi: Đơn giản và cùng URL cho cùng mã, nhưng mỗi lần tạo tốn thêm một lần đọc DB và tỷ lệ va chạm tăng dần theo dữ liệu.

⚠ Chưa có nguồn trong thư viện: sách không phân tích cách hash URL rồi cắt; Grokking Modern System Design · ch.34 Design TinyURL · Encoder for TinyURL dùng ID từ sequencer + base-58 để tránh va chạm thay vì hash

Cách tốt
Chuỗi ngẫu nhiên 7 ký tự base62 + ràng buộc unique

Sinh 7 ký tự ngẫu nhiên từ 62^7 ≈ 3.500 tỷ khả năng. INSERT với unique constraint, trùng thì sinh lại. Với vài tỷ link, xác suất trùng mỗi lần dưới 0,1% nên hiếm khi phải thử lại.

Đánh đổi: Không đoán được và không cần điều phối, nhưng vẫn phụ thuộc DB báo trùng và tỷ lệ thử lại tăng khi không gian mã đầy dần.

Nguồn: Grokking Modern System Design · ch.12 Sequencer ↗ · mục "First solution: UUID" (sinh ngẫu nhiên, vẫn có khả năng trùng) ; Grokking Modern System Design · ch.34 Design TinyURL ↗ · Design and Deployment of TinyURL · mục "Design" (MongoDB trả lỗi duplicate-key khi trùng) ; MongoDB: The Definitive Guide 2e ↗ (.zip) · ch.5 Indexing · mục "Unique Indexes" (tr. 104)

Cách tốt nhất
Bộ đếm cấp theo khoảng (range sequencer) + xáo trộn + base62

Một dịch vụ nhỏ cấp cho mỗi server một khoảng ID, ví dụ 1.000 hoặc 10.000 số. Server tự tăng ID trong bộ nhớ, không gọi mạng. ID đi qua một hoán vị cố định (ví dụ Feistel vài vòng) rồi mã hóa base62, nên mã không trùng và không lộ thứ tự. Ở nhiều region, mỗi region nhận các khoảng rời nhau.

Đánh đổi: Không bao giờ trùng và không cần đọc DB, đổi lại phải vận hành dịch vụ cấp khoảng có HA và chấp nhận mất vài khoảng ID khi server chết.

Nguồn: Grokking Modern System Design · ch.12 Sequencer ↗ · mục "Third solution: using a range handler" (Pros/Cons: mất một khoảng khi server chết) ; Grokking Modern System Design · ch.34 Design TinyURL ↗ · Encoder for TinyURL · mục "The scope of the short URL generator" ; Grokking Modern System Design · ch.34 Design TinyURL ↗ · Evaluation of TinyURL's Design · mục "Unpredictability" (chọn ngẫu nhiên ID trong khoảng) ; Chưa có nguồn trong thư viện: hoán vị Feistel không có trong sách, sách chỉ đề xuất chọn ngẫu nhiên ID và thêm salt

2) Làm sao redirect nhanh mà vẫn đếm được click? Dùng 301 hay 302?

Cách chưa tốt
Trả 301 và đọc thẳng DB

301 nghĩa là chuyển hẳn, trình duyệt sẽ lưu và lần sau tự đi tới URL gốc mà không gọi lại server. Tải giảm, nhưng các lần bấm sau không còn qua server nên mất số liệu click. Link cũng không thể hết hạn hay bị xóa với người đã từng bấm.

Đánh đổi: Ít tải nhất cho server nhưng mất analytics và mất quyền kiểm soát link.

Nguồn: Node.js in Practice ↗ (.zip) · ch.7 Networking · Technique 51 "Following redirects" (301 Moved permanently: client nên cập nhật URL vĩnh viễn, tr. 158–159) ; Chưa có nguồn trong thư viện: hành vi trình duyệt lưu 301 và hệ quả mất số liệu click

Cách tốt
Trả 302 + Redis cache-aside + ghi click qua queue

302 là chuyển tạm thời nên mỗi lần bấm đều quay về server, đếm được đủ. Redis giữ các link nóng, theo giả định 80-20 của Grokking, khoảng 20% link tạo ra 80% lượt bấm nên cache trúng phần lớn. Ghi click đẩy vào queue để redirect không phải đợi DB.

Đánh đổi: Đếm đủ và kiểm soát được link, nhưng mọi lượt bấm đều chạy tới server gốc nên chi phí tăng tuyến tính theo traffic.

Nguồn: Node.js in Practice ↗ (.zip) · ch.7 Networking · Technique 51 "Following redirects" (302 Found, tr. 158–159) ; Grokking Modern System Design · ch.16 Distributed Cache ↗ · Background of Distributed Cache · mục "Writing policies" (write-around: ghi vào cache sau lần trượt) ; Grokking Modern System Design · ch.34 Design TinyURL ↗ · Requirements of TinyURL's Design · mục "Memory estimation" (giả định 80-20) ; Grokking Modern System Design · ch.17 Distributed Messaging Queue ↗ · mục "Motivation" (Improved performance, Granular scalability)

Cách tốt nhất
302 có Cache-Control ngắn ở CDN + log từ edge

Trả 302 với Cache-Control: max-age khoảng 300 s để CDN giữ redirect gần người dùng, còn trình duyệt không cache lâu. Lượt bấm trúng CDN vẫn được đếm qua log stream của CDN đẩy vào Kafka. Khi xóa link hoặc hết hạn thì gọi purge CDN theo code.

Đánh đổi: Server gốc chỉ nhận phần trượt CDN, đổi lại số liệu click trễ vài phút và xóa link có độ trễ tối đa bằng max-age nếu purge lỗi.

Nguồn: Grokking Modern System Design · ch.11 Content Delivery Network (CDN) ↗ · Content Consistency in CDN · mục "Time-to-live (TTL)" ; Grokking Modern System Design · ch.11 Content Delivery Network (CDN) ↗ · mục "Pull CDN" ; Chưa có nguồn trong thư viện: log stream từ CDN vào Kafka và API purge là tính năng của nhà cung cấp CDN

3) Khi số link lên hàng tỷ thì mở rộng đường đọc và lưu trữ thế nào?

Cách chưa tốt
Một Postgres to dần lên

Tăng CPU và RAM cho một node mỗi khi chậm. Ổn tới vài trăm GB, nhưng 10 tỷ bản ghi × 500 B là 5 TB, index không còn nằm trong RAM. Node đó cũng là điểm chết duy nhất của toàn bộ redirect.

Đánh đổi: Đơn giản nhất nhưng có trần cứng về dung lượng và độ sẵn sàng.

Nguồn: Grokking Modern System Design · ch.4 Non-functional System Characteristics ↗ · Scalability · mục "Vertical scalability--scaling up" và "Horizontal scalability--scaling out" ; Grokking Modern System Design · ch.9 Databases ↗ · Data Partitioning · mục "Why do we partition data?"

Cách tốt
Postgres primary + read replica + Redis

Redis đỡ phần lớn lượt đọc, replica đỡ phần cache trượt, primary chỉ nhận ghi. Với tỷ lệ 100:1 thì primary gần như rảnh. Đủ cho tới khoảng 1 TB dữ liệu.

Đánh đổi: Dễ vận hành và vẫn dùng SQL, nhưng dung lượng vẫn bị giới hạn bởi một node primary.

Nguồn: Grokking Modern System Design · ch.9 Databases ↗ · Data Replication · mục "Single leader/primary-secondary replication" và "Synchronous versus asynchronous replication" ; Grokking Modern System Design · ch.16 Distributed Cache ↗ · Background of Distributed Cache · mục "Writing policies" (write-around: ghi vào cache sau lần trượt) ; Chưa có nguồn trong thư viện: mốc "khoảng 1 TB" là ước lượng của khoá học

Cách tốt nhất
Key-value store phân mảnh theo code, nhiều region

Truy cập luôn là lấy theo code nên hợp với kho NoSQL phân mảnh. Grokking chọn MongoDB (có leader, đọc nhanh, báo trùng khóa) thay vì Cassandra/DynamoDB vốn cần đọc theo quorum; ScyllaDB hoặc DynamoDB cũng dùng được nếu chấp nhận nhất quán yếu hơn. Dữ liệu phân mảnh bằng hash của code và tự cân bằng khi thêm node. Bật TTL có sẵn để tự xóa link hết hạn. Mỗi region giữ một bản sao đầy đủ để redirect đọc tại chỗ, Redis Cluster phía trước giữ link nóng.

Đánh đổi: Mở rộng gần như tuyến tính và chịu được mất một region, đổi lại mất truy vấn SQL linh hoạt và replication giữa region là eventual consistency.

Nguồn: Grokking Modern System Design · ch.34 Design TinyURL ↗ · Evaluation of TinyURL's Design · mục "Scalability" (sharding ngang + consistent hashing) ; Grokking Modern System Design · ch.34 Design TinyURL ↗ · Design and Deployment of TinyURL · mục "Design" (Why are NoSQL databases like Cassandra or Riak not good choices instead of MongoDB?) ; Grokking Modern System Design · ch.10 Key-value Store ↗ · Design of a Key-value Store · mục "Consistent hashing" và "Use virtual nodes" ; Grokking Modern System Design · ch.9 Databases ↗ · Data Replication · mục "Single leader/primary-secondary replication" và "Synchronous versus asynchronous replication" ; MongoDB: The Definitive Guide 2e ↗ (.zip) · ch.6 Special Index and Collection Types · mục "Time-To-Live Indexes" (tr. 114)

4) Alias tự đặt và hết hạn xử lý thế nào cho đúng?

Cách chưa tốt
SELECT kiểm tra rồi mới INSERT

Đọc xem alias đã có chưa, chưa có thì insert. Hai người gửi cùng alias trong vài ms sẽ cùng thấy trống và người sau ghi đè người trước. Link hết hạn được xóa bằng một lệnh DELETE lớn mỗi đêm, làm DB khựng.

Đánh đổi: Dễ viết nhưng có race condition và job xóa gây tải đột biến.

Nguồn: MongoDB: The Definitive Guide 2e ↗ (.zip) · ch.5 Indexing · mục "Unique Indexes" (tr. 104) ; Grokking Modern System Design · ch.34 Design TinyURL ↗ · Design and Deployment of TinyURL · mục "Design" (dựa vào lỗi duplicate-key thay vì đọc trước)

Cách tốt
Insert có điều kiện + kiểm tra hạn lúc đọc

Dùng unique constraint hoặc INSERT ... ON CONFLICT DO NOTHING, DB đảm bảo chỉ một người thắng. Lúc redirect so sánh expiresAt, quá hạn thì trả 410 ngay cả khi bản ghi chưa bị xóa. Job xóa chạy theo lô nhỏ.

Đánh đổi: Đúng và đơn giản, nhưng alias có thể đụng mã tự sinh nếu hai không gian mã dùng chung định dạng.

Nguồn: MongoDB: The Definitive Guide 2e ↗ (.zip) · ch.5 Indexing · mục "Unique Indexes" (tr. 104) ; Grokking Modern System Design · ch.34 Design TinyURL ↗ · Requirements of TinyURL's Design · mục "Functional requirements" (Expiry time) ; Grokking Modern System Design · ch.34 Design TinyURL ↗ · Design and Deployment of TinyURL · mục "Workflow" (Deletion khi hết hạn)

Cách tốt nhất
Tách không gian alias + TTL ở cả DB và cache

Mã tự sinh luôn đúng 7 ký tự base62, alias bắt buộc có dấu gạch ngang hoặc độ dài khác, nên hai nhóm không bao giờ va nhau. Ghi bằng conditional write. TTL của bản ghi đặt theo expiresAt để DB tự dọn, TTL cache bằng min(1 giờ, thời gian còn lại). Không tái sử dụng mã đã hết hạn để link cũ trên mạng không trỏ sang nội dung lạ.

Đánh đổi: Không race và không cần job dọn lớn, đổi lại có quy tắc định dạng alias mà người dùng phải theo.

Nguồn: Grokking Modern System Design · ch.34 Design TinyURL ↗ · Requirements of TinyURL's Design · Quiz "we don't reuse the expired short URLs" ; Grokking Modern System Design · ch.34 Design TinyURL ↗ · Design and Deployment of TinyURL · mục "Workflow" (Custom short links, tối đa 11 ký tự) ; MongoDB: The Definitive Guide 2e ↗ (.zip) · ch.6 Special Index and Collection Types · mục "Time-To-Live Indexes" (tr. 114) ; Grokking Modern System Design · ch.16 Distributed Cache ↗ · Background of Distributed Cache · mục "Eviction policies" và "Cache invalidation"

Trong phỏng vấn

Nên nói
  • “Tôi chọn 302 để đếm được click và còn thu hồi được link, sau đó bù hiệu năng bằng Redis và CDN cache ngắn hạn.”
  • “Mã sinh từ bộ đếm cấp theo khoảng nên không bao giờ trùng, tôi xáo trộn ID trước khi base62 để không ai đoán được link kế tiếp.”
  • “Tỷ lệ đọc:ghi khoảng 100:1, nên mọi quyết định đều tối ưu đường redirect và đẩy ghi click ra khỏi đường đó.”
  • “Truy cập luôn theo code, nên khi lớn tôi chuyển sang key-value store phân mảnh theo hash(code).”
Bẫy hay gặp
  • Chọn 301 vì nhanh hơn mà quên rằng sẽ mất số liệu click và không thể xóa link
  • Dùng bộ đếm tăng dần rồi base62 trực tiếp, lộ tổng số link và cho phép duyệt toàn bộ link
  • Ghi click đồng bộ vào DB trên đường redirect
  • Kiểm tra alias bằng SELECT rồi INSERT, bị race khi hai người đặt cùng lúc

Đọc thêm

Sách trong thư viện của bạn. Số chương có thể lệch nhẹ so với bản PDF.