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 管理,防刷和重复生成短码
相关推荐
MediaTea10 分钟前
Python 第三方库:lxml(高性能 XML/HTML 解析与处理)
xml·开发语言·前端·python·html
西陵14 分钟前
Nx带来极致的前端开发体验——使用MF进行增量构建
前端·javascript·架构
Nicholas6826 分钟前
flutter滚动视图之ProxyWidget、ProxyElement、NotifiableElementMixin源码解析(九)
前端
JackieDYH1 小时前
vue3中reactive和ref如何使用和区别
前端·javascript·vue.js
ZZHow10242 小时前
React前端开发_Day4
前端·笔记·react.js·前端框架·web
前端开发爱好者2 小时前
弃用 html2canvas!快 93 倍的截图神器
前端·javascript·vue.js
ss2732 小时前
手写MyBatis第39弹:深入MyBatis BatchExecutor实现原理与最佳实践
前端·javascript·html
leon_teacher2 小时前
HarmonyOS权限管理应用
android·服务器·前端·javascript·华为·harmonyos
lumi.3 小时前
HarmonyOS image组件深度解析:多场景应用与性能优化指南(2.4详细解析,完整见uniapp官网)
前端·javascript·小程序·uni-app·html·css3
OEC小胖胖3 小时前
动态UI的秘诀:React中的条件渲染
前端·react.js·ui·前端框架·web