引言
最近在自己做 GPTs 的朋友可能面临一些传播上的痛点,比如 GPTs 的分享链接过长,难以转化,也看不到相关传播数据等。针对这一问题,一个名为 Dub 的工具应运而生,它不仅能够将冗长的 GPTs 链接转换成短链,还提供了多项增值服务,如自定义域名、链接点击数据分析、个性化分享图片和文案、二维码生成,甚至包括位置和设备定位数据的收集。
这个工具的基础就是一个短链生成系统,可以将无信息、冗长的原链接转化为易于识别、传播的短链接。Dub 的官网首页中展示了该功能,Dub 提供了几个域名,如 chatg.pt、dub.sh 等,用户就可以将自己的 GPTs 转化为 chatg.pt 开头的短链,如下图:
同时,Dub 也支持非 GPTs 链接的转化,例如,我的掘金主页链接为:juejin.cn/user/994372... 。可以被转换为: dub.sh/ayyo-cici
这种短链转换在短信中也很常见: 短信中使用短链的好处主要有:
- 长度限制:短链接大幅度减少了字符使用量,使得链接能够轻松适应短信等字符数有限的环境。
- 简洁性:相比之下,短链接由于其简短性,看起来更加整洁和易于理解。长链接因其冗长和复杂性,往往给人一种混乱和难以理解的感觉。
- 安全隐私:通过短链接,我们可以隐藏原始链接中的跟踪参数和其他敏感信息,从而提高了链接分享的安全性。
- 增值服务:短链接不仅仅是长度上的压缩,它们还可以携带额外的服务,如点击次数统计、访问者来源分析等。这对于跟踪营销效果、用户行为分析等方面非常有用。
正好最近阅读了神光老师的掘金小册 《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/typeorm
和typeorm
,以及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后,生成对应的短链。
-
操作步骤:
- 从数据库获取未使用的压缩码。
- 将长链与获取到的压缩码进行映射,存入
ShortLongMap
表。 - 在
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重定向
-
关键功能:当用户访问短链时,将其重定向到对应的长链。
-
实现方式:
- 根据请求的短链编码查询数据库,获取对应的长链。
- 使用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. 扩展功能(可选)
- 记录每次短链的访问数据,如访问次数、访问者信息等。
实现思路:在重定向逻辑中添加数据记录步骤,可以在数据库中创建新的于存储表格用这些信息。 - 避免缓存穿透,防范恶意用户疯狂请求不存在的短链接。
实现思路:使用布隆过滤器过滤掉不存在的数据请求。
主要参考文章: