引言
自建短链 = 域名自主 + 数据自有 + 成本可控
对于年链接分发量超过 100 万次的团队,商业短链服务的订阅费用会快速累积(Bitly Enterprise 年费 $5000+)。而开源自托管方案,不仅成本仅为云服务器费用,还能获得完整的数据所有权和品牌独立性。
本文对比4种主流自托管方案,从功能、技术栈、部署难度、维护成本四个维度分析,帮你做出正确的选择。
一、四种方案速览
| 方案 | 技术栈 | 首次发布 | 活跃维护 | 亮点 | 适合 |
|---|
| Shlink | PHP/Swoole + MySQL | 2016 | ✅ 极度活跃 | 功能最全、API丰富 | 企业级/开发者 |
| YOURLS | PHP + MySQL | 2009 | ✅ 稳定维护 | 经典、插件生态成熟 | WordPress/PHP团队 |
| Polr | PHP + MySQL | 2015 | ⚠️ 半维护 | 极简界面、轻量 | 快速上线/小团队 |
| Supabase+自研 | Postgres + Edge Functions | 2022 | ✅ 生态活跃 | 无服务器、按需付费 | 技术团队/云原生 |
二、Shlink:现代短链的标杆
核心特性
- 双模式运行:传统 PHP-FPM 或 Swoole 高性能模式
- REST API + CLI:完整的程序化接口
- 多域名支持:同一实例服务多个自定义域名
- GEO 定位:MaxMind 数据库精准定位
- QR 码生成:原生支持动态二维码
- 标签系统:链接分类管理
- 访问规则:基于地理位置/时间/设备的条件跳转 参见 AI智能跳转
Docker 一键部署
# docker-compose.yml
version: '3'
services:
shlink:
image: shlinkio/shlink:stable
restart: always
ports:
- "8080:8080"
environment:
- DEFAULT_DOMAIN=go.yourdomain.com
- IS_HTTPS_ENABLED=true
- GEOLITE_LICENSE_KEY=${GEOLITE_KEY}
- DB_DRIVER=mysql
- DB_NAME=shlink
- DB_USER=shlink
- DB_PASSWORD=${DB_PASSWORD}
- DB_HOST=database
depends_on:
- database
database:
image: mysql:8.0
restart: always
environment:
- MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
- MYSQL_DATABASE=shlink
- MYSQL_USER=shlink
- MYSQL_PASSWORD=${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
启动:
docker-compose up -d
# 生成初始 API Key
docker exec shlink shlink api-key:generate
API 使用示例
# 创建短链
curl -X POST https://go.yourdomain.com/rest/v3/short-urls \
-H "X-Api-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"longUrl": "https://example.com/very/long/path",
"customSlug": "launch2026",
"tags": ["campaign", "spring"],
"findIfExists": true
}'
# 获取点击统计
curl https://go.yourdomain.com/rest/v3/short-urls/launch2026/visits \
-H "X-Api-Key: YOUR_KEY"
访问规则配置(YAML)
# config/redirect-rules.yml
rules:
- shortCode: sale2026
conditions:
- type: geo_country
match: CN
target: https://example.com/cn-sale
- type: device_type
match: mobile
target: https://m.example.com/sale
default: https://example.com/sale
成本估算(月)
| 组件 | 阿里云/腾讯云 | AWS |
|---|
| ECS/EC2 (2C4G) | ¥150-200 | $20-30 |
| RDS/MySQL基础版 | ¥100-150 | $15-25 |
| 带宽 1TB | 包含 | ~$90 |
| 总计 | ¥250-350 | $35-55 + 带宽 |
三、YOURLS:经典 PHP 的稳定之选
核心特性
- 极简哲学:只做短链核心(创建、跳转、统计)
- 插件系统:100+ 社区插件
- 公共/私有模式:支持公共创建或仅管理员
- Bookmarklet:浏览器一键缩短
- JSONP API:跨域调用
Docker 部署
version: '3'
services:
yourls:
image: yourls:latest
restart: always
ports:
- "8080:80"
environment:
- YOURLS_DB_HOST=database
- YOURLS_DB_USER=yourls
- YOURLS_DB_PASS=${DB_PASSWORD}
- YOURLS_DB_NAME=yourls
- YOURLS_SITE=https://go.yourdomain.com
- YOURLS_USER=admin
- YOURLS_PASS=${ADMIN_PASSWORD}
depends_on:
- database
database:
image: mysql:5.7
restart: always
environment:
- MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
- MYSQL_DATABASE=yourls
- MYSQL_USER=yourls
- MYSQL_PASSWORD=${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
必装插件推荐
| 插件 | 功能 |
|---|
| YOURLS-Import-Export | 批量导入/导出链接 |
| Random-Shorturls | 随机短码代替顺序递增 |
| Change-Title | 自定义短链标题(SEO) |
| QRCode-ShortURL | 二维码生成 |
| Better-Analytics | 增强统计数据 |
适用场景
- 已有 PHP/MySQL 技术栈的团队
- WordPress 生态用户(同一主机托管)
- 对功能要求不高,只要"稳定可用"
- 10人以下小团队,月点击量 < 500万
四、Polr:极简轻量的选择
核心特性
- Material Design UI:现代化界面
- 轻量部署:单 PHP 文件即可运行
- 无依赖缓存:文件缓存无需 Redis
部署(共享主机也支持)
git clone https://github.com/Cydrobolt/polr.git
cd polr
composer install --no-dev
php artisan migrate
php artisan key:generate
⚠️ 注意事项
- 最近一次发布为 2020 年,社区维护力度下降
- 功能相对单一,无 API 认证机制
- 建议:仅用于个人或极小团队临时需求
五、Supabase + 自研:云原生方案
架构设计
┌─────────────┐ ┌─────────────────┐ ┌──────────────┐
│ Client │────▶│ Supabase Edge │────▶│ Postgres │
│ │◀────│ Function │◀────│ (RLS) │
└─────────────┘ └─────────────────┘ └──────────────┘
│
┌──────┴──────┐
▼ ▼
Upstash Redis Custom Domain
(Rate Limit) (CNAME + SSL)
数据库表结构
-- 启用 Row Level Security
CREATE TABLE short_links (
id BIGSERIAL PRIMARY KEY,
slug VARCHAR(16) UNIQUE NOT NULL,
target_url TEXT NOT NULL,
title VARCHAR(200),
clicks BIGINT DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
expires_at TIMESTAMPTZ,
is_active BOOLEAN DEFAULT true,
tenant_id UUID REFERENCES auth.users(id)
);
-- RLS 策略:用户只能查看自己的链接
ALTER TABLE short_links ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can only access own links"
ON short_links
FOR ALL
USING (tenant_id = auth.uid());
-- 统计表(时序)
CREATE TABLE click_events (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
link_id BIGINT REFERENCES short_links(id),
ip_hash VARCHAR(64),
country_code CHAR(2),
city VARCHAR(100),
user_agent TEXT,
referrer TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Edge Function(跳转处理)
// supabase/functions/redirect/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
serve(async (req) => {
const slug = new URL(req.url).pathname.replace('/', '')
const supabase = createClient(Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_ANON_KEY')!)
// 1. 从数据库获取目标链接
const { data: link } = await supabase
.from('short_links')
.select('target_url, is_active, expires_at')
.eq('slug', slug)
.single()
if (!link || !link.is_active || (link.expires_at && new Date(link.expires_at) < new Date())) {
return new Response('Not Found', { status: 404 })
}
// 2. 异步记录点击(不阻塞跳转)
const eventData = {
link_id: slug,
ip_hash: await sha256(req.headers.get('x-forwarded-for') || ''),
country_code: req.headers.get('cf-ipcountry'),
referrer: req.headers.get('referer'),
user_agent: req.headers.get('user-agent'),
}
// Edge Function 结束后异步触发
supabase.from('click_events').insert(eventData).then(() => {})
// 3. 302 临时跳转或 301 永久跳转
return new Response(null, {
status: 301,
headers: {
'Location': link.target_url,
'Cache-Control': 'public, max-age=3600',
},
})
})
async function sha256(str: string): Promise<string> {
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str))
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('')
}
创建短链 API
// supabase/functions/create-link/index.ts
serve(async (req) => {
const { target_url, custom_slug, expires_in_days } = await req.json()
// 生成 slug
const slug = custom_slug || await generateUniqueSlug()
const { data, error } = await supabase
.from('short_links')
.insert({
slug,
target_url,
expires_at: expires_in_days ? new Date(Date.now() + expires_in_days * 86400000) : null,
tenant_id: user.id,
})
.select()
.single()
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' },
})
})
async function generateUniqueSlug(): Promise<string> {
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
while (true) {
const slug = Array.from({ length: 6 }, () => chars[Math.floor(Math.random() * 62)]).join('')
const { count } = await supabase.from('short_links').select('*', { count: 'exact' }).eq('slug', slug)
if (count === 0) return slug
}
}
成本估算(月,Supabase 免费档起步)
| 档位 | 费用 | 配置 | 适合 |
|---|
| Free | $0 | 500MB DB, 2GB 带宽, 50万 Edge Function 调用 | 起步/验证 |
| Pro | $25 | 8GB DB, 100GB 带宽, 200万调用 | < 1000万点击/月 |
| Team | $599 | 更大规格 + 支持 | 企业级 |
六、四方案横向深度对比
功能矩阵
| 功能 | Shlink | YOURLS | Polr | Supabase自研 |
|---|
| 自定义 slug | ✅ | ✅ | ✅ | ✅ |
| 密码保护链接 | ✅ | 插件 | ❌ | 需自建 |
| 二维码生成 | ✅ | 插件 | ❌ | 需自建 |
| GEO 跳转规则 | ✅ | ❌ | ❌ | ✅ |
| 时间到期 | ✅ | 插件 | ❌ | ✅ |
| 访问统计 | 丰富 | 基础 | 基础 | 需自建 |
| REST API | 完善 | 基础 | ❌ | 自定义 |
| 多租户/多域名 | ✅ | ❌ | ❌ | ✅ |
| 批量操作 | CLI/API | 插件 | ❌ | 自定义 |
| 标签/分类 | ✅ | ✅ | ❌ | 需自建 |
技术栈对比
| 维度 | Shlink | YOURLS | Polr | Supabase自研 |
|---|
| 学习成本 | 中 | 低 | 低 | 中高 |
| 部署难度 | 中(Docker) | 低 | 极低 | 中 |
| 扩展性 | 高 | 中 | 低 | 极高 |
| 维护负担 | 低 | 低 | 中(维护弱) | 中 |
| 数据主权 | ✅ | ✅ | ✅ | ✅ |
| 无服务器 | ❌ | ❌ | ❌ | ✅ |
性能对比(估算,2C4G 服务器)
| 方案 | 单机 QPS | 十万次点击响应 | 备注 |
|---|
| Shlink + Swoole | ~3000 | < 50ms | Swoole 协程模式 |
| YOURLS + OPcache | ~1000 | ~100ms | PHP-FPM 模式 |
| Polr | ~800 | ~120ms | 无缓存层 |
| Supabase Edge | ~5000 | < 30ms | 全球 CDN 边缘节点 |
七、数据迁移:从商业服务回来自托管
Bitly → Shlink 迁移脚本
# migrate_bitly_to_shlink.py
import requests
BITLY_TOKEN = "your_bitly_token"
SHLINK_URL = "https://go.yourdomain.com"
SHLINK_KEY = "your_shlink_api_key"
def fetch_bitly_links():
links = []
url = "https://api-ssl.bitly.com/v4/groups/default/bitlinks"
headers = {"Authorization": f"Bearer {BITLY_TOKEN}"}
while url:
resp = requests.get(url, headers=headers)
data = resp.json()
links.extend(data['links'])
url = data.get('pagination', {}).get('next')
return links
def import_to_shlink(bitly_link):
payload = {
"longUrl": bitly_link['long_url'],
"customSlug": bitly_link['id'].split('/')[-1],
"tags": bitly_link.get('tags', []),
}
resp = requests.post(
f"{SHLINK_URL}/rest/v3/short-urls",
headers={"X-Api-Key": SHLINK_KEY, "Content-Type": "application/json"},
json=payload
)
return resp.json()
if __name__ == "__main__":
links = fetch_bitly_links()
for link in links:
result = import_to_shlink(link)
print(f"Imported: {link['id']} → {result.get('shortUrl')}")
迁移检查清单
八、选型决策树
需要自托管短链?
│
┌─────────────┴─────────────┐
▼ ▼
团队有PHP经验? 偏好云原生/JS栈?
│ │
┌─────┴─────┐ │
▼ ▼ ▼
是 否 选择Supabase自研
│ │ │
▼ ▼ │
需要丰富 快速上线 │
功能? 即可? │
│ │ │
┌───┴───┐ ▼ │
是 否 选YOURLS(简单稳定) │
│ │ │
▼ ▼ │
Shlink Polr │
(企业级) (极简) │
九、安全加固必读
必做安全设置
- HTTPS 强制:所有短链域名配置 HSTS
- SQL 注入防护:使用 ORM/参数化查询(上述方案均已内置)
- XSS 防护:过滤用户输入的自定义 slug
- 速率限制:
- 创建接口:10次/分钟/IP
- 跳转接口:1000次/分钟/IP
- Referrer 策略:
Referrer-Policy: strict-origin-when-cross-origin - Open Redirect 防护:验证目标域名白名单 参见 短链安全防线
Docker 安全建议
# 安全加固版 docker-compose
services:
shlink:
image: shlinkio/shlink:stable
read_only: true # 只读根目录
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp:noexec,nosuid,size=100m
# 不使用 root 运行
user: "1000:1000"
十、总结
| 你的情况 | 推荐方案 | 理由 |
|---|
| 企业级需求,团队有运维能力 | Shlink | 功能最全,API 完善,社区活跃 |
| WordPress/PHP 团队,简单需求 | YOURLS | 稳定成熟,插件丰富,低维护 |
| 极小团队,临时需求 | Polr | 部署最快,但注意维护风险 |
| 云原生团队,JS/TS 技术栈 | Supabase 自研 | 无服务器架构,按需扩展,数据自主 |
| 已有云服务器(阿里云/AWS) | Shlink/YOURLS | 利用现有基础设施 |
| 零服务器,纯前端团队 | Supabase Free | $0 启动,验证后再扩展 |
自托管短链的核心价值不仅是省钱,而是将链接这个关键基础设施掌握在自己手中。当你的品牌每年通过短链产生千万次触达时,拥有完整的数据和可控的体验,是任何商业 SaaS 难以替代的竞争优势。
相关阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。