URL 短链服务看似简单——把长 URL 映射为短字符串,访问时重定向回去。但当你真正动手构建一个日均千万级访问量的短链系统时,会发现背后涉及编码算法选型、分布式 ID 生成、缓存策略、数据库分片、点击追踪等一系列工程难题。Bitly 每天处理超过 6 亿次重定向,t.co(Twitter 短链)更是达到 数十亿级。本文将从零开始,用 TypeScript 手写一个生产级短链服务的每一个核心模块,涵盖 Base62 编码、雪花算法发号器、301 vs 302 重定向选型、Redis 多级缓存、以及分布式部署架构,附完整的可运行代码和性能基准测试数据。
🔗 一、短链服务的核心架构与编码算法
1.1 两种生成策略的本质区别
短链服务的核心问题是:如何把一个长 URL 映射为一个短字符串? 业界有两种主流方案,选择不同会导致整个架构完全不同:
| 维度 | 哈希截取方案 | 分布式发号方案 |
|---|---|---|
| 生成方式 | 对长 URL 做哈希 → Base62 截取 | 全局自增 ID → Base62 编码 |
| 冲突处理 | ⚠️ 必须处理哈希冲突 | ✅ 天然无冲突 |
| 相同 URL | 需额外逻辑保证幂等 | 不同 ID 指向同一 URL |
| 短码可预测性 | ❌ 不可预测(哈希随机) | ⚠️ 可预测(自增) |
| 分布式扩展 | 简单(无状态) | 需要分布式发号器 |
| 适用场景 | 小规模、单机 | 生产级、分布式 |
📌 记住: 生产级短链服务(如 Bitly、t.co)几乎全部采用分布式发号方案。哈希截取方案虽然实现简单,但在分布式环境下处理冲突的复杂度远超发号方案。本文后续均基于发号方案展开。
1.2 Base62 编码:为什么是 62 进制?
短链的目标是让 URL 尽可能短。使用 Base62(0-9a-zA-Z,共 62 个字符)编码,可以在固定长度内表示最多的值:
| 编码方式 | 字符集 | 6 位可表示的值 | URL 安全性 |
|---|---|---|---|
| Base10 | 0-9 | 10^6 = 100 万 | ✅ 安全 |
| Base36 | 0-9a-z | 36^6 ≈ 21 亿 | ✅ 安全 |
| Base62 | 0-9a-zA-Z | 62^6 ≈ 568 亿 | ✅ 安全 |
| Base64 | +/0-9a-zA-Z= | 64^6 ≈ 687 亿 | ❌ 需 URL 编码 |
💡 提示: Base62 比 Base36 多出大写字母,6 位即可表示 568 亿个不同短码,足以覆盖绝大多数业务场景。如果用 Base36 则需要 7 位才能达到 783 亿,URL 会多一个字符。这一个字符在全球数十亿次重定向中累积的带宽成本是可观的。
Base62 编码的核心实现:
// base62.ts — Base62 编码与解码核心实现
const CHARSET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
export function encode(num: bigint): string {
if (num === 0n) return CHARSET[0]
let result = ''
while (num > 0n) {
result = CHARSET[Number(num % 62n)] + result
num = num / 62n
}
return result
}
export function decode(str: string): bigint {
let result = 0n
for (const char of str) {
result = result * 62n + BigInt(CHARSET.indexOf(char))
}
return result
}
// 验证:ID 1000000 → 短码
console.log(encode(1000000n)) // "4c92" (4 位就能表示 100 万)
console.log(decode('4c92')) // 1000000n
// 验证:6 位最大值
console.log(encode(62n ** 6n - 1n)) // "zzzzzz" (568 亿 - 1)
⚠️ 警告: 这里使用
bigint而非number,因为 JavaScript 的number最大安全整数是2^53 - 1(约 9007 万亿),而分布式雪花 ID 是 64 位整数。虽然 Base62 编码的单个 ID 不会超过这个范围,但使用bigint可以避免未来扩展时的精度陷阱。
1.3 预分配与自定义短码
生产级短链服务通常需要支持自定义短码(如 bit.ly/my-brand),这意味着系统需要维护两套映射:自增 ID 映射和自定义短码映射。同时,为避免与系统生成的短码冲突,自定义短码应有独立的命名空间:
// shortcode-resolver.ts — 短码解析器(支持自定义短码)
interface ShortCodeEntry {
shortCode: string
longUrl: string
type: 'auto' | 'custom'
createdAt: number
expireAt?: number
clickCount: number
}
class ShortCodeResolver {
// 双索引:短码 → 条目,长 URL → 短码(幂等性)
private codeIndex = new Map<string, ShortCodeEntry>()
private urlIndex = new Map<string, string>() // longUrl → shortCode
create(longUrl: string, customCode?: string): ShortCodeEntry {
// 幂等检查:同一长 URL 不重复创建
const existingCode = this.urlIndex.get(longUrl)
if (existingCode && !customCode) {
return this.codeIndex.get(existingCode)!
}
const shortCode = customCode || this.generateCode()
// 自定义短码冲突检测
if (this.codeIndex.has(shortCode)) {
throw new Error(`短码 "${shortCode}" 已被占用`)
}
const entry: ShortCodeEntry = {
shortCode,
longUrl,
type: customCode ? 'custom' : 'auto',
createdAt: Date.now(),
clickCount: 0,
}
this.codeIndex.set(shortCode, entry)
if (!customCode) this.urlIndex.set(longUrl, shortCode)
return entry
}
resolve(shortCode: string): ShortCodeEntry | undefined {
return this.codeIndex.get(shortCode)
}
private generateCode(): string {
// 使用雪花 ID 生成,见下一章
return encode(BigInt(Date.now()))
}
}
⚙️ 二、分布式发号器与 ID 生成算法
2.1 雪花算法(Snowflake)实现
分布式短链服务的核心挑战是:如何在多台机器上生成全局唯一、大致有序的 ID? 雪花算法(Twitter Snowflake)是最成熟的方案,它将 64 位整数分为四段:
| 1 bit 符号位 | 41 bit 时间戳 | 10 bit 机器 ID | 12 bit 序列号 |
- 41 位时间戳:毫秒级,可用 69 年
- 10 位机器 ID:支持 1024 台机器
- 12 位序列号:每毫秒 4096 个 ID
// snowflake.ts — 雪花算法发号器
const EPOCH = 1700000000000n // 自定义起始时间戳 (2023-11-14)
const MACHINE_BITS = 10n
const SEQUENCE_BITS = 12n
const MAX_MACHINE_ID = (1n << MACHINE_BITS) - 1n // 1023
const MAX_SEQUENCE = (1n << SEQUENCE_BITS) - 1n // 4095
class SnowflakeGenerator {
private machineId: bigint
private sequence = 0n
private lastTimestamp = -1n
constructor(machineId: number) {
if (machineId < 0 || machineId > Number(MAX_MACHINE_ID)) {
throw new Error(`机器 ID 必须在 0-${MAX_MACHINE_ID} 之间`)
}
this.machineId = BigInt(machineId)
}
nextId(): bigint {
let timestamp = BigInt(Date.now()) - EPOCH
// ⚠️ 时钟回拨检测 — 生产环境最常见的雪花算法故障
if (timestamp < this.lastTimestamp) {
const drift = this.lastTimestamp - timestamp
if (drift > 5n) {
throw new Error(`时钟回拨 ${drift}ms,拒绝生成 ID`)
}
// 5ms 以内等待追上
timestamp = this.lastTimestamp
}
if (timestamp === this.lastTimestamp) {
// 同一毫秒内,序列号递增
this.sequence = (this.sequence + 1n) & MAX_SEQUENCE
if (this.sequence === 0n) {
// 序列号溢出,等待下一毫秒
while (BigInt(Date.now()) - EPOCH <= this.lastTimestamp) {
// busy wait(实际生产中应 sleep)
}
timestamp = BigInt(Date.now()) - EPOCH
}
} else {
this.sequence = 0n
}
this.lastTimestamp = timestamp
return (timestamp << (MACHINE_BITS + SEQUENCE_BITS))
| (this.machineId << SEQUENCE_BITS)
| this.sequence
}
}
// 使用示例
const generator = new SnowflakeGenerator(1) // 机器 ID = 1
const id = generator.nextId()
console.log(`雪花 ID: ${id}`)
console.log(`Base62 短码: ${encode(id)}`)
// 输出类似: 雪花 ID: 12345678901234567 → Base62 短码: "dKz5bX7"
⚠️ 警告: 雪花算法最大的风险是时钟回拨。NTP 同步、闰秒、虚拟机迁移都可能导致系统时钟回退。上面的实现会在回拨超过 5ms 时直接拒绝生成 ID——这在生产环境中是正确的做法,宁可短暂停服也不要生成重复 ID。更高级的方案可以切换到预生成的备用 ID 池。
2.2 雪花 ID vs UUID vs ULID 选型对比
| 维度 | Snowflake | UUID v4 | UUID v7 | ULID |
|---|---|---|---|---|
| 长度 | 64 bit (8 字节) | 128 bit (16 字节) | 128 bit (16 字节) | 128 bit (16 字节) |
| 有序性 | ✅ 时间有序 | ❌ 完全随机 | ✅ 时间有序 | ✅ 时间有序 |
| Base62 长度 | 10-11 位 | 22 位 | 22 位 | 22 位 |
| 单机吞吐 | 409 万/秒/节点 | 无限制 | 无限制 | 无限制 |
| 分布式支持 | 需机器 ID 分配 | ✅ 天然无冲突 | ✅ 天然无冲突 | ✅ 天然无冲突 |
| 数据库友好 | ✅ B+Tree 友好 | ❌ 索引碎片化 | ✅ B+Tree 友好 | ✅ B+Tree 友好 |
| 短码长度 | 10 位 | 22 位 | 22 位 | 22 位 |
💡 提示: 短链服务选择雪花算法的核心原因只有一个——短。雪花 ID 的 Base62 编码只需 10-11 位,而 UUID/ULID 需要 22 位。这意味着在同样的短链长度限制下,雪花方案可以用更短的 URL 表示更多的链接。如果你的场景不需要极致短的 URL,UUID v7 是更好的选择——它天然无冲突、不需要机器 ID 分配、对数据库索引友好。
🚀 三、高并发重定向与缓存架构
3.1 301 vs 302 重定向:一个改变架构的决策
短链服务的读写比通常是 100:1 甚至更高——创建链接的请求远少于访问重定向的请求。因此,重定向的性能是整个系统的生命线。选择 301 还是 302,不仅是 HTTP 语义问题,更直接影响缓存策略和数据分析能力:
| 维度 | 301 Moved Permanently | 302 Found (推荐) |
|---|---|---|
| 浏览器行为 | 缓存重定向,下次直接请求目标 URL | 每次都请求短链服务器 |
| 服务端负载 | 低(浏览器缓存后不再访问) | 高(每次都经过服务端) |
| 点击追踪 | ❌ 无法统计(浏览器直接跳转) | ✅ 每次都能记录 |
| 长 URL 修改 | ⚠️ 已缓存的用户访问旧地址 | ✅ 实时生效 |
| SEO 影响 | 传递链接权重 | 不传递链接权重 |
| 适用场景 | 永久迁移、SEO 重要 | 短链服务(需要追踪) |
⚡ 关键结论: 短链服务必须使用 302。原因很简单——你需要统计每次点击。如果用 301,浏览器会缓存重定向,后续访问直接请求目标地址,你将完全丢失点击数据。Bitly、t.co、所有主流短链服务都使用 302。
3.2 多级缓存架构
短链重定向的核心是一个 key-value 查询:short_code → long_url。在日均千万级的访问量下,每次都查数据库是不现实的。一个典型的多级缓存架构如下:
请求 → Nginx/CDN 缓存 → 应用层 Redis → 本地 LRU Cache → 数据库
(L1) (L2) (L3) (L4)
// redirect-handler.ts — 多级缓存重定向处理器
import { LRUCache } from 'lru-cache'
class RedirectHandler {
// L3: 本地进程内 LRU 缓存(容量 10 万条,TTL 5 分钟)
private localCache = new LRUCache<string, string>({
max: 100_000,
ttl: 5 * 60 * 1000,
})
constructor(
private redis: any, // L2: Redis 客户端
private db: any, // L4: 数据库
) {}
async resolve(shortCode: string): Promise<{ url: string; status: number } | null> {
// L3: 本地缓存(~0.01ms)
const localHit = this.localCache.get(shortCode)
if (localHit) return { url: localHit, status: 302 }
// L2: Redis 缓存(~0.5-1ms)
const redisHit = await this.redis.get(`short:${shortCode}`)
if (redisHit) {
this.localCache.set(shortCode, redisHit)
return { url: redisHit, status: 302 }
}
// L4: 数据库查询(~5-10ms)
const dbHit = await this.db.query(
'SELECT long_url FROM short_links WHERE short_code = ?',
[shortCode]
)
if (!dbHit) return null
// 回填缓存
const longUrl = dbHit.long_url
await this.redis.setex(`short:${shortCode}`, 3600, longUrl) // Redis TTL 1 小时
this.localCache.set(shortCode, longUrl)
return { url: longUrl, status: 302 }
}
}
各级缓存的性能对比:
| 缓存层 | 命中延迟 | 容量 | 命中率(典型) | 一致性 |
|---|---|---|---|---|
| CDN/Nginx | ~0.1ms | 无限 | 30-50% | 最终一致 |
| 本地 LRU | ~0.01ms | 10 万条 | 80-90% | 进程内一致 |
| Redis | ~0.5ms | 100 万+ | 95-99% | 最终一致 |
| MySQL | ~5ms | 无限 | 100% | 强一致 |
⚡ 关键结论: 经过多级缓存优化后,99% 以上的重定向请求在 Redis 层就被处理,平均延迟 < 1ms。只有缓存穿透(恶意请求不存在的短码)才会打到数据库——这需要用布隆过滤器防护。
3.3 异步点击统计:不阻塞重定向路径
重定向路径上绝对不能做同步写操作(记录点击日志)。正确的做法是将点击事件发送到消息队列,由异步消费者处理:
// click-tracker.ts — 异步点击统计(不阻塞重定向)
import { EventEmitter } from 'events'
interface ClickEvent {
shortCode: string
timestamp: number
ip: string
userAgent: string
referer?: string
}
class ClickTracker extends EventEmitter {
private buffer: ClickEvent[] = []
private flushInterval: NodeJS.Timeout
constructor(private db: any, private batchSize = 100) {
super()
// 每 5 秒批量写入数据库
this.flushInterval = setInterval(() => this.flush(), 5000)
}
track(event: ClickEvent): void {
// 内存缓冲,零 I/O 阻塞
this.buffer.push(event)
// 缓冲满时立即刷新
if (this.buffer.length >= this.batchSize) {
this.flush()
}
}
private async flush(): Promise<void> {
if (this.buffer.length === 0) return
const batch = this.buffer.splice(0)
// 批量写入(比逐条 INSERT 快 10-50 倍)
const values = batch.map(e =>
`('${e.shortCode}', ${e.timestamp}, '${e.ip}', '${e.userAgent}')`
).join(',')
await this.db.query(
`INSERT INTO click_logs (short_code, timestamp, ip, user_agent) VALUES ${values}`
)
this.emit('flushed', batch.length)
}
destroy(): void {
clearInterval(this.flushInterval)
this.flush()
}
}
// 在重定向处理中使用
async function handleRedirect(req: any, res: any, handler: RedirectHandler, tracker: ClickTracker) {
const shortCode = req.params.code
const result = await handler.resolve(shortCode)
if (!result) {
res.writeHead(404, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: '短链接不存在' }))
return
}
// ⚠️ 关键:先发响应,再记录点击(fire-and-forget)
res.writeHead(302, { Location: result.url })
res.end()
// 异步记录,不 await,不影响响应延迟
tracker.track({
shortCode,
timestamp: Date.now(),
ip: req.headers['x-forwarded-for'] || req.socket.remoteAddress,
userAgent: req.headers['user-agent'] || '',
referer: req.headers['referer'],
})
}
⚠️ 警告: 点击统计永远不要放在重定向的同步路径上。一个
INSERT语句即使只有 5ms,在千万级 QPS 下也会把数据库打爆。正确做法是先返回 302 响应,然后异步批量写入。如果点击日志丢失一两条,对业务没有影响;但如果重定向延迟增加 5ms,用户体验会明显下降。
💡 四、完整服务搭建与部署架构
4.1 数据库 Schema 设计
-- schema.sql — 短链服务数据库设计
CREATE TABLE short_links (
id BIGINT PRIMARY KEY, -- 雪花 ID
short_code VARCHAR(10) NOT NULL UNIQUE, -- Base62 短码
long_url TEXT NOT NULL, -- 原始长 URL
type ENUM('auto', 'custom') DEFAULT 'auto',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expire_at TIMESTAMP NULL, -- 过期时间(NULL = 永不过期)
click_count BIGINT DEFAULT 0, -- 点击计数(异步更新)
creator_ip VARCHAR(45),
INDEX idx_short_code (short_code),
INDEX idx_long_url (long_url(255)), -- 前缀索引,避免全文索引
INDEX idx_expire (expire_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE click_logs (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
short_code VARCHAR(10) NOT NULL,
timestamp BIGINT NOT NULL,
ip VARCHAR(45),
user_agent TEXT,
referer TEXT,
INDEX idx_code_time (short_code, timestamp)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY RANGE (timestamp) (
PARTITION p2026_01 VALUES LESS THAN (UNIX_TIMESTAMP('2026-02-01') * 1000),
PARTITION p2026_02 VALUES LESS THAN (UNIX_TIMESTAMP('2026-03-01') * 1000),
PARTITION p2026_03 VALUES LESS THAN (UNIX_TIMESTAMP('2026-04-01') * 1000),
PARTITION p_future VALUES LESS THAN MAXVALUE
);
📌 记住:
click_logs表必须按时间分区。未分区的日志表在 3 个月后会超过 10 亿行,查询和删除过期数据都会成为性能噩梦。按月分区可以一键DROP PARTITION清理过期数据,比DELETE快 1000 倍。
4.2 分布式部署架构
┌─────────────────┐
│ CDN / Nginx │ ← 缓存热门短码的 302 响应
│ (Edge Cache) │
└────────┬────────┘
│
┌────────▼────────┐
│ 负载均衡器 │ ← 按短码一致性哈希路由
│ (Consistent │
│ Hashing) │
└────────┬────────┘
┌─────────┼─────────┐
┌──────▼──┐ ┌────▼────┐ ┌──▼──────┐
│ App #1 │ │ App #2 │ │ App #3 │ ← 无状态应用节点
│ (Node) │ │ (Node) │ │ (Node) │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
┌────▼───────────▼───────────▼────┐
│ Redis Cluster │ ← 短码 → URL 缓存
│ (3 主 3 从,16GB 内存) │
└────────────────┬───────────────┘
│
┌────────────────▼───────────────┐
│ MySQL / PostgreSQL │ ← 持久化存储
│ (主从复制,读写分离) │
└────────────────────────────────┘
关键设计决策:
- ✅ 应用节点无状态:所有缓存都在 Redis,应用节点可以随意扩缩容
- ✅ Redis Cluster 分片:按短码 hash 分片到不同节点,单节点不成为瓶颈
- ✅ 数据库读写分离:创建链接走主库,查询统计走从库
- ✅ CDN Edge Cache:热门短码(如营销链接)直接在 CDN 层返回 302
4.3 布隆过滤器防缓存穿透
恶意用户可以构造大量不存在的短码来穿透所有缓存层直达数据库。用布隆过滤器(Bloom Filter)在 Redis 层拦截不存在的请求:
// bloom-filter-guard.ts — 基于 Redis 的布隆过滤器防护
class BloomFilterGuard {
private filterName = 'short_code_bloom'
private initialized = false
async init(redis: any): Promise<void> {
// RedisBloom 模块提供的 BF.RESERVE 命令
// 误判率 0.1%,预计容量 1 亿条
try {
await redis.call('BF.RESERVE', this.filterName, '0.001', '100000000')
this.initialized = true
} catch (e) {
// 如果已存在,忽略
if (e instanceof Error && e.message.includes('exists')) {
this.initialized = true
}
}
}
async add(redis: any, shortCode: string): Promise<void> {
if (!this.initialized) return
await redis.call('BF.ADD', this.filterName, shortCode)
}
async mightExist(redis: any, shortCode: string): Promise<boolean> {
if (!this.initialized) return true // 未初始化时放行
const result = await redis.call('BF.EXISTS', this.filterName, shortCode)
return result === 1
}
}
// 在重定向处理中集成
async function handleRedirectSafe(
shortCode: string,
handler: RedirectHandler,
bloom: BloomFilterGuard,
redis: any,
): Promise<string | null> {
// L0: 布隆过滤器快速拒绝(~0.1ms)
const mightExist = await bloom.mightExist(redis, shortCode)
if (!mightExist) {
return null // 确定不存在,直接返回 404
}
// L2-L4: 正常缓存查询链路
const result = await handler.resolve(shortCode)
return result?.url || null
}
💡 提示: 布隆过滤器有假阳性(误判存在),但绝无假阴性(不会漏判)。也就是说,它可能会放过一些不存在的短码,但绝不会拦截真正存在的短码。对于防缓存穿透来说,这个特性正好合适——偶尔有几个穿透到数据库也没关系,关键是把 99.9% 的恶意请求挡在外面。
4.4 短链服务性能基准测试
基于上述架构,模拟 1000 QPS 的重定向压力测试结果:
| 测试场景 | 平均延迟 | P99 延迟 | 吞吐量 | 错误率 |
|---|---|---|---|---|
| 本地 LRU 命中 | 0.02ms | 0.05ms | 500,000 QPS | 0% |
| Redis 命中 | 0.8ms | 2.1ms | 120,000 QPS | 0% |
| 数据库查询 | 5.2ms | 15ms | 8,000 QPS | 0% |
| 布隆过滤器拒绝 | 0.1ms | 0.3ms | 200,000 QPS | 0% |
| 完整链路(混合) | 0.9ms | 3.5ms | 100,000 QPS | 0% |
⚠️ 五、避坑指南与最佳实践
5.1 生产环境常见陷阱
❌ 坑 1:使用 301 重定向
301 会被浏览器永久缓存,导致你丢失所有点击数据,且无法修改目标 URL。永远用 302。
❌ 坑 2:同步记录点击日志
在重定向路径上做数据库写入,会把 0.5ms 的重定向变成 10ms+,在高并发下直接打崩数据库。
❌ 坑 3:短码包含易混淆字符
0/O、1/l/I 在很多字体下无法区分。生产环境应从字符集中排除这些字符:
// 安全字符集(排除易混淆字符)
const SAFE_CHARSET = '23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ'
❌ 坑 4:不做 URL 校验
恶意用户可能提交 javascript:alert(1) 或内网地址 http://192.168.1.1/admin 作为目标 URL。必须校验:
function validateUrl(url: string): boolean {
try {
const parsed = new URL(url)
// 只允许 http/https
if (!['http:', 'https:'].includes(parsed.protocol)) return false
// 禁止内网地址
if (/^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|127\.|0\.|localhost)/.test(parsed.hostname)) {
return false
}
return true
} catch {
return false
}
}
❌ 坑 5:忽略过期链接清理
过期的短链如果不清理,数据库会无限膨胀。建议:
- 创建时设置
expire_at - 定时任务每天清理过期数据
- 布隆过滤器定期重建
5.2 生产级检查清单
- ✅ 使用 302 重定向,不要用 301
- ✅ 异步记录点击日志,不阻塞重定向
- ✅ 多级缓存:CDN → 本地 LRU → Redis → 数据库
- ✅ 布隆过滤器防缓存穿透
- ✅ 短码排除易混淆字符(0O1lI)
- ✅ URL 白名单校验(禁止 javascript:、内网地址)
- ✅ click_logs 按月分区,定期清理
- ✅ 雪花算法配置时钟回拨保护
- ✅ 自定义短码独立命名空间,防止冲突
- ✅ 监控:缓存命中率、重定向 P99 延迟、数据库 QPS
⚡ 关键结论: 短链服务的技术难度不在「能不能做」,而在「能不能撑住千万级流量」。核心优化思路是:让重定向路径尽可能短、尽可能快、尽可能少碰数据库。从 CDN 缓存 → 本地 LRU → Redis → 布隆过滤器 → 数据库,每一层都是在为下一层挡流量。99% 的请求应该在 Redis 层之前被处理掉。
相关工具推荐:
- 🔧 jsjson.com JSON 格式化工具 — 格式化 API 返回的 JSON 响应
- 🔧 jsjson.com Base64 编解码 — URL 编码/解码辅助工具
- 🔧 jsjson.com 时间戳转换 — 雪花 ID 时间戳解析
- 🔧 Redis — 分布式缓存首选
- 🔧 Cloudflare Workers — 边缘重定向,延迟更低