企业级短链系统设计:高并发、多域名治理、风控与容灾

短链系统看起来只是“根据短码查询长链接并返回 HTTP 重定向”,但进入企业场景后,它会同时面对高并发读、热点链接、域名迁移、租户隔离、链接修改、实时下线、恶意链接、统计分析和多地域容灾。

本文中的“多域名切换”用于业务连续性、品牌迁移、客户自定义域名和风险隔离,不用于通过伪装页面、差异化跳转或快速域名轮换绕过平台审核与安全拦截。

一、业务背景

用户需要将较长的业务地址转换为便于分享的短链接:

1
2
3
4
5
原始链接:
https://www.example.com/marketing/campaign/detail?id=984731&channel=wechat

短链接:
https://s.example.com/A8kP3xQm

企业短链通常用于:

  • 短信、邮件、企业微信、公众号和 App Push;
  • 营销活动、优惠券、邀请注册与渠道投放;
  • 文件、订单、账单和审批页面分享;
  • 二维码生成;
  • 内部系统临时访问链接;
  • 多租户 SaaS 的客户品牌域名;
  • 域名升级、品牌迁移和故障切换。

真正的难点不是生成一个短字符串,而是解决:

  1. 跳转请求远多于创建请求,如何支撑十万级甚至更高峰值 QPS;
  2. 热门活动形成单短码热点,如何避免 Redis 热 Key 和数据库击穿;
  3. 目标地址允许修改时,如何让缓存快速且可靠地失效;
  4. 域名、DNS、证书或外部信誉发生异常时,如何进行合规切换;
  5. 如何防止钓鱼、恶意软件、开放重定向和 SSRF;
  6. 如何在不影响跳转性能的情况下完成点击分析;
  7. 多租户自定义域名如何验证所有权、签发证书并隔离数据;
  8. Redis、数据库、消息队列或地域故障时如何降级。

二、多域名的现实边界

必须先明确一个事实:

已经分享出去的 URL,其域名已经固化在文本、二维码或消息中。后台切换默认域名,只能影响之后生成的新链接,不能让旧 URL 自动变成另一个域名。

例如用户已经分享:

1
https://a.example/A8kP3xQm

系统后来把推荐域名切换为:

1
https://b.example/A8kP3xQm

已经发出去的 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
1,000,000,000 / 86,400 ≈ 11,574 QPS

营销活动、批量推送和热点事件往往产生几十倍瞬时流量,因此不能只按日均容量设计。


四、总体架构

短链系统建议拆成两个平面:

  • 数据平面:负责高性能跳转,路径短、依赖少、无状态;
  • 控制平面:负责创建、修改、域名、风控、审核和运营。
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

核心原则:

  1. 跳转服务无状态,支持水平扩容;
  2. 跳转链路不做同步统计写入;
  3. 域名选择发生在创建或刷新分享链接时,不在每次点击时随机选择;
  4. 链接配置与点击事件使用不同存储;
  5. 控制平面故障时,已有短链应尽量继续可用;
  6. 数据库故障时,缓存中的热门链接继续服务;
  7. 风控失败时,高风险新链接应失败关闭或进入审核。

五、核心领域模型

短链、域名组和具体域名必须解耦。

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
2
3
s1.example.com
s2.example.com
brand.example.net

短码 A8kP3xQm 绑定到域名组,而不是绑定到具体域名。下面三个 URL 可解析到同一记录:

1
2
3
https://s1.example.com/A8kP3xQm
https://s2.example.com/A8kP3xQm
https://brand.example.net/A8kP3xQm

推荐唯一约束:

1
UNIQUE(domain_group_id, short_code)

Redirect Service 收到请求后:

1
2
3
4
5
Host
-> domain_id
-> domain_group_id
-> domain_group_id + short_code
-> LinkSnapshot

优势:

  • 域名组内更换推荐域名时短码不变;
  • 新旧域名可以并行工作;
  • 不同租户可复用相同短码;
  • 可按租户、业务和风险级别隔离域名;
  • 域名退役无需批量修改短链主表。

5.2 数据表设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
CREATE TABLE short_link (
link_id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL,
domain_group_id BIGINT NOT NULL,
short_code VARCHAR(16) NOT NULL,
target_url TEXT NOT NULL,
target_hash BINARY(32) NOT NULL,
status TINYINT NOT NULL DEFAULT 1,
risk_status TINYINT NOT NULL DEFAULT 0,
redirect_type SMALLINT NOT NULL DEFAULT 302,
version BIGINT NOT NULL DEFAULT 1,
effective_at DATETIME(3) NULL,
expires_at DATETIME(3) NULL,
created_by BIGINT NOT NULL,
updated_by BIGINT NOT NULL,
created_at DATETIME(3) NOT NULL,
updated_at DATETIME(3) NOT NULL,
PRIMARY KEY (link_id),
UNIQUE KEY uk_group_code (domain_group_id, short_code),
KEY idx_tenant_created (tenant_id, created_at),
KEY idx_target_hash (tenant_id, target_hash)
);

CREATE TABLE short_domain (
domain_id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL,
domain_group_id BIGINT NOT NULL,
hostname VARCHAR(253) NOT NULL,
status TINYINT NOT NULL,
weight INT NOT NULL DEFAULT 100,
dns_status TINYINT NOT NULL,
cert_status TINYINT NOT NULL,
reputation_status TINYINT NOT NULL,
certificate_expire_at DATETIME(3) NULL,
created_at DATETIME(3) NOT NULL,
updated_at DATETIME(3) NOT NULL,
PRIMARY KEY (domain_id),
UNIQUE KEY uk_hostname (hostname),
KEY idx_group_status (domain_group_id, status)
);

CREATE TABLE short_link_target_version (
version_id BIGINT NOT NULL,
link_id BIGINT NOT NULL,
version BIGINT NOT NULL,
target_url TEXT NOT NULL,
operator_id BIGINT NOT NULL,
change_reason VARCHAR(500) NULL,
created_at DATETIME(3) NOT NULL,
PRIMARY KEY (version_id),
UNIQUE KEY uk_link_version (link_id, version)
);

六、短码生成方案

6.1 常见方案

方案 优点 缺点 适用场景
数据库自增 ID + Base62 简单、无碰撞、长度短 暴露增长趋势,序列可能成为瓶颈 中小规模
号段模式 + Base62 高吞吐、无碰撞 仍可枚举,需要打乱 企业内部系统
Snowflake + Base62 本地生成、分布式吞吐高 偏长,依赖时钟和机器号 分布式创建
安全随机数 + 唯一索引 难枚举,不暴露业务量 有碰撞概率,需要重试 公共短链
预生成 Key 池 创建速度快 维护复杂,需要库存补充 超大规模营销
URL 哈希截断 相同 URL 可复用 截断碰撞,修改语义复杂 去重辅助

Base62 字符集通常为:

1
0-9 A-Z a-z

理论空间:

长度 理论空间
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 位:

  1. 使用密码学安全随机数生成器;
  2. 映射到 Base62 或去除易混淆字符后的 Base58;
  3. 插入数据库;
  4. 命中唯一索引冲突后重试;
  5. 限制最大重试次数并告警。

企业内部系统可使用:

1
2
3
号段 ID / Snowflake ID
-> 带密钥可逆置换
-> Base62 编码

不要直接把连续自增 ID 编成 Base62 暴露到公网,否则容易被枚举,并泄露增长趋势。

6.3 自定义短码

  • 长度 4~32;
  • 仅允许字母、数字、短横线和下划线;
  • 保留 apiadminloginhealth 等关键字;
  • 大小写规则全局统一;
  • 进行敏感词与品牌词检查;
  • 高价值短码需要审批;
  • 删除后进入冷却期;
  • 自定义短码不能绕过 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
2
3
4
POST /api/v1/links
Authorization: Bearer <token>
Idempotency-Key: 7e96f78d-cc4f-4f21-8ac9-6d71750db511
Content-Type: application/json
1
2
3
4
5
6
7
8
9
10
11
12
{
"targetUrl": "https://www.example.com/campaign/2026",
"domainGroup": "marketing-cn",
"customCode": null,
"redirectType": 302,
"effectiveAt": null,
"expiresAt": "2026-12-31T23:59:59+08:00",
"tags": ["summer-campaign", "wechat"],
"metadata": {
"campaignId": "CMP-20260730-001"
}
}

响应:

1
2
3
4
5
6
7
8
9
{
"linkId": "2037012848364400640",
"shortCode": "A8kP3xQm",
"shortUrl": "https://s1.example.com/A8kP3xQm",
"domainGroup": "marketing-cn",
"status": "ACTIVE",
"version": 1,
"expiresAt": "2026-12-31T23:59:59+08:00"
}

7.2 创建幂等

使用:

1
tenant_id + idempotency_key

幂等记录保存请求摘要、执行状态、返回结果和过期时间。相同幂等键对应不同请求体时直接拒绝,避免调用方错误复用。


八、跳转链路

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
2
3
4
5
host
-> domain_id
-> domain_group_id
-> domain_group_id + short_code
-> link

未知 Host 返回 421 或统一 404。

8.2 为什么默认使用 302

允许修改目标地址的短链推荐返回:

1
2
3
HTTP/1.1 302 Found
Location: https://www.example.com/campaign/2026
Cache-Control: no-store, max-age=0

原因:

  • 301 和 308 表示永久重定向,客户端可能长期缓存;
  • 永久缓存后,后台修改目标地址可能无法及时生效;
  • 302 更适合可修改、可停用和需要统计的短链;
  • 307 会严格保留请求方法,但普通短链应只接受 GET/HEAD。
场景 状态码
可编辑营销短链 302
临时迁移且需要保留方法 307
永久且不可修改映射 301 或 308
风险下线 404、410 或风险提示页
已过期 410 Gone 或业务过期页

不要依赖浏览器缓存重定向提升性能,应在 Edge、Redirect Service 本地缓存和 Redis 中缓存映射。

8.3 禁止开放重定向

危险接口:

1
GET /redirect?url=https://attacker.example

安全接口:

1
GET /A8kP3xQm

目标地址只能来自已审核、已授权的数据库记录,跳转请求不能临时传入任意 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
domain_group_id + ":" + short_code

缓存值应为不可变快照:

1
2
3
4
5
6
7
8
9
{
"targetUrl": "https://www.example.com/campaign/2026",
"status": "ACTIVE",
"riskStatus": "ALLOW",
"version": 17,
"effectiveAt": null,
"expiresAt": 1798732799000,
"redirectType": 302
}

9.2 Redis 缓存

Key:

1
sl:{domainGroupId}:{shortCode}

策略:

  • 正常链接 TTL 为 1~24 小时;
  • 永久链接也设置最大 TTL;
  • TTL 加随机抖动;
  • 不存在短码缓存 30~300 秒空值;
  • 紧急封禁使用优先级更高的封禁 Key;
  • 热点短码进入 L1 或 Edge;
  • 不要把所有 Key 放入同一 Redis Cluster Hash Tag。

9.3 防缓存穿透

  1. 短码格式校验;
  2. CDN/WAF 限速;
  3. IP、ASN、设备和租户维度限流;
  4. Redis 空值缓存;
  5. Bloom Filter 辅助;
  6. 随机扫描检测;
  7. 数据库兜底限流与熔断。

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
2
3
4
数据库事务
+ Transactional Outbox
+ Redis 删除或覆盖
+ Kafka 广播 L1 失效

紧急封禁不能只依赖异步消息。可维护:

1
blocked:{domainGroupId}:{shortCode}

让封禁状态优先于普通链接缓存。


十、分库分表

10.1 分片键

跳转查询条件是:

1
domain_group_id + short_code

推荐:

1
shard = hash(domain_group_id + short_code) % N

或在短码随机性足够时:

1
shard = hash(short_code) % N

只按 tenant_id 分片可能产生超级租户热点。可采用:

1
hash(tenant_id + short_code)

后台按租户检索不应直接跨所有跳转分片分页,可通过 CDC 同步到 Elasticsearch、OpenSearch 或专用查询库。

10.2 虚拟分片

1
短码 -> 1024 个虚拟桶 -> 实际数据库分片
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)]

扩容迁移:

  1. 新旧分片双读;
  2. 旧分片写入后 CDC 同步新分片;
  3. 校验行数和摘要;
  4. 切换虚拟桶路由;
  5. 保留回滚窗口;
  6. 清理旧数据。

十一、多域名控制平面

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
go.customer.com CNAME short.example-saas.com

接入流程:

  1. 客户提交域名;
  2. 系统生成 TXT 或 CNAME 验证记录;
  3. 验证域名所有权;
  4. 配置 CDN Custom Hostname;
  5. 自动签发 TLS 证书;
  6. 检查 TLS 策略;
  7. 绑定 tenant_id 和 domain_group_id;
  8. 完成健康检查;
  9. 状态切换为 ACTIVE;
  10. 持续监控 CNAME 和证书。

自定义域名必须与租户强绑定,禁止通过修改 Host 访问其他租户短链。

11.7 证书自动化

  • ACME 自动签发;
  • DNS-01 或 HTTP-01 验证;
  • SAN、通配符和单域名策略;
  • CA 签发速率限制;
  • 自动续期和指数退避;
  • 备用证书颁发机构;
  • 私钥隔离;
  • 到期前 30、15、7、3、1 天告警。

备用域名应提前完成 DNS、TLS、CDN 和服务预热,不要等故障发生后再申请证书。


十二、链接修改、下线与版本控制

12.1 乐观锁

1
2
3
4
5
6
7
8
9
UPDATE short_link
SET target_url = ?,
target_hash = ?,
version = version + 1,
updated_by = ?,
updated_at = NOW(3)
WHERE link_id = ?
AND tenant_id = ?
AND version = ?;

影响行数为 0 时说明发生并发修改,应让调用方重新读取。

12.2 版本表保存内容

  • 旧目标和新目标;
  • 操作人和操作时间;
  • 修改原因;
  • 审批单号;
  • 风险结果;
  • 请求来源;
  • IP 和 Trace ID。

用于审计、回滚、安全调查和分版本统计。

12.3 跳转判断顺序

1
2
3
4
5
6
7
8
9
10
Host 是否有效
-> 短码格式是否合法
-> 是否紧急封禁
-> 链接是否存在
-> 租户是否有效
-> 链接是否启用
-> 是否到生效时间
-> 是否过期
-> 风险状态是否允许
-> 返回重定向

紧急封禁必须优先于普通缓存记录。


十三、点击统计与实时分析

13.1 禁止同步更新点击数

错误做法:

1
2
3
UPDATE short_link
SET click_count = click_count + 1
WHERE link_id = ?;

会造成热点行锁、数据库写放大、跳转延迟和故障耦合。

正确做法是异步发送事件。

13.2 点击事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"eventId": "01JQX8Z53AHTT78X45K1H0T8T2",
"eventTime": "2026-07-30T09:43:12.345+08:00",
"linkId": "2037012848364400640",
"tenantId": "10001",
"domainGroupId": "30001",
"domainId": "31008",
"shortCode": "A8kP3xQm",
"targetVersion": 17,
"requestId": "req-7f28a",
"clientIpHash": "sha256:...",
"userAgent": "Mozilla/5.0 ...",
"referer": "https://...",
"country": "JP",
"region": "Tokyo",
"deviceType": "MOBILE",
"bot": false,
"responseStatus": 302,
"costMs": 4
}

原始 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;
  • 仅允许 httphttps
  • 统一主机名大小写;
  • 处理 IDN/Punycode;
  • 移除默认端口;
  • 规范化路径;
  • 限制 URL 长度;
  • 拒绝控制字符和 CRLF;
  • 检查 Authority 中的用户名密码;
  • 计算规范化 URL 哈希;
  • 明确 Fragment 和 Query 去重规则。

禁止协议:

1
2
3
4
5
6
7
javascript:
data:
file:
ftp:
gopher:
jar:
classpath:

14.2 SSRF 防护

重定向本身通常不会由服务端请求目标站点,但以下功能会产生服务端访问:

  • 页面预览;
  • 风险扫描;
  • 可用性检查;
  • Open Graph 抓取;
  • 截图服务。

必须:

  • 阻止环回、RFC 1918 私网和链路本地地址;
  • 阻止云厂商元数据地址;
  • 阻止 IPv6 ULA 和本地地址;
  • DNS 解析前后都检查;
  • 每次跟随重定向后重新检查;
  • 限制跳转次数、端口、超时和响应大小;
  • 使用无凭证隔离网络;
  • 防止 DNS Rebinding。

14.3 恶意 URL 检测

组合使用:

  • 商业 URL 威胁情报;
  • Google Web Risk 等服务;
  • 自建黑白名单;
  • 域名注册时间和信誉;
  • 目标内容扫描;
  • 文件类型检查;
  • 钓鱼品牌识别;
  • 用户和租户历史风险;
  • 投诉率;
  • 创建量与点击量异常;
  • 多层跳转分析;
  • 人工审核。

风险状态:

1
2
3
4
5
PENDING
ALLOW
REVIEW
WARN
BLOCK

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 仍可能集中在一个节点。

优化顺序:

  1. CDN 或 Edge Worker 缓存映射;
  2. Redirect Service L1 缓存;
  3. 热点识别和自动预热;
  4. Redis 仅作为 L2;
  5. 热点 Key 多副本读取;
  6. 活动前容量评估;
  7. 保护最终目标站点,避免瞬间流量打爆下游。
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
2
3
4
5
6
7
8
9
POST   /api/v1/links
GET /api/v1/links/{linkId}
PATCH /api/v1/links/{linkId}
POST /api/v1/links/{linkId}/disable
POST /api/v1/links/{linkId}/enable
POST /api/v1/links/{linkId}/refresh-share-url
GET /api/v1/links/{linkId}/analytics
GET /{shortCode}
HEAD /{shortCode}

修改接口使用乐观锁:

1
2
PATCH /api/v1/links/{linkId}
If-Match: "17"

刷新分享链接响应:

1
2
3
4
5
6
7
{
"linkId": "2037012848364400640",
"shortCode": "A8kP3xQm",
"previousUrl": "https://s1.example.com/A8kP3xQm",
"recommendedUrl": "https://s2.example.com/A8kP3xQm",
"domainChanged": true
}

该接口不会修改已经发送出去的文本,只影响后续分享。


十八、Java 跳转服务示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@RestController
@RequiredArgsConstructor
public class RedirectController {

private final DomainRegistry domainRegistry;
private final ShortLinkResolver shortLinkResolver;
private final ClickEventPublisher clickEventPublisher;

@GetMapping("/{code:[0-9A-Za-z_-]{4,32}}")
public ResponseEntity<Void> redirect(
@RequestHeader("Host") String host,
@PathVariable String code,
HttpServletRequest request) {

DomainSnapshot domain = domainRegistry.requireActiveOrDraining(host);

LinkSnapshot link = shortLinkResolver.resolve(
domain.domainGroupId(),
code
);

link.assertAccessible(Instant.now());

clickEventPublisher.publishAsync(
ClickEvent.from(request, domain, link)
);

HttpHeaders headers = new HttpHeaders();
headers.setLocation(URI.create(link.targetUrl()));
headers.setCacheControl("no-store, max-age=0");
headers.set("X-Content-Type-Options", "nosniff");
headers.set("Referrer-Policy", "no-referrer-when-downgrade");

return new ResponseEntity<>(
headers,
HttpStatus.valueOf(link.redirectType())
);
}
}

解析服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Service
@RequiredArgsConstructor
public class ShortLinkResolver {

private final Cache<LinkCacheKey, Optional<LinkSnapshot>> localCache;
private final LinkRedisRepository redisRepository;
private final LinkRepository linkRepository;

public LinkSnapshot resolve(long domainGroupId, String code) {
LinkCacheKey key = new LinkCacheKey(domainGroupId, code);

Optional<LinkSnapshot> local = localCache.getIfPresent(key);
if (local != null) {
return local.orElseThrow(LinkNotFoundException::new);
}

Optional<LinkSnapshot> redis = redisRepository.find(key);
if (redis.isPresent()) {
localCache.put(key, redis);
return redis.get();
}

Optional<LinkSnapshot> database =
linkRepository.findByDomainGroupIdAndCode(domainGroupId, code);

if (database.isEmpty()) {
redisRepository.cacheMiss(key, Duration.ofSeconds(60));
localCache.put(key, Optional.empty());
throw new LinkNotFoundException();
}

LinkSnapshot snapshot = database.get();
redisRepository.saveWithJitter(key, snapshot);
localCache.put(key, Optional.of(snapshot));
return snapshot;
}
}

生产代码还需要:

  • SingleFlight;
  • Redis 超时和熔断;
  • 数据库回源限流;
  • 紧急封禁覆盖;
  • 版本校验;
  • L1 容量控制;
  • Trace、Metrics 和结构化日志;
  • Host 去端口、IDN 和 URI 规范化。

十九、可观测性

19.1 核心指标

跳转:

1
2
3
4
5
6
7
8
redirect_requests_total
redirect_success_total
redirect_latency_seconds
link_not_found_total
link_expired_total
link_blocked_total
invalid_host_total
invalid_code_total

缓存:

1
2
3
4
5
6
7
l1_cache_hit_ratio
redis_cache_hit_ratio
negative_cache_hit_total
cache_load_latency
cache_invalidation_delay
hot_key_qps
redis_timeout_total

域名:

1
2
3
4
5
6
7
domain_dns_health
domain_tls_health
domain_http_health
domain_certificate_days_left
domain_reputation_status
domain_redirect_error_ratio
domain_config_version

事件:

1
2
3
4
5
click_event_publish_total
click_event_drop_total
kafka_producer_lag
stream_consumer_lag
olap_ingest_delay

19.2 结构化日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"timestamp": "2026-07-30T09:43:12.345+08:00",
"level": "INFO",
"service": "short-link-redirect",
"traceId": "b6ea1e43d8a9",
"requestId": "req-7f28a",
"host": "s1.example.com",
"domainGroupId": 30001,
"shortCode": "A8kP3xQm",
"linkId": 2037012848364400640,
"cacheLevel": "L1",
"redirectStatus": 302,
"costMs": 3
}

默认不要记录完整目标 URL 的敏感 Query,可记录域名、路径摘要或脱敏版本。

19.3 告警

  • 域名 5xx、TLS 或 DNS 错误突增;
  • 证书即将到期;
  • Redirect P99 超标;
  • Redis 命中率下降;
  • 数据库回源 QPS 异常;
  • 单短码 QPS 激增;
  • 随机扫描比例上升;
  • Kafka 堆积;
  • 风控服务不可用;
  • 域名风险状态变化;
  • 缓存失效延迟超过 SLA;
  • 多地域配置版本不一致。

二十、容量估算

假设:

  • 每日 10 亿次跳转;
  • 每条点击事件约 200 字节;
  • 峰值是平均值 10 倍;
  • 总缓存命中率 99.9%。

20.1 点击数据

1
10 亿 × 200 B ≈ 200 GB/天

还需考虑 Kafka 副本、消息头、压缩、OLAP 索引和数据保留期限。

20.2 数据库回源

峰值 116,000 QPS,命中率 99.9%:

1
116,000 × 0.1% = 116 QPS

但 Redis 故障、缓存同时过期、随机扫描和批量失效会让回源激增,因此数据库仍需冗余,Redirect Service 必须限流和熔断。

20.3 链接存储

每条主记录按 1 KB 粗估:

1
1 亿 × 1 KB ≈ 100 GB

加上索引、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

核心结论:

  1. 短链是典型读密集型系统,主链路必须短、无状态、缓存优先;
  2. 短码与具体域名解耦,通过域名组实现迁移和多租户隔离;
  3. 域名切换只能影响新分享链接,无法自动修改已经传播的旧 URL;
  4. 可编辑短链默认使用 302,避免永久缓存;
  5. 点击统计必须异步化;
  6. 目标修改采用版本控制、Outbox、Redis 失效和 L1 广播;
  7. 域名池用于合规容灾和信誉隔离,不用于差异化欺骗;
  8. 风控、投诉、审计和紧急下线与短码生成同等重要;
  9. 热点链接尽量在 Edge 和本地缓存消化;
  10. 控制平面故障不应影响已有短链的数据平面服务。

真正的企业级短链系统,不只是把长 URL 压缩成几位字符,而是围绕稳定分享、快速跳转、可控变更、域名治理、安全可信和故障恢复建立的一套完整基础设施。


参考资料


企业级短链系统设计:高并发、多域名治理、风控与容灾
https://allendericdalexander.github.io/2026/07/30/archtect/distribute/enterprise-short-link-system-design/
作者
AtLuoFu
发布于
2026年7月30日
许可协议