短網址服務核心需求:長網址 → 短代碼 → 301/302 重導向。本文從演算法選型、資料庫設計、併發處理、快取策略、分析追蹤完整實作一個可上線的短網址服務核心,並說明為何不建議使用原文的 INI 檔案方案。
const BASE62_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
function encodeBase62(int $num): string
{
if ($num === 0) return '0';
$result = '';
while ($num > 0) {
$result = BASE62_CHARS[$num % 62] . $result;
$num = (int)($num / 62);
}
return $result;
}
function decodeBase62(string $code): int
{
$num = 0;
$len = strlen($code);
for ($i = 0; $i < $len; $i++) {
$num = $num * 62 + strpos(BASE62_CHARS, $code[$i]);
}
return $num;
}
// 測試
echo encodeBase62(123456789); // "8M0k"
echo decodeBase62('8M0k'); // 123456789
CREATE TABLE `short_urls` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`long_url` TEXT NOT NULL,
`short_code` VARCHAR(12) NOT NULL,
`creator_ip` VARBINARY(16) DEFAULT NULL, -- IPv6 binary
`clicks` INT UNSIGNED NOT NULL DEFAULT 0,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`expires_at` TIMESTAMP NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_short_code` (`short_code`),
KEY `idx_creator_ip` (`creator_ip`),
KEY `idx_expires` (`expires_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
class ShortUrlService
{
private PDO $pdo;
private string $domain; // 如 'https://s.ly'
public function __construct(PDO $pdo, string $domain)
{
$this->pdo = $pdo;
$this->domain = rtrim($domain, '/');
}
// 建立短網址 (冪等:相同長網址回傳同一短碼)
public function create(string $longUrl, string $creatorIp = '', ?int $ttlDays = null): array
{
// 1. 正規化 URL
$longUrl = $this->normalizeUrl($longUrl);
if (!filter_var($longUrl, FILTER_VALIDATE_URL)) {
throw new InvalidArgumentException('Invalid URL');
}
// 2. 檢查是否已存在 (使用 SELECT ... FOR UPDATE 避免併發重複)
$stmt = $this->pdo->prepare("SELECT id, short_code FROM short_urls WHERE long_url = ? FOR UPDATE");
$stmt->execute([$longUrl]);
if ($row = $stmt->fetch()) {
return ['short_url' => $this->domain . '/' . $row['short_code'], 'code' => $row['short_code']];
}
// 3. 插入新紀錄,取得自增 ID
$expires = $ttlDays ? date('Y-m-d H:i:s', time() + $ttlDays * 86400) : null;
$stmt = $this->pdo->prepare(
"INSERT INTO short_urls (long_url, creator_ip, expires_at) VALUES (?, ?, ?)"
);
$stmt->execute([$longUrl, inet_pton($creatorIp), $expires]);
$id = $this->pdo->lastInsertId();
// 4. 編碼 ID 為短碼,更新
$code = $this->encodeBase62($id);
$stmt = $this->pdo->prepare("UPDATE short_urls SET short_code = ? WHERE id = ?");
$stmt->execute([$code, $id]);
return ['short_url' => $this->domain . '/' . $code, 'code' => $code];
}
// 重導向並記錄點擊 (非同步寫入點擊數)
public function redirect(string $code): ?string
{
$stmt = $this->pdo->prepare("SELECT long_url FROM short_urls WHERE short_code = ? AND (expires_at IS NULL OR expires_at > NOW())");
$stmt->execute([$code]);
$url = $stmt->fetchColumn();
if ($url) {
// 非同步增加點擊數 (可用 Redis 隊列或 UPDATE ... clicks = clicks + 1)
$this->pdo->prepare("UPDATE short_urls SET clicks = clicks + 1 WHERE short_code = ?")->execute([$code]);
return $url;
}
return null;
}
private function normalizeUrl(string $url): string
{
$url = trim($url);
if (!preg_match('~^https?://~i', $url)) {
$url = 'https://' . $url;
}
return $url;
}
private function encodeBase62(int $num): string
{
// ... 同前
}
}
<?php
// POST /api/shorten
$input = json_decode(file_get_contents('php://input'), true);
$longUrl = $input['url'] ?? '';
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
try {
$result = $service->create($longUrl, $ip);
header('Content-Type: application/json');
echo json_encode(['success' => true, 'data' => $result]);
} catch (Exception $e) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
// GET /{code}
$code = $_GET['code'] ?? '';
$url = $service->redirect($code);
if ($url) {
header("Location: $url", true, 301); // 永久重導向利於 SEO
exit;
}
http_response_code(404);
echo 'Short URL not found or expired.';
| 問題 | INI 方案 | 資料庫方案 |
|---|---|---|
| 併發寫入 | parse_ini_file 非原子,極易資料遺失/損毀 | 交易 + 行鎖保證一致性 |
| 查詢效能 | 全量載入記憶體,O(N) | 索引 O(log N),支援百萬級 |
| 多伺服器部署 | 檔案同步極其困難 | 共享資料庫/叢集原生支援 |
| 過期/統計 | 需掃描全檔 | 索引 + 定時任務高效清理 |
| 安全性 | 檔案權限風險 | 資料庫帳號權限控制 |
GET short:{code} 直接回傳長網址,減少 DB 壓力。short_code,唯一鍵衝突時回傳錯誤。click_logs 表 (短碼、IP、UA、Referer、時間),定時聚合。shorturl_created_total, shorturl_redirected_total, shorturl_latency_seconds。短網址服務看似簡單,但涉及編碼演算法、併發一致性、儲存選型、快取策略、觀測性等工程細節。採用「資料庫 + Base62 + 非同步計數」架構,可支撐日均百萬請求的生產負載。