企业级短链系统设计:高并发、多域名治理、风控与容灾
短链系统看起来只是“根据短码查询长链接并返回 HTTP 重定向”,但进入企业场景后,它会同时面对高并发读、热点链接、域名迁移、租户隔离、链接修改、实时下线、恶意链接、统计分析和多地域容灾。
本文中的“多域名切换”用于业务连续性、品牌迁移、客户自定义域名和风险隔离,不用于通过伪装页面、差异化跳转或快速域名轮换绕过平台审核与安全拦截。
一、业务背景
用户需要将较长的业务地址转换为便于分享的短链接:
1 | |
企业短链通常用于:
- 短信、邮件、企业微信、公众号和 App Push;
- 营销活动、优惠券、邀请注册与渠道投放;
- 文件、订单、账单和审批页面分享;
- 二维码生成;
- 内部系统临时访问链接;
- 多租户 SaaS 的客户品牌域名;
- 域名升级、品牌迁移和故障切换。
真正的难点不是生成一个短字符串,而是解决:
- 跳转请求远多于创建请求,如何支撑十万级甚至更高峰值 QPS;
- 热门活动形成单短码热点,如何避免 Redis 热 Key 和数据库击穿;
- 目标地址允许修改时,如何让缓存快速且可靠地失效;
- 域名、DNS、证书或外部信誉发生异常时,如何进行合规切换;
- 如何防止钓鱼、恶意软件、开放重定向和 SSRF;
- 如何在不影响跳转性能的情况下完成点击分析;
- 多租户自定义域名如何验证所有权、签发证书并隔离数据;
- Redis、数据库、消息队列或地域故障时如何降级。
二、多域名的现实边界
必须先明确一个事实:
已经分享出去的 URL,其域名已经固化在文本、二维码或消息中。后台切换默认域名,只能影响之后生成的新链接,不能让旧 URL 自动变成另一个域名。
例如用户已经分享:
1 | |
系统后来把推荐域名切换为:
1 | |
已经发出去的 a.example 仍需要能够完成 DNS 解析、TLS 握手和 HTTP 请求,旧链接才能继续工作。
多域名设计能够解决:
- 新链接不再使用异常域名;
- 同一短码可在同一域名组的多个域名上解析;
- 原域名仍可访问时,旧链接继续直接跳转;
- 后台、App 或消息模板重新获取当前推荐分享链接;
- 不同租户、渠道和业务线进行域名信誉隔离;
- 品牌迁移时灰度切换,而不是一次性硬切。
多域名设计不能保证:
- 恢复一个已经无法解析、证书失效或被客户端彻底拦截的旧域名;
- 绕过平台对最终落地页、内容、账号和行为模式的治理;
- 通过频繁换域名永久保持可用;
- 对审核机器人展示安全页、对真实用户展示另一页面。
合理路线应是:内容合规、域名信誉治理、租户隔离、投诉处理、备份域名和故障容灾。域名轮换只是治理手段,不是治理替代品。
三、需求分析
3.1 功能需求
| 能力 | 说明 |
|---|---|
| 创建短链 | 将长链接转换为短码 |
| 自定义短码 | 企业客户可申请业务别名 |
| 修改目标地址 | 短码不变,目标 URL 可版本化修改 |
| 启用与停用 | 支持立即下线、恢复和定时生效 |
| 有效期 | 永久、固定时间过期或一次性访问 |
| 多租户 | 数据、配额、域名和统计隔离 |
| 多域名 | 平台域名、业务域名和客户自定义域名 |
| 域名组 | 同一短码可在组内多个域名解析 |
| 分享链接刷新 | 返回当前健康的推荐域名 |
| 访问统计 | PV、UV、渠道、地域、设备和 Referer |
| 风险检测 | 钓鱼、恶意软件、违规域名和开放重定向 |
| 审核与投诉 | 人工审核、申诉、下线和审计 |
| 二维码 | 使用当前推荐域名生成二维码 |
| 批量创建 | 营销和通知场景批量生成 |
| 事件通知 | 创建、修改、过期和封禁事件 |
3.2 非功能需求
以下以一个中大型系统作为示例基线:
| 指标 | 示例目标 |
|---|---|
| 累计短链数量 | 1 亿 |
| 每日跳转量 | 10 亿次 |
| 平均跳转 QPS | 约 11,600 |
| 10 倍峰值 QPS | 约 116,000 |
| 创建峰值 QPS | 5,000 |
| 跳转接口可用性 | 99.99% |
| Redirect Service P99 | 30 ms 以内,不含目标站点 |
| 普通修改生效时间 | 5 秒以内 |
| 紧急下线生效时间 | 1 秒级 |
| 核心配置 RPO | 0 或接近 0 |
| 统计数据 RPO | 允许秒级或分钟级 |
| 单地域故障 RTO | 5 分钟以内 |
日均 QPS:
1 | |
营销活动、批量推送和热点事件往往产生几十倍瞬时流量,因此不能只按日均容量设计。
四、总体架构
短链系统建议拆成两个平面:
- 数据平面:负责高性能跳转,路径短、依赖少、无状态;
- 控制平面:负责创建、修改、域名、风控、审核和运营。
flowchart LR
subgraph Client[访问端]
Browser[浏览器 / App]
Biz[业务系统]
Console[运营控制台]
end
subgraph Edge[接入与边缘层]
DNS[权威 DNS / GSLB]
CDN[CDN / WAF / Anti-DDoS]
Gateway[API Gateway]
end
subgraph DataPlane[数据平面]
Redirect[Redirect Service]
L1[L1 本地缓存 Caffeine]
Redis[L2 Redis Cluster]
LinkDB[(短链分片数据库)]
end
subgraph ControlPlane[控制平面]
LinkAdmin[Link Management Service]
DomainAdmin[Domain Control Plane]
Risk[URL Risk Service]
Config[配置中心]
Scheduler[过期与巡检任务]
end
subgraph Analytics[事件与分析]
MQ[Kafka / Pulsar]
Stream[Flink / Kafka Streams]
OLAP[(ClickHouse / Doris)]
Lake[(对象存储 / Data Lake)]
end
Browser --> DNS --> CDN --> Redirect
Biz --> Gateway --> LinkAdmin
Console --> Gateway
Gateway --> DomainAdmin
Redirect --> L1 --> Redis --> LinkDB
LinkAdmin --> Risk
LinkAdmin --> LinkDB
LinkAdmin --> Redis
DomainAdmin --> Config
Config --> Redirect
Scheduler --> LinkDB
Scheduler --> DomainAdmin
Redirect -.异步点击事件.-> MQ
LinkAdmin -.配置事件.-> MQ
MQ --> Stream
Stream --> OLAP
Stream --> Lake
核心原则:
- 跳转服务无状态,支持水平扩容;
- 跳转链路不做同步统计写入;
- 域名选择发生在创建或刷新分享链接时,不在每次点击时随机选择;
- 链接配置与点击事件使用不同存储;
- 控制平面故障时,已有短链应尽量继续可用;
- 数据库故障时,缓存中的热门链接继续服务;
- 风控失败时,高风险新链接应失败关闭或进入审核。
五、核心领域模型
短链、域名组和具体域名必须解耦。
erDiagram
TENANT ||--o{ LINK : owns
TENANT ||--o{ DOMAIN_GROUP : owns
DOMAIN_GROUP ||--o{ SHORT_DOMAIN : contains
DOMAIN_GROUP ||--o{ LINK : resolves
LINK ||--o{ LINK_TARGET_VERSION : versions
LINK ||--o{ LINK_AUDIT_LOG : audits
TENANT {
bigint tenant_id PK
varchar tenant_name
varchar status
int risk_level
}
DOMAIN_GROUP {
bigint domain_group_id PK
bigint tenant_id
varchar group_name
varchar namespace_mode
varchar status
}
SHORT_DOMAIN {
bigint domain_id PK
bigint domain_group_id
varchar hostname
varchar status
int weight
varchar cert_status
varchar dns_status
varchar reputation_status
}
LINK {
bigint link_id PK
bigint tenant_id
bigint domain_group_id
varchar short_code
text target_url
varchar status
varchar risk_status
bigint version
timestamp effective_at
timestamp expires_at
}
LINK_TARGET_VERSION {
bigint version_id PK
bigint link_id
bigint version
text target_url
bigint operator_id
timestamp created_at
}
LINK_AUDIT_LOG {
bigint audit_id PK
bigint link_id
varchar action
bigint operator_id
timestamp created_at
}
5.1 域名组设计
假设域名组 marketing-cn 包含:
1 | |
短码 A8kP3xQm 绑定到域名组,而不是绑定到具体域名。下面三个 URL 可解析到同一记录:
1 | |
推荐唯一约束:
1 | |
Redirect Service 收到请求后:
1 | |
优势:
- 域名组内更换推荐域名时短码不变;
- 新旧域名可以并行工作;
- 不同租户可复用相同短码;
- 可按租户、业务和风险级别隔离域名;
- 域名退役无需批量修改短链主表。
5.2 数据表设计
1 | |
六、短码生成方案
6.1 常见方案
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 数据库自增 ID + Base62 | 简单、无碰撞、长度短 | 暴露增长趋势,序列可能成为瓶颈 | 中小规模 |
| 号段模式 + Base62 | 高吞吐、无碰撞 | 仍可枚举,需要打乱 | 企业内部系统 |
| Snowflake + Base62 | 本地生成、分布式吞吐高 | 偏长,依赖时钟和机器号 | 分布式创建 |
| 安全随机数 + 唯一索引 | 难枚举,不暴露业务量 | 有碰撞概率,需要重试 | 公共短链 |
| 预生成 Key 池 | 创建速度快 | 维护复杂,需要库存补充 | 超大规模营销 |
| URL 哈希截断 | 相同 URL 可复用 | 截断碰撞,修改语义复杂 | 去重辅助 |
Base62 字符集通常为:
1 | |
理论空间:
| 长度 | 理论空间 |
|---|---|
| 6 | 56,800,235,584 |
| 7 | 3,521,614,606,208 |
| 8 | 218,340,105,584,896 |
| 10 | 839,299,365,868,340,224 |
6.2 推荐方案
公共短链推荐:
1 | |
建议长度 8~10 位:
- 使用密码学安全随机数生成器;
- 映射到 Base62 或去除易混淆字符后的 Base58;
- 插入数据库;
- 命中唯一索引冲突后重试;
- 限制最大重试次数并告警。
企业内部系统可使用:
1 | |
不要直接把连续自增 ID 编成 Base62 暴露到公网,否则容易被枚举,并泄露增长趋势。
6.3 自定义短码
- 长度 4~32;
- 仅允许字母、数字、短横线和下划线;
- 保留
api、admin、login、health等关键字; - 大小写规则全局统一;
- 进行敏感词与品牌词检查;
- 高价值短码需要审批;
- 删除后进入冷却期;
- 自定义短码不能绕过 URL 风险检测。
七、创建短链流程
sequenceDiagram
autonumber
participant C as 业务系统
participant G as API Gateway
participant L as Link Service
participant D as Domain Service
participant R as Risk Service
participant DB as Link DB
participant Cache as Redis
participant MQ as Kafka
C->>G: POST /api/v1/links + Idempotency-Key
G->>G: 鉴权、租户配额、限流
G->>L: 创建请求
L->>L: URL 规范化与参数校验
L->>D: 获取域名组和推荐域名
D-->>L: domainGroupId + hostname
L->>R: 检测目标地址与租户风险
R-->>L: ALLOW / REVIEW / BLOCK
alt 风险阻断
L-->>C: 403 / 422
else 允许创建
L->>L: 生成短码
L->>DB: 幂等写入 + 唯一约束
DB-->>L: linkId + version
L->>Cache: 写入或删除缓存
L->>MQ: 发布 LinkCreated 事件
L-->>C: 返回短链接
end
7.1 创建 API
1 | |
1 | |
响应:
1 | |
7.2 创建幂等
使用:
1 | |
幂等记录保存请求摘要、执行状态、返回结果和过期时间。相同幂等键对应不同请求体时直接拒绝,避免调用方错误复用。
八、跳转链路
sequenceDiagram
autonumber
participant U as 用户
participant E as CDN/WAF
participant S as Redirect Service
participant L1 as Local Cache
participant R as Redis
participant DB as Link DB
participant MQ as Kafka
U->>E: GET https://s.example.com/A8kP3xQm
E->>S: 转发 Host + Path
S->>S: 校验 Host、短码格式与限流
S->>L1: 查询 groupId + code
alt L1 命中
L1-->>S: LinkSnapshot
else L1 未命中
S->>R: GET sl:{groupId}:{code}
alt Redis 命中
R-->>S: LinkSnapshot
S->>L1: 回填短 TTL
else Redis 未命中
S->>DB: 按分片查询
alt 数据存在
DB-->>S: LinkRecord
S->>R: 回填缓存 + 随机 TTL
S->>L1: 回填缓存
else 数据不存在
DB-->>S: Not Found
S->>R: 写入短期空值
end
end
end
S->>S: 检查状态、时间、风险和版本
S-->>U: 302 Location: targetUrl
S--)MQ: 异步发送点击事件
8.1 Host 必须参与解析
系统不能只根据 Path 查询,否则任何指向同一服务的未知域名都可能成为入口。
正确过程:
1 | |
未知 Host 返回 421 或统一 404。
8.2 为什么默认使用 302
允许修改目标地址的短链推荐返回:
1 | |
原因:
- 301 和 308 表示永久重定向,客户端可能长期缓存;
- 永久缓存后,后台修改目标地址可能无法及时生效;
- 302 更适合可修改、可停用和需要统计的短链;
- 307 会严格保留请求方法,但普通短链应只接受 GET/HEAD。
| 场景 | 状态码 |
|---|---|
| 可编辑营销短链 | 302 |
| 临时迁移且需要保留方法 | 307 |
| 永久且不可修改映射 | 301 或 308 |
| 风险下线 | 404、410 或风险提示页 |
| 已过期 | 410 Gone 或业务过期页 |
不要依赖浏览器缓存重定向提升性能,应在 Edge、Redirect Service 本地缓存和 Redis 中缓存映射。
8.3 禁止开放重定向
危险接口:
1 | |
安全接口:
1 | |
目标地址只能来自已审核、已授权的数据库记录,跳转请求不能临时传入任意 URL。
九、缓存设计
推荐三级缓存:
flowchart LR
Request[跳转请求] --> Edge[Edge KV / CDN Worker]
Edge --> L1[L1 Caffeine 10~60 秒]
L1 --> L2[L2 Redis 分钟~小时]
L2 --> DB[(分片数据库)]
9.1 L1 本地缓存
- 缓存最热短码;
- 降低 Redis 热 Key;
- TTL 10~60 秒;
- 限制最大容量;
- 通过事件主动失效;
- 依靠短 TTL 最终收敛。
缓存键:
1 | |
缓存值应为不可变快照:
1 | |
9.2 Redis 缓存
Key:
1 | |
策略:
- 正常链接 TTL 为 1~24 小时;
- 永久链接也设置最大 TTL;
- TTL 加随机抖动;
- 不存在短码缓存 30~300 秒空值;
- 紧急封禁使用优先级更高的封禁 Key;
- 热点短码进入 L1 或 Edge;
- 不要把所有 Key 放入同一 Redis Cluster Hash Tag。
9.3 防缓存穿透
- 短码格式校验;
- CDN/WAF 限速;
- IP、ASN、设备和租户维度限流;
- Redis 空值缓存;
- Bloom Filter 辅助;
- 随机扫描检测;
- 数据库兜底限流与熔断。
Bloom Filter 只能说明“可能存在”或“一定不存在”,不能作为最终判断。
9.4 防缓存击穿
- 逻辑过期;
- SingleFlight;
- 后台异步刷新;
- 热点预热;
- TTL 抖动;
- 必要时使用互斥锁。
不要让大量请求阻塞在同一把分布式锁上。一个请求负责回源,其余请求可短暂等待、使用旧快照或快速失败。
9.5 修改后的缓存一致性
sequenceDiagram
autonumber
participant A as 管理端
participant L as Link Service
participant DB as Link DB
participant O as Outbox
participant MQ as Kafka
participant R as Redis
participant S as Redirect Instances
A->>L: 修改目标地址
L->>DB: 事务更新 link + version
L->>O: 同事务写 LinkChanged
DB-->>L: Commit
L->>R: 删除或覆盖缓存
L-->>A: 修改成功
O->>MQ: 可靠投递变更事件
MQ->>S: 广播缓存失效
S->>S: 删除 L1 缓存
推荐:
1 | |
紧急封禁不能只依赖异步消息。可维护:
1 | |
让封禁状态优先于普通链接缓存。
十、分库分表
10.1 分片键
跳转查询条件是:
1 | |
推荐:
1 | |
或在短码随机性足够时:
1 | |
只按 tenant_id 分片可能产生超级租户热点。可采用:
1 | |
后台按租户检索不应直接跨所有跳转分片分页,可通过 CDC 同步到 Elasticsearch、OpenSearch 或专用查询库。
10.2 虚拟分片
1 | |
flowchart LR
Code[domainGroupId + shortCode] --> Hash[Hash 路由]
Hash --> V1[Virtual Bucket 0..255]
Hash --> V2[Virtual Bucket 256..511]
Hash --> V3[Virtual Bucket 512..767]
Hash --> V4[Virtual Bucket 768..1023]
V1 --> DB1[(Shard 1)]
V2 --> DB2[(Shard 2)]
V3 --> DB3[(Shard 3)]
V4 --> DB4[(Shard 4)]
扩容迁移:
- 新旧分片双读;
- 旧分片写入后 CDC 同步新分片;
- 校验行数和摘要;
- 切换虚拟桶路由;
- 保留回滚窗口;
- 清理旧数据。
十一、多域名控制平面
11.1 域名状态机
stateDiagram-v2
[*] --> PENDING_DNS
PENDING_DNS --> PENDING_CERT: DNS 验证通过
PENDING_CERT --> WARMING: TLS 证书签发
WARMING --> ACTIVE: 健康检查通过
ACTIVE --> DEGRADED: 部分探测失败
DEGRADED --> ACTIVE: 恢复
DEGRADED --> DRAINING: 持续异常
ACTIVE --> DRAINING: 主动迁移
DRAINING --> RETIRED: 无新增分配
RETIRED --> [*]
ACTIVE --> QUARANTINED: 风险或安全事件
DEGRADED --> QUARANTINED: 风险或安全事件
QUARANTINED --> DRAINING: 人工处置
QUARANTINED --> ACTIVE: 复核通过
| 状态 | 新链接分配 | 旧链接服务 | 说明 |
|---|---|---|---|
| PENDING_DNS | 否 | 否 | 等待域名所有权验证 |
| PENDING_CERT | 否 | 否 | 等待 TLS 证书 |
| WARMING | 否 | 测试流量 | DNS、TLS、CDN 和路由预热 |
| ACTIVE | 是 | 是 | 正常状态 |
| DEGRADED | 降权 | 是 | 部分地域或指标异常 |
| DRAINING | 否 | 是 | 停止新分配,保留旧链接 |
| QUARANTINED | 否 | 按策略 | 风险隔离 |
| RETIRED | 否 | 可选 | 完成退役 |
11.2 域名健康指标
Domain Control Plane 定期检查:
- DNS A、AAAA、CNAME;
- 多地域 DNS 解析;
- TLS 证书链、SNI、有效期和最低 TLS 版本;
- CDN 配置;
- HTTP 服务健康;
- P50、P95、P99 延迟;
- 4xx、5xx、TLS 和 DNS 错误率;
- 外部风险情报状态;
- 域名注册到期时间;
- 证书自动续期状态;
- 客户 CNAME 是否被删除;
- 租户投诉率和风险趋势。
必须使用多个地域和网络探测,不能只依赖单点监控。
11.3 推荐域名选择
flowchart TD
A[域名组候选列表] --> B{状态为 ACTIVE?}
B -- 否 --> X[排除]
B -- 是 --> C{DNS 与 TLS 健康?}
C -- 否 --> X
C -- 是 --> D{风险状态允许?}
D -- 否 --> X
D -- 是 --> E{符合租户和渠道策略?}
E -- 否 --> X
E -- 是 --> F[按权重、地区和粘性选择]
F --> G[返回推荐分享域名]
采用粘性分配:
- 短链创建后默认分享域名保持稳定;
- 域名进入 DRAINING 后停止新分配;
- 用户主动刷新分享链接时获取新域名;
- 同一短码仍可在组内所有可用域名解析;
- 权重变化只影响之后的分配。
11.4 域名切换流程
sequenceDiagram
autonumber
participant M as 监控/运营
participant D as Domain Control Plane
participant C as 配置中心
participant E as Edge/Redirect
participant A as Link Admin
participant U as 用户
M->>D: 域名异常或计划迁移
D->>D: 旧域名设为 DRAINING
D->>D: 选择并预热新域名
D->>C: 发布新域名组版本
C->>E: 推送配置
E->>E: 更新 Host 路由
A->>A: 新建和刷新链接使用新域名
U->>A: 获取最新分享链接
A-->>U: https://new.example/code
Note over E,U: 原域名仍可访问时,旧链接继续解析同一短码
Note over E,U: 原域名不可达时,已发送 URL 无法由后台自动修复
11.5 避免跳转链
不推荐:
1 | |
问题:延迟增加、故障点增加、归因复杂、风险特征更明显。
推荐所有仍受控的短域名直接访问统一 Redirect Service,再直接跳转最终目标。
11.6 客户自定义域名
1 | |
接入流程:
- 客户提交域名;
- 系统生成 TXT 或 CNAME 验证记录;
- 验证域名所有权;
- 配置 CDN Custom Hostname;
- 自动签发 TLS 证书;
- 检查 TLS 策略;
- 绑定 tenant_id 和 domain_group_id;
- 完成健康检查;
- 状态切换为 ACTIVE;
- 持续监控 CNAME 和证书。
自定义域名必须与租户强绑定,禁止通过修改 Host 访问其他租户短链。
11.7 证书自动化
- ACME 自动签发;
- DNS-01 或 HTTP-01 验证;
- SAN、通配符和单域名策略;
- CA 签发速率限制;
- 自动续期和指数退避;
- 备用证书颁发机构;
- 私钥隔离;
- 到期前 30、15、7、3、1 天告警。
备用域名应提前完成 DNS、TLS、CDN 和服务预热,不要等故障发生后再申请证书。
十二、链接修改、下线与版本控制
12.1 乐观锁
1 | |
影响行数为 0 时说明发生并发修改,应让调用方重新读取。
12.2 版本表保存内容
- 旧目标和新目标;
- 操作人和操作时间;
- 修改原因;
- 审批单号;
- 风险结果;
- 请求来源;
- IP 和 Trace ID。
用于审计、回滚、安全调查和分版本统计。
12.3 跳转判断顺序
1 | |
紧急封禁必须优先于普通缓存记录。
十三、点击统计与实时分析
13.1 禁止同步更新点击数
错误做法:
1 | |
会造成热点行锁、数据库写放大、跳转延迟和故障耦合。
正确做法是异步发送事件。
13.2 点击事件
1 | |
原始 IP、User-Agent、Referer 和 Query 参数可能包含个人信息,必须按隐私政策脱敏并设置保留期限。
13.3 事件流
flowchart LR
R[Redirect Service] --> K[Kafka click-events]
K --> F[Flink]
F --> RT[(Redis 实时计数)]
F --> OLAP[(ClickHouse / Doris)]
F --> DL[(对象存储 / Data Lake)]
F --> Risk[异常流量检测]
RT --> Dashboard[实时看板]
OLAP --> Report[多维报表]
DL --> Offline[离线分析]
Risk --> Block[限流与风控策略]
13.4 事件语义
- 采用 At-Least-Once;
- 每条事件生成全局唯一 event_id;
- Kafka Producer 开启幂等;
- 流处理按 event_id 窗口去重;
- 聚合写入使用幂等批次号;
- 明确 PV、UV、机器人和转化口径。
十四、安全与反滥用
14.1 URL 规范化
创建前:
- 解析 URI;
- 仅允许
http和https; - 统一主机名大小写;
- 处理 IDN/Punycode;
- 移除默认端口;
- 规范化路径;
- 限制 URL 长度;
- 拒绝控制字符和 CRLF;
- 检查 Authority 中的用户名密码;
- 计算规范化 URL 哈希;
- 明确 Fragment 和 Query 去重规则。
禁止协议:
1 | |
14.2 SSRF 防护
重定向本身通常不会由服务端请求目标站点,但以下功能会产生服务端访问:
- 页面预览;
- 风险扫描;
- 可用性检查;
- Open Graph 抓取;
- 截图服务。
必须:
- 阻止环回、RFC 1918 私网和链路本地地址;
- 阻止云厂商元数据地址;
- 阻止 IPv6 ULA 和本地地址;
- DNS 解析前后都检查;
- 每次跟随重定向后重新检查;
- 限制跳转次数、端口、超时和响应大小;
- 使用无凭证隔离网络;
- 防止 DNS Rebinding。
14.3 恶意 URL 检测
组合使用:
- 商业 URL 威胁情报;
- Google Web Risk 等服务;
- 自建黑白名单;
- 域名注册时间和信誉;
- 目标内容扫描;
- 文件类型检查;
- 钓鱼品牌识别;
- 用户和租户历史风险;
- 投诉率;
- 创建量与点击量异常;
- 多层跳转分析;
- 人工审核。
风险状态:
1 | |
14.4 禁止差异化欺骗
系统不应实现:
- 审核机器人看到安全页,真人看到另一页;
- 根据 User-Agent、Referer、IP 或指纹选择违规目标;
- 多层短链隐藏最终目标;
- DNS Fast-Flux;
- 被封后自动注册并启用大量新域名;
- 对扫描器返回虚假 404;
- 在前端解密恶意目标后跳转。
14.5 限流
创建接口维度:
- tenant_id;
- user_id;
- access_key;
- IP;
- API 路径;
- 风险等级;
- 日配额。
跳转接口维度:
- IP 和网段;
- ASN;
- Host;
- short_code;
- User-Agent;
- 地域;
- 扫描行为特征。
热门合法链接不能简单按短码总 QPS 限流,应结合 Edge 缓存、机器人识别和单来源速率。
14.6 管理面安全
- OAuth2/OIDC;
- RBAC 和租户权限;
- 高风险操作二次确认;
- 目标地址修改审批;
- API 签名或 mTLS;
- 审计日志;
- API Key 轮换;
- 管理端和跳转端使用不同域名;
- 禁止日志打印完整 Token;
- 批量封禁与域名切换提供回滚。
十五、高可用与多地域容灾
15.1 单地域多可用区
flowchart TB
LB[CDN / Load Balancer]
subgraph AZ1[可用区 A]
R1[Redirect Pod]
Redis1[Redis Node]
DB1[DB Node]
end
subgraph AZ2[可用区 B]
R2[Redirect Pod]
Redis2[Redis Node]
DB2[DB Node]
end
subgraph AZ3[可用区 C]
R3[Redirect Pod]
Redis3[Redis Node]
DB3[DB Node]
end
LB --> R1
LB --> R2
LB --> R3
R1 --> Redis1
R2 --> Redis2
R3 --> Redis3
Redis1 <--> Redis2
Redis2 <--> Redis3
DB1 <--> DB2
DB2 <--> DB3
要求:
- Redirect Service 跨 3 个可用区;
- Redis、数据库和应用避免全部共置;
- 负载均衡主动健康检查;
- Pod 反亲和与 PodDisruptionBudget;
- 合理的连接池、超时和熔断;
- 避免所有实例同时重启;
- 域名和证书配置使用版本化快照。
15.2 多地域
flowchart LR
User[全球用户] --> GSLB[GeoDNS / Anycast / GSLB]
subgraph R1[Region A]
E1[Edge / Redirect]
C1[Redis]
D1[(Link DB)]
end
subgraph R2[Region B]
E2[Edge / Redirect]
C2[Redis]
D2[(Link DB)]
end
subgraph R3[Region C]
E3[Edge / Redirect]
C3[Redis]
D3[(Link DB)]
end
GSLB --> E1
GSLB --> E2
GSLB --> E3
E1 --> C1 --> D1
E2 --> C2 --> D2
E3 --> C3 --> D3
D1 -.CDC / 事件复制.-> D2
D1 -.CDC / 事件复制.-> D3
推荐优先使用单写多读:
- 创建和修改进入主地域;
- 配置通过 CDC 或 Kafka 复制;
- 跳转请求就近读取;
- 实现简单;
- 接受秒级复制延迟。
全局多写会引入冲突处理、全局唯一 ID 和一致性成本,除非有明确需求,否则不建议首选。
15.3 故障降级
| 故障 | 降级策略 |
|---|---|
| L1 故障 | 直接查询 Redis |
| Redis 部分故障 | 本地缓存继续服务,受控回源数据库 |
| Redis 全故障 | 热点 L1 + 数据库限流回源 |
| 数据库故障 | 缓存链接继续服务,创建修改进入只读或排队 |
| Kafka 故障 | 跳转不阻塞,事件落本地磁盘或丢弃低价值统计 |
| 风控故障 | 已审核链接继续,新建外链进入 REVIEW |
| 配置中心故障 | 使用最后一次成功的版本化快照 |
| 单域名故障 | 进入 DRAINING,新链接使用其他 ACTIVE 域名 |
| 证书异常 | 自动续期、备用 CA 和提前告警 |
| 单地域故障 | GSLB 切换到其他地域 |
十六、热点链接专项优化
一个大型活动可能让单短码达到几十万 QPS。即使 Redis Cluster 总容量充足,单 Key 仍可能集中在一个节点。
优化顺序:
- CDN 或 Edge Worker 缓存映射;
- Redirect Service L1 缓存;
- 热点识别和自动预热;
- Redis 仅作为 L2;
- 热点 Key 多副本读取;
- 活动前容量评估;
- 保护最终目标站点,避免瞬间流量打爆下游。
flowchart LR
U[大量用户] --> CDN[CDN / Edge Cache]
CDN --> R1[Redirect Instance 1 L1]
CDN --> R2[Redirect Instance 2 L1]
CDN --> R3[Redirect Instance N L1]
R1 --> Redis[Redis L2]
R2 --> Redis
R3 --> Redis
Redis --> DB[(Link DB)]
热点目标紧急修改时:
- 提高配置版本;
- 清理 Edge KV;
- 广播 L1 失效;
- 更新 Redis;
- 使用紧急封禁覆盖旧缓存;
- 验证多地域版本是否收敛。
十七、接口设计
1 | |
修改接口使用乐观锁:
1 | |
刷新分享链接响应:
1 | |
该接口不会修改已经发送出去的文本,只影响后续分享。
十八、Java 跳转服务示例
1 | |
解析服务:
1 | |
生产代码还需要:
- SingleFlight;
- Redis 超时和熔断;
- 数据库回源限流;
- 紧急封禁覆盖;
- 版本校验;
- L1 容量控制;
- Trace、Metrics 和结构化日志;
- Host 去端口、IDN 和 URI 规范化。
十九、可观测性
19.1 核心指标
跳转:
1 | |
缓存:
1 | |
域名:
1 | |
事件:
1 | |
19.2 结构化日志
1 | |
默认不要记录完整目标 URL 的敏感 Query,可记录域名、路径摘要或脱敏版本。
19.3 告警
- 域名 5xx、TLS 或 DNS 错误突增;
- 证书即将到期;
- Redirect P99 超标;
- Redis 命中率下降;
- 数据库回源 QPS 异常;
- 单短码 QPS 激增;
- 随机扫描比例上升;
- Kafka 堆积;
- 风控服务不可用;
- 域名风险状态变化;
- 缓存失效延迟超过 SLA;
- 多地域配置版本不一致。
二十、容量估算
假设:
- 每日 10 亿次跳转;
- 每条点击事件约 200 字节;
- 峰值是平均值 10 倍;
- 总缓存命中率 99.9%。
20.1 点击数据
1 | |
还需考虑 Kafka 副本、消息头、压缩、OLAP 索引和数据保留期限。
20.2 数据库回源
峰值 116,000 QPS,命中率 99.9%:
1 | |
但 Redis 故障、缓存同时过期、随机扫描和批量失效会让回源激增,因此数据库仍需冗余,Redirect Service 必须限流和熔断。
20.3 链接存储
每条主记录按 1 KB 粗估:
1 | |
加上索引、MVCC、版本表、审计和副本,实际可能达到数百 GB。
二十一、常见错误设计
21.1 每次跳转同步更新点击数
造成热点行和数据库耦合,应异步发送事件。
21.2 使用 301 但允许修改目标
客户端永久缓存后修改无法及时生效,可编辑短链默认使用 302。
21.3 只根据短码查询,不校验 Host
容易形成未知域名接入和跨租户访问,Host 必须映射域名组。
21.4 把域名写死在每条短链中
域名迁移需要批量更新海量记录,应绑定域名组。
21.5 每次点击随机选择域名
请求到达前 Host 已确定。域名选择应发生在分享链接生成阶段。
21.6 认为切换域名能修复所有旧链接
旧 URL 不会自动更新,原域名不可达时后台无法凭空修复。
21.7 允许任意 ?url= 跳转
形成开放重定向漏洞,目标必须来自受控记录。
21.8 缓存永久不过期
事件丢失后会长期返回旧配置,即使永久链接也应设置最大 TTL。
21.9 风控故障时默认放行
高风险新链接应失败关闭或进入审核。
21.10 所有租户共享一个公共域名
高风险租户可能影响全部客户,应按租户、业务和风险隔离域名池。
21.11 多层跳转隐藏目标
不会解决信誉问题,只会增加延迟、故障点和风险特征。
二十二、落地路线
阶段一:可用版本
- 单地域多可用区;
- Link Service 与 Redirect Service 分离;
- MySQL/PostgreSQL;
- Redis;
- 随机短码;
- 302 跳转;
- Kafka 异步点击事件;
- 基础黑名单;
- 单平台域名。
阶段二:企业版本
- 多租户;
- 域名组;
- 自定义域名;
- 域名状态机;
- URL 风险服务;
- 审计与版本回滚;
- 分库分表;
- ClickHouse/Doris;
- Transactional Outbox;
- L1 + L2 多级缓存;
- 热点检测。
阶段三:全球化版本
- 多地域就近跳转;
- 单写多读配置复制;
- Edge Worker 或边缘 KV;
- GSLB;
- 多 CA 证书策略;
- 多地域域名探测;
- 自动灾备演练;
- 租户信誉隔离;
- 实时异常流量识别;
- 数据湖归档。
二十三、最终架构
flowchart TB
subgraph Access[全球接入]
DNS[GeoDNS / Anycast]
WAF[CDN / WAF / Anti-DDoS]
end
subgraph RedirectPlane[跳转数据平面]
EdgeKV[Edge KV / Worker Cache]
Redirect[Stateless Redirect Service]
Caffeine[Caffeine L1]
Redis[Redis Cluster L2]
LinkDB[(Sharded Link DB)]
end
subgraph ManagementPlane[企业控制平面]
Gateway[API Gateway]
LinkSvc[Link Management]
DomainSvc[Domain Control Plane]
RiskSvc[Risk & Abuse Service]
Audit[Audit & Approval]
Config[Versioned Config]
end
subgraph EventAnalytics[事件与分析]
Kafka[Kafka]
Flink[Flink]
ClickHouse[(ClickHouse / Doris)]
DataLake[(Object Storage)]
end
User[用户] --> DNS --> WAF --> EdgeKV --> Redirect
Redirect --> Caffeine --> Redis --> LinkDB
Biz[业务系统 / 控制台] --> Gateway
Gateway --> LinkSvc
Gateway --> DomainSvc
Gateway --> Audit
LinkSvc --> RiskSvc
LinkSvc --> LinkDB
LinkSvc --> Redis
DomainSvc --> Config
Config --> Redirect
Config --> WAF
Redirect -.Click Event.-> Kafka
LinkSvc -.Link Event.-> Kafka
DomainSvc -.Domain Event.-> Kafka
Kafka --> Flink
Flink --> ClickHouse
Flink --> DataLake
Flink --> RiskSvc
核心结论:
- 短链是典型读密集型系统,主链路必须短、无状态、缓存优先;
- 短码与具体域名解耦,通过域名组实现迁移和多租户隔离;
- 域名切换只能影响新分享链接,无法自动修改已经传播的旧 URL;
- 可编辑短链默认使用 302,避免永久缓存;
- 点击统计必须异步化;
- 目标修改采用版本控制、Outbox、Redis 失效和 L1 广播;
- 域名池用于合规容灾和信誉隔离,不用于差异化欺骗;
- 风控、投诉、审计和紧急下线与短码生成同等重要;
- 热点链接尽量在 Edge 和本地缓存消化;
- 控制平面故障不应影响已有短链的数据平面服务。
真正的企业级短链系统,不只是把长 URL 压缩成几位字符,而是围绕稳定分享、快速跳转、可控变更、域名治理、安全可信和故障恢复建立的一套完整基础设施。
参考资料
- IETF RFC 9110:HTTP Semantics
https://www.rfc-editor.org/rfc/rfc9110.html - IETF RFC 3986:Uniform Resource Identifier Generic Syntax
https://datatracker.ietf.org/doc/html/rfc3986 - OWASP:Unvalidated Redirects and Forwards Cheat Sheet
https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html - OWASP:Open Redirect
https://owasp.org/www-community/attacks/open_redirect - Google Cloud:Web Risk
https://cloud.google.com/security/products/web-risk - Google Safe Browsing
https://developers.google.com/safe-browsing - Redis:Client-side caching reference
https://redis.io/docs/latest/develop/reference/client-side-caching/ - Apache Kafka:Producer Configurations
https://kafka.apache.org/41/configuration/producer-configs/ - NGINX:ngx_http_limit_req_module
https://nginx.org/en/docs/http/ngx_http_limit_req_module.html - Cloudflare for SaaS:Custom Hostnames
https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/ - Let’s Encrypt:Rate Limits
https://letsencrypt.org/docs/rate-limits/