短網址本質是長 URL → 短代碼的映射。訪問短鏈時,服務端查找代碼對應的長 URL 並 301/302 重定向。核心挑戰:代碼唯一、長度極短(5-7 字符)、生成極快、抗衝突。
| 策略 | 原理 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|---|
| 自增 ID + Base62 | 資料庫自增主鍵 → Base62 編碼 | 極簡、零衝突、有序、可預測長度 | ID 可被遍歷猜測、洩露業務量 | 內部系統、非敏感鏈接 |
| 哈希/CRC32 + Base62 | crc32(url) 或 md5(url)[0:6] → Base62 |
無需資料庫協調、分散式友好 | 必然存在衝突、需處理碰撞 | 去重需求低、臨時鏈接 |
| 隨機字符串 + 唯一索引 | random_bytes(4) → Base62,DB 唯一索引重試 |
不可預測、衝突概率極低 | 高併發下重試開銷 | 公開短鏈服務(如 bit.ly) |
| Snowflake/ULID | 時間戳 + 機器 ID + 序列號 → Base62 | 全局唯一、粗略有序、無協調 | 代碼較長(10+ 字符) | 分散式高併發系統 |
<?php
namespace App\Utils;
class Base62
{
private const ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
private const BASE = 62;
private static array $decodeMap = [];
public static function encode(int $num): string
{
if ($num === 0) return '0';
$result = '';
while ($num > 0) {
$result = self::ALPHABET[$num % self::BASE] . $result;
$num = (int)($num / self::BASE);
}
return $result;
}
public static function decode(string $str): int
{
if (empty(self::$decodeMap)) {
foreach (str_split(self::ALPHABET) as $i => $ch) {
self::$decodeMap[$ch] = $i;
}
}
$num = 0;
foreach (str_split($str) as $ch) {
if (!isset(self::$decodeMap[$ch])) {
throw new \InvalidArgumentException("非法 Base62 字符: $ch");
}
$num = $num * self::BASE + self::$decodeMap[$ch];
}
return $num;
}
}
<?php
// 資料表
CREATE TABLE short_urls (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
long_url TEXT NOT NULL,
short_code VARCHAR(12) NOT NULL UNIQUE,
hits INT UNSIGNED DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
// 服務類
namespace App\Service;
use App\Utils\Base62;
use PDO;
class ShortUrlService
{
private PDO $db;
public function __construct(PDO $db) { $this->db = $db; }
public function create(string $longUrl): string
{
// 1. 可選:檢查是否已存在(去重)
$stmt = $this->db->prepare('SELECT short_code FROM short_urls WHERE long_url = ? LIMIT 1');
$stmt->execute([$longUrl]);
if ($code = $stmt->fetchColumn()) {
return $code;
}
// 2. 插入獲取自增 ID
$this->db->prepare('INSERT INTO short_urls (long_url) VALUES (?)')->execute([$longUrl]);
$id = $this->db->lastInsertId();
// 3. Base62 編碼
$code = Base62::encode($id);
// 4. 更新 short_code(也可用觸發器/生成列自動維護)
$this->db->prepare('UPDATE short_urls SET short_code = ? WHERE id = ?')->execute([$code, $id]);
return $code;
}
public function resolve(string $code): ?string
{
$stmt = $this->db->prepare('SELECT long_url FROM short_urls WHERE short_code = ?');
$stmt->execute([$code]);
$url = $stmt->fetchColumn();
if ($url) {
// 異步增加點擊數
$this->db->prepare('UPDATE short_urls SET hits = hits + 1 WHERE short_code = ?')->execute([$code]);
}
return $url ?: null;
}
}
<?php
namespace App\Service;
use App\Utils\Base62;
use PDO;
use RuntimeException;
class RandomShortUrlService
{
private PDO $db;
private int $codeLength = 6; // 62^6 ≈ 568 億,足夠大多數應用
public function __construct(PDO $db, int $codeLength = 6) {
$this->db = $db;
$this->codeLength = $codeLength;
}
public function create(string $longUrl, int $maxRetries = 5): string
{
// 去重:若業務允許同一長 URL 對應多個短碼,可移除此查詢
$stmt = $this->db->prepare('SELECT short_code FROM short_urls WHERE long_url = ? LIMIT 1');
$stmt->execute([$longUrl]);
if ($code = $stmt->fetchColumn()) return $code;
for ($i = 0; $i < $maxRetries; $i++) {
// 4 字節隨機數 → 62 進制,不足左補 0
$random = random_bytes(4);
$num = unpack('N', $random)[1]; // 大端序無符號整數
$code = str_pad(Base62::encode($num), $this->codeLength, '0', STR_PAD_LEFT);
try {
$this->db->beginTransaction();
$stmt = $this->db->prepare('INSERT INTO short_urls (long_url, short_code) VALUES (?, ?)');
$stmt->execute([$longUrl, $code]);
$this->db->commit();
return $code;
} catch (\PDOException $e) {
if ($e->getCode() === '23000') { // 唯一索引衝突
$this->db->rollBack();
continue; // 重試
}
throw $e;
}
}
throw new RuntimeException('短碼生成衝突過多,請增加 codeLength');
}
public function resolve(string $code): ?string
{
$stmt = $this->db->prepare('SELECT long_url FROM short_urls WHERE short_code = ?');
$stmt->execute([$code]);
return $stmt->fetchColumn() ?: null;
}
}
server {
listen 80;
server_name short.example.com;
root /var/www/short-url/public;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# 短碼路由:/abc123 -> /index.php?code=abc123
location ~ ^/([A-Za-z0-9]{4,12})$ {
rewrite ^/([A-Za-z0-9]{4,12})$ /index.php?code=$1 last;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
<?php
require __DIR__ . '/../vendor/autoload.php';
$code = $_GET['code'] ?? '';
if (!$code) {
// 首頁:提交表單生成短鏈
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$url = trim($_POST['url'] ?? '');
if (filter_var($url, FILTER_VALIDATE_URL)) {
$service = new App\Service\RandomShortUrlService($pdo);
$code = $service->create($url);
echo "短網址: https://short.example.com/$code";
} else {
echo "無效的 URL";
}
}
exit;
}
$service = new App\Service\RandomShortUrlService($pdo);
$longUrl = $service->resolve($code);
if ($longUrl) {
header("Location: $longUrl", true, 301);
exit;
}
http_response_code(404);
echo "短鏈不存在";
INCR short:url:{code}:hits 定期持久化到 MySQL。expires_at,解析時檢查;或用 Redis TTL 自動清理。password、allow_ips、referer_whitelist 欄位。custom_code,驗證正則 ^[A-Za-z0-9_-]{4,20}$ 並檢查唯一性。短網址系統看似簡單,實則涉及編碼選型、衝突處理、高併發寫入、重定向性能、緩存策略、安全防刷等工程細節。建議:起步用自增 ID + Base62,成長期遷移至隨機代碼 + Redis 緩存,大規模採用 Snowflake/ULID。核心代碼僅 50 行,其餘皆為運維與擴展。