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

PHP 短網址生成實戰:Base62 編碼、CRC32/哈希策略、防衝突與擴展設計

     編輯:PHP 相關知識

短網址核心原理

短網址本質是長 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+ 字符) 分散式高併發系統

Base62 編碼實現(通用工具)

<?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;
    }
}

方案一:自增 ID + Base62(推薦起步)

<?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;
    }
}

Nginx 重寫規則(隱藏 index.php)

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;
    }
}

index.php 入口

<?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 "短鏈不存在";

擴展:統計、過期、權限

  • 點擊統計:Redis 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 行,其餘皆為運維與擴展。

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