短链生成系统由浅入深及Nest.js实现

引言

最近在自己做 GPTs 的朋友可能面临一些传播上的痛点,比如 GPTs 的分享链接过长,难以转化,也看不到相关传播数据等。针对这一问题,一个名为 Dub 的工具应运而生,它不仅能够将冗长的 GPTs 链接转换成短链,还提供了多项增值服务,如自定义域名、链接点击数据分析、个性化分享图片和文案、二维码生成,甚至包括位置和设备定位数据的收集。

这个工具的基础就是一个短链生成系统,可以将无信息、冗长的原链接转化为易于识别、传播的短链接。Dub 的官网首页中展示了该功能,Dub 提供了几个域名,如 chatg.ptdub.sh 等,用户就可以将自己的 GPTs 转化为 chatg.pt 开头的短链,如下图:

同时,Dub 也支持非 GPTs 链接的转化,例如,我的掘金主页链接为:juejin.cn/user/994372... 。可以被转换为: dub.sh/ayyo-cici

这种短链转换在短信中也很常见: 短信中使用短链的好处主要有:

  1. 长度限制:短链接大幅度减少了字符使用量,使得链接能够轻松适应短信等字符数有限的环境。
  2. 简洁性:相比之下,短链接由于其简短性,看起来更加整洁和易于理解。长链接因其冗长和复杂性,往往给人一种混乱和难以理解的感觉。
  3. 安全隐私:通过短链接,我们可以隐藏原始链接中的跟踪参数和其他敏感信息,从而提高了链接分享的安全性。
  4. 增值服务:短链接不仅仅是长度上的压缩,它们还可以携带额外的服务,如点击次数统计、访问者来源分析等。这对于跟踪营销效果、用户行为分析等方面非常有用。

正好最近阅读了神光老师的掘金小册 《Nest通关秘籍》 ,了解了短链生成系统的实现过程。接下来我们将深入探讨短链生成技术的各个方面,从基础原理到具体实现,特别是如何利用Nest.js这一流行的JavaScript服务端开发框架来构建一个短链生成服务。

实现思路

短链接展示的逻辑

这里的重点是重定向:

  • 301永久重定向:用户第一次访问短链时,会被重定向到长链接。以后直接访问长链接,不再访问短链服务。这种方式短链服务压力小一点。
  • 302临时重定向:每次访问短链都会先访问短链服务,然后被重定向到长链接。这种方式允许短链服务记录链接的访问次数和其他数据。

一般短链服务都是用 302 来重定向,便于分析如pv、uv、ip等网站数据。

短链接产生的逻辑

1. 递增ID与Base62结合

这种方法简单明了:每个新链接都分配一个唯一的、递增的数字ID,比如1, 2, 3,以此类推。这些ID通常存储在数据库中。

现在,让我们引入Base62。这是一种编码方法,使用了26个小写字母、26个大写字母和10个数字,总共62个字符。这可以将我们的数字ID转换成Base62编码,就像是用一种更紧凑的语言重新编写编号。比如,数字123456在Base62编码中可能是一个简单的三字符串,比如"w7e"。选择base62主要有两个优点:

1.高密性。base62编码中,每个位置的字符可以有62种可能的值,这意味着即使是较短的字符串也能表示非常大的数值。例如,两位Base62字符串就有62^2,即3844种可能的组合。

2.兼容性。之前常常使用的base64相比base62会多两个额外符号(通常是+/)。然而特定字符(如+/)可能会引起问题。例如,+字符在URL中经常被解释为空格,而/字符在路径中有特殊意义,还需要进行额外的处理。

这种方法的优点在于它的简单性和保证了短链接的唯一性。但缺点也很明显:由于ID是递增的,因此生成的短链接也是可预测的。如果你知道了一个编号,就很容易猜到下一个编号一样。在安全性方面,这可能不是最佳选择,因为有心人可能会尝试通过修改链接来访问未授权的内容。

2. 基于URL哈希的Base62压缩码

接下来让我们看看第二种方法:使用URL的哈希值。哈希,简单来说,就是将一段信息(在这里是URL)转换成固定长度的、看似随机的字符串。这就像是对每个URL制作一个独特的指纹。

在这个方法中,我们先将URL通过哈希函数处理,然后取这个哈希值的一部分(比如前8个字符),最后将这部分转换为Base62编码。这样做的好处是生成的短链接看起来是随机的,不像递增ID那样可预测。然而,这种方法有一个潜在的问题:碰撞。碰撞是指两个不同的URL可能产生相同的哈希值。虽然这种情况出现的概率不高,但它确实存在,这意味着两个不同的URL可能最终指向同一个短链接。

3. 随机字符串生成与查重

最后一种方法是随机生成字符串。我们生成一个随机的字符序列(比如长度为6的字符串),然后检查这个序列是否已经被分配给了另一个URL。如果没有,它就成为新URL的短链接。

这种方法的优点是它既不可预测,也不容易产生碰撞。

但缺点在于性能方面。随着可用字符串的数量减少,查找未使用的字符串变得越来越耗时。

一种解决方法是批量生成大量短链接并存储起来,当需要时再分配给新的URL。这样,生成压缩码的方案就完美了。

Nest.js实现

实际操作中,当用户访问短链时,服务根据短链编码从数据库中查询对应的长URL。然后,服务根据选择的重定向方式(301或302)将用户重定向到原始的长URL。接下来,我们就尝试用代码实现。

1. 创建NestJS项目:

使用命令nest new short-url创建一个新的NestJS项目。

2. 搭建数据库环境:

使用Docker运行MySQL数据库。

使用MySQL Workbench或其他数据库客户端连接数据库。

3. 使用TypeORM连接数据库:

安装依赖@nestjs/typeormtypeorm,以及MySQL驱动mysql2

js 复制代码
npm install --save @nestjs/typeorm typeorm mysql2

AppModule中配置TypeORM以连接MySQL数据库。

4. 创建实体(Entity)

  • 实体的作用:在数据库中创建对应的表格,用于存储数据。

  • 实体定义

    • UniqueCode:存储生成的唯一压缩码和其使用状态。
    • ShortLongMap:存储短链和长链的映射关系,以及创建时间。

代码示例

js 复制代码
// UniqueCode 实体
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity()
export class UniqueCode {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ length: 10, comment: '压缩码' })
  code: string;

  @Column({ comment: '状态, 0 未使用、1 已使用' })
  status: number;
}

// ShortLongMap 实体
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';

@Entity()
export class ShortLongMap {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ length: 10, comment: '压缩码' })
  shortUrl: string;

  @Column({ length: 200, comment: '原始URL' })
  longUrl: string;

  @CreateDateColumn()
  createTime: Date;
}

5. 生成唯一压缩码并插入表中

  • 核心方法 :生成随机的Base62字符串作为压缩码。先随机生成一个介于 0 到 61 之间的整数 num 。使用 base62.encode(num) 将整数 num 转换为对应的 base62 字符,并将它添加到 str 中。
  • 实现逻辑:随机生成一个长度为6的字符串,然后检查数据库中是否已存在此压缩码,若不存在则使用,否则重新生成。
  • 优化 :一个一个增加太麻烦,可以提前批量生成的方式,如凌晨四点统一生成一批,可以使用@nestjs/schedule 库跑定时任务实现。

代码示例

js 复制代码
import * as base62 from "base62/lib/ascii";

export function generateRandomStr(len: number) {
  let str = '';
  for(let i = 0; i < len; i++) {
    const num = Math.floor(Math.random() * 62);
    str += base62.encode(num);
  }
  return str;
}
js 复制代码
import { Injectable } from '@nestjs/common';
import { InjectEntityManager } from '@nestjs/typeorm';
import { EntityManager } from 'typeorm';
import { UniqueCode } from './entities/UniqueCode';
import { generateRandomStr } from './utils';

@Injectable()
export class UniqueCodeService {
  constructor(@InjectEntityManager() private entityManager: EntityManager) {}

  // 生成新的压缩码
  async generateCode() {
    let str = generateRandomStr(6); // 生成6位的随机压缩码
    let uniqueCode = await this.entityManager.findOneBy(UniqueCode, { code: str });

    // 如果压缩码已存在,则重新生成
    if (uniqueCode) {
      return this.generateCode();
    } 

    // 压缩码不存在,保存新生成的压缩码
    const newCode = new UniqueCode();
    newCode.code = str;
    newCode.status = 0; // 0 表示未使用
    await this.entityManager.save(newCode);

    return newCode;
  }

  // 在凌晨 4 点左右批量插入一堆,比如一次性插入 10000 个。
  // 这里我们是每次 insert 一个,你也可以每次 insert 10 个 20 个这种
  // 批量插入性能会好,因为执行的 sql 语句少。这里我们就先不优化了。
  @Cron(CronExpression.EVERY_DAY_AT_4AM)
    async batchGenerateCode() {
        for(let i = 0; i< 10000; i++) {
            this.generateCode();
        }
    }
}

6. 映射短链与长链

  • 功能实现:在接收到长URL后,生成对应的短链。

  • 操作步骤

    1. 从数据库获取未使用的压缩码。
    2. 将长链与获取到的压缩码进行映射,存入ShortLongMap表。
    3. UniqueCode表中,将使用过的压缩码标记为已使用。

代码示例

js 复制代码
@Injectable()
export class ShortLongMapService {
  // ...省略依赖注入部分...
  
  async generate(longUrl: string) {
    let uniqueCode = await this.entityManager.findOneBy(UniqueCode, { status: 0 });
    if (!uniqueCode) {
      uniqueCode = await this.uniqueCodeService.generateCode();
    }
    const map = new ShortLongMap();
    map.shortUrl = uniqueCode.code;
    map.longUrl = longUrl;
    await this.entityManager.insert(ShortLongMap, map);
    await this.entityManager.update(UniqueCode, { id: uniqueCode.id }, { status: 1 });
    return uniqueCode.code;
  }
}

7. 实现URL重定向

  • 关键功能:当用户访问短链时,将其重定向到对应的长链。

  • 实现方式

    1. 根据请求的短链编码查询数据库,获取对应的长链。
    2. 使用HTTP 302状态码实现重定向。返回长链及302即可

代码示例

js 复制代码
@Controller()
export class AppController {
  // ...省略依赖注入部分...

  @Get(':code')
  @Redirect()
  async redirectToLongUrl(@Param('code') code) {
    const longUrl = await this.shortLongMapService.getLongUrl(code);
    if (!longUrl) {
      throw new BadRequestException('短链不存在');
    }
    return { url: longUrl, statusCode: 302 };
  }
}

8. 扩展功能(可选)

  1. 记录每次短链的访问数据,如访问次数、访问者信息等。
    实现思路:在重定向逻辑中添加数据记录步骤,可以在数据库中创建新的于存储表格用这些信息。
  2. 避免缓存穿透,防范恶意用户疯狂请求不存在的短链接。
    实现思路:使用布隆过滤器过滤掉不存在的数据请求。

主要参考文章:

面试官说:你来设计一个短链接生成系统吧 - 掘金 (juejin.cn)

Nest 通关秘籍 - zxg_神说要有光 - 掘金小册 (juejin.cn)

相关推荐
AlienZHOU5 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
Captaincc8 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师10 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen10 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒11 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow11 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林81811 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang8007811 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明11 小时前
第 6 章 组件进阶
前端·vue.js
江华森11 小时前
HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析
前端