短链接是怎么设计的?带你入门


前言

说到 URL 你肯定不陌生,浏览器输入一段 URL,立马就跳转到你想要的网站,不过你应该也遇到过一些带了很多参数、特别长的 URL,看起来就乱糟糟的,能不能把它变短一点?

首先你要知道的是,长链是没法压缩成短链的,那我们这里怎么设计短链?

答案是:映射

将需要的长链生成对应的短链,你可以把这个映射关系放在缓存、也可以放在数据库。然后,每次访问短链的时候,都需查到这个对应关系,并重定向到真正长链接对应的网址。

短链设计本身很简单,不过现在的需求量非常大,这个量级上去了,简单的东西都会变得复杂,因此,本文从简入深探讨锻炼设计原理与方法。


一、短链

URL 短链,就是把原来较长的网址,转换成比较短的网址,比如这样:https://s.xxx.com/1WB5A3。那这样设计的好处?

首先,网址短、美观、便于发布、传播,可以写更多有意义的文字。

其次,省钱省成本:在短信中,如果含长网址的短信内容超过 70 字,就会被拆成两条发送,而用短链则可能一条短信就搞定,如果短信量大也可以省下不少钱。

再则,易识别:我们平常看到的二维码,本质上也是一串 URL ,如果是长链,对应的二维码会密密麻麻,扫码的时候机器很难识别,而短链则不存在这个问题。

另外,出于安全考虑,不想让有意图的人看到原始网址。

1、原理

1.1 短链生成原理

获取短链:

短链标志(flag)长度一般 5 - 7位,一般由数字、字母(可选择区分大小写)等字符组成。有几种常见的获取方式:

1)截取前缀:我们可以从众多 hash 算法中选择一种(比如 MD5),将 url 经过 hash 计算之后得到一串字符,然后截取前缀 5 - 7位即可。

案例如下:

    public static String toHex(String url) {
        String hash = Md5.hash(url);
        return hash.substring(0, 6);
    }

2)除法取模:选择一种 hash 算法可以输出 32 位、64位 … 的整数 x,对字符集合大小取模,得到下标 id,记录下来,从指定的字符编码集合中找到对应字符,循环该操作直到 x 小于编码集大小。

这里的 hash 算法我们选择比较有代表性的 MurMurHash,是一种简单、高效、散列均匀的算法,众多开源框架中都采用了这种算法,比如 Redis。

这种算法不用你自己去实现,很多工具包已经帮你实现了,我们这里采用 Hutool 工具包里的实现,如下:

int hash = HashUtil.murmur32(url.getBytes());

然后将整数散列值通过 除法取模 运算并从指定字符编码集选定字符输出,如下:

    private static String to62HEX(int num) {
        num = Math.abs(num);
        String chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

        StringBuilder sb = new StringBuilder();
        int remainder;

        while (num > 62 - 1) {
            remainder = Long.valueOf(num % 62).intValue();
            sb.append(chars.charAt(remainder));
            num = num / 62;
        }

        sb.append(chars.charAt(Long.valueOf(num).intValue()));
        return sb.reverse().toString();
    }

解决短链碰撞:

所谓碰撞就是出现了重复,短链重复了肯定不行,因为映射关系需要唯一对应。

你也知道,很多常见的散列算法输出的散列值,都有可能出现极小概率冲突,尤其是我们这里只有 5 - 7 位,冲突概率就进一步加大,怎么解决?考虑短链的应用场景,我们介绍常见的一种方式:

加随机串,多次计算,直到找到没有冲突的短链:

    // 已被占用,则增加随机延长串进行重新计算
    for (int i = 0; i < 10; i++) {
        flag = genShortLink(link + RandomStringUtils.randomAlphabetic(10));
        if (shortLinkDao.findByFlag(flag).isEmpty()) {
            break;
        }
    }

以上是根据我们目前的场景设计,循环 10 次基本够用,如果你的场景不够用,你可以考虑酌情优化。

1.2 短链跳转原理:

当你从浏览器输入短链地址并回车时,浏览器地址栏立马出现类似于这样一长串的地址:

https://wx36fb82dbb481a6a2.shop.xxxxxx.com.cn/shop/nw0f7c60fd7000/success/19515?type=source&sign=01FB60A657676C6905C84F8C90A1DA99&timestamp=1678366800

这个地址才是短链真正的目标地址。也就是说,短链不是真正的目标网址,我们需要重定向找到短链映射的目标地址,怎么找?从哪里找?

还是那句话,完全取决于你的场景选择,如果你的用量非常大,设计一个单独的短链服务并独立部署也是很有必要的。

当然,如果你的用量一般,直接在基础服务上设计短链接口也是没有问题的。

然后,重定向?请求一般是先通过网关,所以,在网关,需要识别到当前是 短链 请求,然后直接调用接口查询短链的映射地址,并给客户端返回重定向信息。

具体我这里给一个例子:

在这里插入图片描述

访问流程基本就以下几点:

  • 1)客户端通过浏览器访问短链

  • 2)服务端返回 301 / 302 重定向码并携带 location= 原链接

  • 3)客户端浏览器重新访问原链接

2、设计:

前文我们讲到了短链的基本原理,实际场景变化万千,我们来看看通常情况下,你在设计短链的时候需要考虑到哪些问题。

2.1 短链需求

1)唯一:给定原始的长 URL ,短链服务能生成比它短且唯一的 URL,即 短链

2)映射:用户点击短链 , 能跳转到原始的长 URL

3)过期:短链经过一定时间后,会过期

4)REST API:接口需要设计成 REST API

5)过滤:有些关键字眼,比如 NSSBM 比如这种,可酌情考虑过滤掉。

2.2 考虑的问题?

1)过期:短链一般都不是长期有效的,3个月、6个月、一年 … 处理方式很多,你可以定期手动删除、任务定时检查删除 …

2)过滤:有些客户可能对特殊的字眼比较敏感,比如 SB、SC、MD … 可以酌情考虑过滤掉。

3)安全:短链不可被预测,否则简单的遍历就能把所有短链都遍历完,空耗系统资源。因此,常见的递增主键的方式一般不能采用。

4)高性能:生成短链的过程,以及从短链跳转到原始 URL 要近实时,你可以考虑预先生成足够多的短链、映射关系缓存等等 …

5)高可用:服务不能存在单点故障,如果你的请求量比较大或者要求低容错率,一般你需要考虑集群部署,分散单节点的压力。

6)系统容量预估:需要提前预估你的需求场景,尤其是量大的时候,如何快速存、快速取是一个设计要点。

7)系统扩展:这个一般也是量大的时候需要考虑。

二、实践案例

这里我们看看如何快速设计一个可线上使用的短链服务。

1、设计表:

CREATE TABLE `t_short_link` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `flag` varchar(10) NOT NULL COMMENT '短链标识',
  `link` varchar(2048) NOT NULL COMMENT '原始链接',
  `md5` varchar(64) NOT NULL COMMENT '原始链接的md5',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `deleted` tinyint(4) DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_flag` (`flag`),
  UNIQUE KEY `uk_md5` (`md5`)
) 

其中,flag 就是我们的短链标志,通过 flag 可以找出具体的映射的 link(url),然后重定向访问 link 即可。

另外,这里我们记录了原始链接的 MD5 值,有什么作用?你想,如果你想要判断某个长链接是否已经存在,要去对比原链接吗?太麻烦,直接用 MD5 值来判断,非常方便。

最后,还是建议你从数据库层面确保短链的唯一性,因此,我们这里将 flag、md5 字段设置成了唯一键,确保不会出现重复。

2、生成短链:

业务层面还是比较简单了,主要有查询判断、去重、以及散列冲突解决。

    public String createShortLink(Params params) {
        String link=params.getLink();
        String host=params.getHost();
        String md5 = DigestUtils.md5DigestAsHex(link.getBytes(StandardCharsets.UTF_8));
        Optional<ShortLink> shortLinkOptional = shortLinkDao.findByMd5(md5);
        // 已存在,直接返回
        if (shortLinkOptional.isPresent()) {
            return getShortLink(shortLink, host);
        }
        
        // 加锁避免相同链接并发处理
        final RLock lock = redissonClient.getLock(CacheConstants.LOCK_SHORT_LOCK_CREATE + md5);
        try {
            if (lock.tryLock(3, TimeUnit.SECONDS)) {
                // 二次检测
                shortLinkOptional = shortLinkDao.findByMd5(md5);
                if (shortLinkOptional.isPresent()) {
                    return getShortLink(shortLinkOptional.get(), host);
                }
                
                String flag = "";
                flag = genShortLink(link);
                if (shortLinkDao.findByFlag(flag).isPresent()) {
                    // 已被占用,则增加随机延长串进行重新计算
                    for (int i = 0; i < 10; i++) {
                        flag = genShortLink(link + RandomStringUtils.randomAlphabetic(10));
                        if (shortLinkDao.findByFlag(flag).isEmpty()) {
                            break;
                        }
                    }
                }
                ShortLink shortLink = new ShortLink(flag, link, md5);
                shortLinkDao.save(shortLink);
                return getShortLink(shortLink, host);
            }
        } catch (Exception e) {
            if (e instanceof InterruptedException) {
                Thread.currentThread().interrupt();
            }
            log.error("[short_link] 短链生成失败", e);
            
            throw new BizException(ErrorEnum.SHORT_LINK_FAIL);
        } finally {
            if (lock.isHeldByCurrentThread() && lock.isLocked()) {
                lock.unlock();
            }
        }
        return link;
    }

生成短链,核心就是要生成一个不重复的 短链标志(flag),一般短链的 flag 会设计成 5、6 位由数字、字母组成的随机字符串,原则上长度越长,冲突的概率越小。

具体生成短链的方法我们前面已经提过了,我们简单回顾下:

    public static String genShortLink(String link) {
        return to62HEX(HashUtil.murmur32(link.getBytes()));
    }

至此,一个基本可用短链能够满足一般的场景了 …

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柏油

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值