专栏首页JAVA葵花宝典你知道短URL服务是怎么设计的吗?

你知道短URL服务是怎么设计的吗?

前言

想必大家也经常收到垃圾短信吧... 短信中的链接一般都是短链接, 类似于下图这样:

为什么这里面的 url 都是短的呢? 有什么好处呢? 怎么做到的呢?

短 url 的好处有:

  1. 短. 短信和许多平台 (微博) 有字数限制, 太长的链接加进去都没有办法写正文了.
  2. 好看. 比起一大堆不知所以的参数, 短链接更加简洁友好.
  3. 方便做一些统计. 你点了链接会有人记录然后分析的.
  4. 安全. 不暴露访问参数.

这就是为什么我们现在收到的垃圾短信大多数都是短 URL 的原因了.

那么短 URL 是怎么做到的呢?

短 URL 基础原理

短 URL 从生成到使用分为以下几步.

  1. 有一个服务, 将要发送给你的长 URL 对应到一个短 URL 上. 例如 www.baidu.com->www.t.cn/1
  2. 把短 url 拼接到短信等的内容上发送.
  3. 用户点击短 URL, 浏览器用 301/302 进行重定向, 访问到对应的长 URL.
  4. 展示对应的内容.

本文主要集中于第一步, 即如何将一个长 URL 对应到短 URL 上.

服务设计

如果你在往长短 URL 真实的对应关系上想, 那么就走远了.

最理想的情况是: 我们用一种算法, 对每一个长 URL, 唯一的转换成短 URL. 还能保持反向转换的能力.

但是这是不可能的, 如果有这样的算法, 世界上的所有压缩算法都可以原地去世了.

正确的思路是建立一个发号器, 每次有一个新的长 URL 进来, 我们就增加一, 并且将新的数值返回. 第一个来的 url 返回 "www.x.cn/0", 第二个返回 "www.x.cn/1".

接下来以 QA 形式写几个小问题:

对应关系如何存储?

这个对应数据肯定是要落盘的, 不能每次系统重启就重新排号, 所以可以采用 mysql 等数据库来存储. 而且如果数据量小且 qps 低, 直接使用数据库的自增主键就可以实现.

如何保证长短链接一一对应?

按照上面的发号器策略, 是不能保证长短链接的一一对应的, 你连续用同一个 URL 请求两次, 结果值都是不一样的.

为了实现长短链接一一对应, 我们需要付出很大的空间代价, 尤其是为了快速响应, 我们可以需要在内存中做一层缓存, 这样子太浪费了.

但是可以实现一些变种的, 来实现部分的一一对应, 比如将最近 / 最热门的对应关系存储在 K-V 数据库中, 这样子可以节省空间的同时, 加快响应速度.

短 URL 的存储

我们返回的短 URL 一般是将数字转换成 32 进制, 这样子可以更加有效的缩短 URL 长度, 那么 32 进制的数字对计算机来说只是字符串, 怎么存储呢? 直接存储字符串对等值查找好找, 对范围查找等太不友好了.

其实可以直接存储 10 进制的数字, 这样不仅占用空间少, 对查找的支持较好, 同时还可以更加方便的转换到更多 / 更少的进制来进一步缩短 URL.

高并发

如果直接存储在 MySQL 中, 当并发请求增大, 对数据库的压力太大, 可能会造成瓶颈, 这时候是可以有一些优化的.

缓存

上面保证长短链接一一对应中也提到过缓存, 这里我们是为了加快程序处理速度. 可以将热门的长链接 (需要对长链接进来的次数进行计数), 最近的长链接(可以使用 redis 保存最近一个小时的) 等等进行一个缓存, 保存在内存中或者类似 redis 的内存数据库中, 如果请求的长 URL 命中了缓存, 那么直接获取对应的短 URL 进行返回, 不需要再进行生成操作.

批量发号

每一次发号都需要访问一次 MySQL 来获取当前的最大号码, 并且在获取之后更新最大号码, 这个压力是比较大的.

我们可以每次从数据库获取 10000 个号码, 然后在内存中进行发放, 当剩余的号码不足 1000 时, 重新向 MySQL 请求下 10000 个号码. 在上一批号码发放完了之后, 批量进行写入.

这样可以将对数据库持续的操作移到代码中进行, 并且异步进行获取和写入操作, 保证服务的持续高并发.

分布式

上面设计的系统是有单点的, 那就是发号器是个单点, 容易挂掉.

可以采用分布式服务, 分布式的话, 如果每一个发号器进行发号之后都需要同步给其他发号器, 那未必也太麻烦了.

换一种思路, 可以有两个发号器, 一个发单号, 一个发双号, 发号之后不再是递增 1, 而是递增 2.

类比可得, 我们可以用 1000 个服务, 分别发放 0-999 尾号的数字, 每次发号之后递增 1000. 这样做很简单, 服务互相之间基本都不用通信, 做好自己的事情就好了.

实现

由于我懒得写 JDBC 代码, 更懒得弄 Mybatis, 所以代码中使用到 MySQL 的地方都使用了 Redis.

package util;

import redis.clients.jedis.Jedis;

/**
 * Created by pfliu on 2019/06/23.
 */
public class ShortUrlUtil {

    private static final String SHORT_URL_KEY = "SHORT_URL_KEY";
    private static final String LOCALHOST = "http://localhost:4444/";
    private static final String SHORT_LONG_PREFIX = "short_long_prefix_";
    private static final String CACHE_KEY_PREFIX = "cache_key_prefix_";
    private static final int CACHE_SECONDS = 1 * 60 * 60;

    private final String redisConfig;
    private final Jedis jedis;

    public ShortUrlUtil(String redisConfig) {
        this.redisConfig = redisConfig;
        this.jedis = new Jedis(this.redisConfig);
    }

    public String getShortUrl(String longUrl, Decimal decimal) {
        // 查询缓存
        String cache = jedis.get(CACHE_KEY_PREFIX + longUrl);
        if (cache != null) {
            return LOCALHOST + toOtherBaseString(Long.valueOf(cache), decimal.x);
        }

        // 自增
        long num = jedis.incr(SHORT_URL_KEY);
        // 在数据库中保存短-长URL的映射关系,可以保存在MySQL中
        jedis.set(SHORT_LONG_PREFIX + num, longUrl);
        // 写入缓存
        jedis.setex(CACHE_KEY_PREFIX + longUrl, CACHE_SECONDS, String.valueOf(num));
        return LOCALHOST + toOtherBaseString(num, decimal.x);
    }

    /**
     * 在进制表示中的字符集合
     */
    final static char[] digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8',
            '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
            'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',
            'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};

    /**
     * 由10进制的数字转换到其他进制
     */
    private String toOtherBaseString(long n, int base) {
        long num = 0;
        if (n < 0) {
            num = ((long) 2 * 0x7fffffff) + n + 2;
        } else {
            num = n;
        }
        char[] buf = new char[32];
        int charPos = 32;
        while ((num / base) > 0) {
            buf[--charPos] = digits[(int) (num % base)];
            num /= base;
        }
        buf[--charPos] = digits[(int) (num % base)];
        return new String(buf, charPos, (32 - charPos));
    }

    enum Decimal {
        D32(32),
        D64(64);

        int x;

        Decimal(int x) {
            this.x = x;
        }
    }

    public static void main(String[] args) {

        for (int i = 0; i < 100; i++) {
            System.out.println(new ShortUrlUtil("localhost").getShortUrl("www.baidudu.com", Decimal.D32));
            System.out.println(new ShortUrlUtil("localhost").getShortUrl("www.baidu.com", Decimal.D64));
        }
    }
}

作者:呼延十 https://juejin.im/post/5d10ecab518825795a4d380e

-END-

本文分享自微信公众号 - JAVA葵花宝典(Javakhbd)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-09-25

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java 里的 for (;;) 与 while (true),哪个更快?

    其次,for (;;) 在Java中的来源。个人看法是喜欢用这种写法的人,追根溯源是受到C语言里的写法的影响。这些人不一定是自己以前写C习惯了这样写,而可能是间...

    JAVA葵花宝典
  • 【实战原创】SpringBoot应用docker化并发布到远程服务器

    首先编辑docker的宿主机文件/lib/systemd/system/docker.service

    JAVA葵花宝典
  • 一个历时五天的 Bug

    一个程序员在没有成长成为架构师之前,几乎都要跟 Bug为伴,程序员有很多时间都是花在了查找各种 Bug上。

    JAVA葵花宝典
  • 短URL服务的设计与实现

    作者:呼延十原文:https://juejin.im/post/5d10ecab518825795a4d380e

    黄泽杰
  • 短url服务的设计以及实现

    最理想的情况是: 我们用一种算法,对每一个长URL,唯一的转换成短URL.还能保持反向转换的能力.

    呼延十
  • URL

    URL是统一资源定位符的简称,它表示Internet上某资源的地址。通过URL我们可以访问网络上的各种资源。

    sr
  • 如何构建网站URL,使其更加百度搜索友好?

    相当于搜索引擎而言,URL对于百度蜘蛛的抓取、索引、排名显得格外重要,合理的配置URL,往往使你的SEO工作,事半功倍,相反,则是事倍功半。

    数据通20847430
  • URL 与网络资源分享

    这时候我突然反应过来,我过去其实一直没有区分清楚这里的概念的差别,World Wide Web,简称 WWW,中文“万维网”,又叫 Web,而不是 intern...

    zgq354
  • Flask | Flask基础 - URL与视图

    从新建的文件,我们已经看到,一个URL要与执行函数进行映射,使用的是@app.route装饰器。@app.route装饰器中,可以指定URL的规则来进行更加详细...

    咸鱼学Python
  • 搜索引擎优化 高级编程 PHP版 读书笔记

    http://www.cristiandarie.ro/ http://www.seoegghead.com/

    lilugirl

扫码关注云+社区

领取腾讯云代金券

玩转腾讯云 有奖征文活动