短链接服务系统设计

功能简单描述:

功能很简单,实现将长网址缩短的功能,如:
在这里插入图片描述
短链服务总的来说,就做两件事:

  • 将长链接变为短链接,当然是越短越好
  • 用户点击短链接的时候,实现自动跳转到原来的长链接
长链转短链

在转短链的时候,我们其实就是要将一个长长的链接映射为只有 4 到 7 个字母的字符串。这里我用了 MySQL 来存储,存放 short_key 和 original_url 的记录。

数据表很简单,最主要的列有以下几个:

  • id: 逻辑主键,BIGINT
  • short_key: 短链中的字符串,域名部分一般不需要加进去,加入唯一索引 unique
  • original_url: 原长网址,限 256 字符
  • 另外,基于业务需要,可以加入业务标识 biz、过期时间 expire_time 等。

在生成 key 的时候,一种最简单的实现方式是使用随机字符串,因为是随机码,所以可能会出现失败,通常就需要重试。随着记录越来越多,就越容易发生 key 重复的情况,这种方案显然不适合数据量大的场景。

我们不容易保证我们随机生成的 key 不重复,但是我们容易实现的就是 id 不重复,我们只要想个办法把 id 和 key 一一对应起来就可以了。

单表场景,直接使用数据库自增 id 就能实现 id 唯一。多库多表,大家肯定都有一个全局发号器来生成唯一 id。

直接将 id 放在短链上可以吗?

功能上是没有问题的,不过问题就是还是会太长,然后由于 id 通常都是基本自增的,会引发很多问题,如被别人用一个简单的脚本给遍历出来。

接下来,我们讨论怎么将 id 变为 key。

在短链中,我们通常可以使用的字符有 a-z、A-Z 和 0-9 共 62 个字符,所以,接下来,我们其实就是要将 10 进制的 id 转换为 62 进制的字符串。

转换方法很简单,大家都学过二进制和十进制之间的转换,这里贴下简单的实现:

private static final String BASE = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

public static String toBase62(long num) {
    StringBuilder sb = new StringBuilder();
    int targetBase = BASE.length();
    do {
        int i = (int) (num % targetBase);
        sb.append(BASE.charAt(i));
        num /= targetBase;
    } while (num > 0);

    return sb.reverse().toString();
}

这样,十进制的 id 总是能生成一个唯一的 key,同样地,我们也可以通过 key 还原出 id。

在分库分表的时候,我们可以选择使用 id 来做分表键,也可以使用 key 来做分表键。如果是使用 id 的话,因为前端过来都是 key,所以需要先将 key 转换为 id。

短链转长链

这一步非常简单,用户点击我们发给他们的短信中的短链,请求发送到我们的解析系统中,我们根据 key 到数据库中找原来的长链接,然后做个 302 跳转就可以了。

细节优化

1、加入随机码

62 进制用更短的字符串能表示更大的数,使得我们可以使用更少的字符,同时不会让用户直接知道我们的 id 大小,但是稍微懂一点技术的,很容易就能将 62 进制转换为 10 进制,在行家眼里,和直接使用 id 没什么区别。

这里简单介绍下我的做法,使得生成的 key 不那么有规律,不那么容易被遍历出来。

我们得到 id 以后,先在其二进制表示的固定位置插入随机位。如上图所示,从低位开始,每 5 位后面插入一个随机位,直到高位都是 0 就不再插入。

一定要对每个 id 进行一样的处理,一开始就确定下来固定的位置,如可以每 4 位插一个随机位,也可以在固定第 10 位、第 17 位、第 xx 位等,这样才能保证算法的安全性:两个不一样的数,在固定位置都插入随机位,结果一定不一样。

这里我贴下我的插入随机位实现:

private static long insertRandomBitPer5Bits(long val) {
    long result = val;
    long high = val;
    for (int i = 0; i < 10; i++) {
        if (high == 0) {
            break;
        }
        int pos = 5 + 5 * i + i;
        high = result >> pos;
        result = ((high << 1 | RandomUtils.nextInt(0, 2)) << pos)
                | (result & (-1L >>> (64 - pos)));
    }
    return result;
}

这样,我们 10 进制的 999998 和 999999 就可能被转换为 16U06 和 XpJX。因为有随机位的存在,所以会有好几种可能。到这里,是不是觉得生成出来的字符串就好多了,相邻的两个数出来的两个字符串没什么规律了。

另外,建议 id 从一个中等模式的大小开始,如 100w,而不是从 1 开始,这个应该很好理解。

2、加入缓存

为了提高效率,我们应该使用适当的缓存,在系统中,我分别使用了一个读缓存和一个写缓存。

通常,我们使用读缓存 (key => originalUrl) 可以获得很多好处,大家想想,如果我们往一批用户的手机发送同一个短链,可能大家都是在收到短信的几分钟内打开链接的,这个时候读缓存就能大大提高读性能。

至于写请求,接口来了一个 originalUrl,我们不能去数据库中查询是否已经有这条记录,所以两条一模一样的链接我们会生成两个不一样的短链接,当然,通常我们也是允许这种情况的。

由于存在短时间内使用两条一模一样的长链接拿过来转短链的情况,所以我们可以维护一个写缓存 (originalUrl => key),这里使用 originalUrl 做键,如设置最大允许缓存最近 10000 条,过期时间 1 小时,根据自己实际情况来设置即可。这里写缓存能不能提高效率,取决于我们的业务。

由于生成短链的接口一般是提供给其他各个业务系统使用的,所以其实可以由调用方来决定是否要使用写缓存,这样能得到最好的效果。如果调用方知道自己接下来需要批量转换的长链是不会重复的,那么调用方可以设置不使用缓存,而对于一般性的场景,默认开启写缓存。

性能分析

这个系统非常简单,性能瓶颈其实都集中在数据库中,前面我们也说了可以通过缓存来适当提高性能。

这里,我们不考虑缓存,来看下应该怎么设计数据库和表。

首先,我们应该预估一个适当的量,如按照自己的业务规模,预估接下来 2 年或更长时间,大概会增长到什么量级的数据。

如预估未来可能需要存放 50-100 亿条记录,然后我们大概按照单表 1000w 数据来设计,那么就需要 500-1000 张表,那么我们可以定 512 张表,512 张表我们可以考虑放 2 个或 4 个库。

我们使用 key 来做分表键,同时在 key 上加唯一索引,对于单表 1000w 这种级别,查询性能一般都差不了。

我测试环境中使用单库 2 张表,在不使用缓存的情况下,写操作可以比较轻松地达到 3000 TPS,基本上也就满足我们的需求了。本来测试环境各种硬件资源就和生产环境没法比,更何况我们生产环境会设置多库多表来分散压力。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值