Nuxt 3 + Redis 实战:高并发短链接服务从零到部署全解析

本文旨在帮助开发者快速理解短链接服务的完整实现逻辑。文章结合文字说明与核心代码示例,让读者能边看逻辑边理解实现方式。


一、整体流程概览

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,自动过期

五、短码生成逻辑

文字说明

  1. 对 URL + 时间戳 + 重试次数做 HMAC-SHA256

  2. 使用 Base62 编码

  3. 截取 6~8 位作为短码

  4. 使用 Redis Lua 脚本原子写入

    • 主索引:sl:{code} → longUrl
    • 去重索引:url:{hash} → code
  5. 冲突自动重试

核心代码

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 限流计数

九、总结逻辑

  1. POST /v1/links

    • URL 校验 → IP 限流 → HMAC + Base62 → Redis Lua 原子写入 → 返回短码
  2. GET /:short_code

    • 短码格式校验 → Redis 查询 → 302 跳转或 404
  3. Redis 单例

    • 整个服务复用一个 Redis 客户端,避免重复连接
  4. IP 限流 & 去重

    • Redis Key + TTL 管理,防刷和重复生成短码
相关推荐
老程序员刘飞38 分钟前
node.js 和npm 搭建项目基本流程
前端·npm·node.js
歪歪1001 小时前
在C#中除了按属性排序,集合可视化器还有哪些辅助筛选的方法?
开发语言·前端·ide·c#·visual studio
wangbing11251 小时前
开发指南139-VUE里的高级糖块
前端·javascript·vue.js
半桶水专家2 小时前
Vue 3 动态组件详解
前端·javascript·vue.js
csj502 小时前
前端基础之《React(6)—webpack简介-图片模块处理》
前端·react
我有一棵树2 小时前
避免 JS 报错阻塞 Vue 组件渲染:以 window.jsbridge 和 el-tooltip 为例
前端·javascript·vue.js
Fanfffff7202 小时前
前端样式局部作用域:从Scoped到CSS Modules 的完整指南
前端·css
前端大神之路2 小时前
vue2 模版编译原理
前端
00后程序员张2 小时前
Web 前端工具全流程指南 从开发到调试的完整生态体系
android·前端·ios·小程序·uni-app·iphone·webview
凌泽2 小时前
写了那么多年的代码,我开始写“规范”了:AI 驱动的开发范式革命
前端·vibecoding