首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 短網址服務實作:Base62 編碼、資料庫儲存、重導向與擴充設計

     編輯:PHP 相關知識

短網址服務核心需求:長網址 → 短代碼 → 301/302 重導向。本文從演算法選型、資料庫設計、併發處理、快取策略、分析追蹤完整實作一個可上線的短網址服務核心,並說明為何不建議使用原文的 INI 檔案方案。

核心演算法:Base62 編碼 (自增 ID → 短碼)

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

資料庫設計 (MySQL)

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
    {
        // ... 同前
    }
}

控制器範例 (Slim/Laravel/原生)

<?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 檔案方案?

問題INI 方案資料庫方案
併發寫入parse_ini_file 非原子,極易資料遺失/損毀交易 + 行鎖保證一致性
查詢效能全量載入記憶體,O(N)索引 O(log N),支援百萬級
多伺服器部署檔案同步極其困難共享資料庫/叢集原生支援
過期/統計需掃描全檔索引 + 定時任務高效清理
安全性檔案權限風險資料庫帳號權限控制

擴充建議

  • 快取熱門短碼:Redis GET short:{code} 直接回傳長網址,減少 DB 壓力。
  • 自訂短碼:允許使用者指定 short_code,唯一鍵衝突時回傳錯誤。
  • 點擊分析:寫入 click_logs 表 (短碼、IP、UA、Referer、時間),定時聚合。
  • 防濫用:IP 限流、驗證碼、域名白名單。
  • 監控:Prometheus 指標:shorturl_created_total, shorturl_redirected_total, shorturl_latency_seconds。

短網址服務看似簡單,但涉及編碼演算法、併發一致性、儲存選型、快取策略、觀測性等工程細節。採用「資料庫 + Base62 + 非同步計數」架構,可支撐日均百萬請求的生產負載。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved