短链生成系统由浅入深及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)

相关推荐
光影少年10 分钟前
vue2与vue3的全局通信插件,如何实现自定义的插件
前端·javascript·vue.js
As977_11 分钟前
前端学习Day12 CSS盒子的定位(相对定位篇“附练习”)
前端·css·学习
susu108301891113 分钟前
vue3 css的样式如果background没有,如何覆盖有background的样式
前端·css
Ocean☾14 分钟前
前端基础-html-注册界面
前端·算法·html
Dragon Wu17 分钟前
前端 Canvas 绘画 总结
前端
CodeToGym21 分钟前
Webpack性能优化指南:从构建到部署的全方位策略
前端·webpack·性能优化
~甲壳虫22 分钟前
说说webpack中常见的Loader?解决了什么问题?
前端·webpack·node.js
~甲壳虫26 分钟前
说说webpack proxy工作原理?为什么能解决跨域
前端·webpack·node.js
Cwhat28 分钟前
前端性能优化2
前端
熊的猫1 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js