本文旨在帮助开发者快速理解短链接服务的完整实现逻辑。文章结合文字说明与核心代码示例,让读者能边看逻辑边理解实现方式。
一、整体流程概览
1. 创建短链流程
yaml
客户端 POST /v1/links
|
v
读取请求体 -> URL 校验 -> IP 限流
|
v
生成短码 (HMAC + Base62 + Redis Lua 原子写入)
|
v
返回 JSON { short_code, short_url }
2. 访问短链流程
yaml
客户端 GET /:short_code
|
v
短码格式校验 -> Redis 查询 sl:{short_code}
|
+----+----+
| |
存在 不存在
| |
v v
302 重定向 404 Not Found
二、Redis 单例客户端
避免每次请求都创建 Redis 连接,使用 单例模式:
javascript
// server/utils/redis.ts
import { createClient } from 'redis'
let client: ReturnType<typeof createClient> | null = null
export function redis() {
if (!client) {
client = createClient({ url: process.env.REDIS_URL || 'redis://localhost:6379' })
client.on('error', (err) => console.error('Redis Error:', err))
client.connect().then(() => console.log('Redis connected'))
}
return client
}
- 第一次调用
redis()
会创建并连接客户端 - 后续请求直接复用已连接的客户端
三、URL 校验与 IP 获取
javascript
// server/utils/validate.ts
import { parse } from 'url'
export function assertValidUrl(input: string) {
const url = parse(input)
if (!['http:', 'https:'].includes(url.protocol || '')) throw new Error('Invalid URL protocol')
if (!url.host) throw new Error('Missing host')
if (['localhost', '127.0.0.1', '::1'].includes(url.hostname || '')) throw new Error('Disallowed host')
return new URL(input)
}
// 获取客户端 IP(可用于限流)
export function clientIp(event) {
return event.node.req.headers['x-forwarded-for']?.toString().split(',')[0] || event.node.req.socket.remoteAddress || 'unknown'
}
四、IP 限流逻辑
typescript
// server/utils/rate-limit.ts
export async function rateLimitPerIp(r, ip: string, limit: number, windowSec: number) {
const key = `rl:${ip}:${Math.floor(Date.now() / (windowSec * 1000))}`
const count = await r.incr(key)
if (count === 1) await r.expire(key, windowSec)
const allowed = count <= limit
const ttl = await r.ttl(key)
return { allowed, remaining: Math.max(0, limit - count), resetIn: ttl >= 0 ? ttl : windowSec }
}
- 固定窗口计数
- 超过次数返回 429
- Redis 存储 TTL,自动过期
五、短码生成逻辑
文字说明
-
对 URL + 时间戳 + 重试次数做 HMAC-SHA256
-
使用 Base62 编码
-
截取 6~8 位作为短码
-
使用 Redis Lua 脚本原子写入:
- 主索引:
sl:{code} → longUrl
- 去重索引:
url:{hash} → code
- 主索引:
-
冲突自动重试
核心代码
typescript
// server/utils/codegen.ts
import { createHmac } from 'node:crypto'
import { toBase62, clampCode } from './base62'
import { useRuntimeConfig } from '#imports'
export async function generateAndStoreCode(r, longUrl: string, opts?: { codeLength?: number, maxRetries?: number, dedupeHash?: string }) {
const { codeSecret } = useRuntimeConfig()
const codeLength = opts?.codeLength ?? 7
const maxRetries = opts?.maxRetries ?? 5
const dedupeHash = opts?.dedupeHash ?? null
for (let i = 0; i <= maxRetries; i++) {
const h = createHmac('sha256', codeSecret)
.update(longUrl)
.update(String(Date.now()))
.update(String(i))
.digest()
const code = clampCode(toBase62(h), codeLength)
// 原子写入 Redis
const ok = await r.evalsha(
LUA_SCRIPT_SHA,
2,
`sl:${code}`,
dedupeHash ? `url:${dedupeHash}` : '',
longUrl,
'0'
)
if (ok === 1) return code
}
throw createError({ statusCode: 500, statusMessage: 'Failed to allocate short code' })
}
六、创建短链路由示例
scss
export default defineEventHandler(async (event) => {
const r = redis()
const ip = clientIp(event)
const rl = await rateLimitPerIp(r, ip, 10, 60)
if (!rl.allowed) throw createError({ statusCode: 429 })
const body = await readBody<{ url?: string }>(event)
if (!body?.url) throw createError({ statusCode: 400 })
const parsed = assertValidUrl(body.url)
const normalized = parsed.toString()
const dedupeHash = createHash('sha256').update(normalized).digest('hex')
const code = await generateAndStoreCode(r, normalized, { codeLength: 7, maxRetries: 7, dedupeHash })
const { baseUrl } = useRuntimeConfig()
return { short_code: code, short_url: `${baseUrl.replace(//$/, '')}/${code}` }
})
七、访问短链路由示例
csharp
export default defineEventHandler(async (event) => {
const { short_code } = event.context.params
if (!/^[0-9A-Za-z]{6,8}$/.test(short_code)) throw createError({ statusCode: 404 })
const r = redis()
const url = await r.get(`sl:${short_code}`)
if (!url) throw createError({ statusCode: 404 })
setResponseStatus(event, 302, 'Found')
setResponseHeader(event, 'Location', url)
return 'Redirecting...'
})
八、Redis Key 设计逻辑
Key | Value | 用途 |
---|---|---|
sl:{code} | longUrl | 短码 → 原始 URL |
url:{hash} | code | URL 去重 |
rl:{ip}:{win} | count | IP 限流计数 |
九、总结逻辑
-
POST /v1/links:
- URL 校验 → IP 限流 → HMAC + Base62 → Redis Lua 原子写入 → 返回短码
-
GET /:short_code:
- 短码格式校验 → Redis 查询 → 302 跳转或 404
-
Redis 单例:
- 整个服务复用一个 Redis 客户端,避免重复连接
-
IP 限流 & 去重:
- Redis Key + TTL 管理,防刷和重复生成短码