腾讯女后端设计了一套短链系统,当场就想给她offer!

Hi,你好,我是Java

如下图,对于这种客评短信,相信大家并不陌生,通过点击短信里“蓝色字体”跳转到一个具体的网页。腾讯女后端设计了一套短链系统,当场就想给她offer!

其实,上图中那串蓝色字符,有个美丽的专业术语:短链。它可以是一个 URL地址,也可以是一个二维码。而整个跳转流程的背后是一套完整的短链系统,因此,今天我们来一起分析:如何设计一套高性能短链系统。

为什么需要短链?

存在即合理,这里列举 3个主要原因。

1. 相对安全

短链不容易暴露访问参数,生成方式可以完全迎合短信平台的规则,能够有效地规避关键词、域名屏蔽等风险,而原始 URL地址,很可能因为包含特殊字符被短信系统误判,导致链接无法跳转。

2. 美观

对于精简的文字,似乎更符合美学观念,不太让人产生反感。

3. 平台限制

短信发送平台有字数限制,在整条短信字数不变的前提下,把链接缩短,其他部分的文字描述就能增加,这样似乎更能达到该短信的实际目的(比如,营销)。

短链的组成

如下图,短链的组成通常包含两个部分:域名 + 随机码

腾讯女后端设计了一套短链系统,当场就想给她offer!

短链的域名最好和其他业务域名分开,而且要尽量简短,可以不具备业务含义(比如:xyz.com),因为短链大部分是用于营销,可能会被三方平台屏蔽。

短链的随机码需要全局唯一,建议 10位以下。

短链的跳转原理

首先,我们先看一个短链跳转的简单例子,如下代码,定义了一个 302重定向的代码示例:

import org.Springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.servlet.view.RedirectView;

@Controller
public class RedirectController {

  @GetMapping("/{shortCode}")
  public RedirectView redirect(@PathVariable String shortCode) {
    String destUrl = "https://yuanjava.com";
    // destUrl = getDestUrlByShortCode(shortCode); //真实的业务逻辑
    return new RedirectView(destUrl);
  }

接着,在浏览器访问短链”http://127.0.0.1:8080/s2TYdWd” 后,请求会被重定向到 https://yuanjava.com ,下图为浏览器控制台信息:

腾讯女后端设计了一套短链系统,当场就想给她offer!

从上图,我们看到了 302状态码并且请求被 Location到另外一个 URL,整个交互流程图如下:

腾讯女后端设计了一套短链系统,当场就想给她offer!

是不是有一种偷梁换柱的感觉???

最后,总结下短链跳转的核心思想:

  1. 生成随机码,将随机码和目标 URL(长链)的映射关系存入数据库;

  2. 用域名+随机码生成短链,并推送给目标用户;

  3. 当用户点击短链后,请求会先到达短链系统,短链系统根据随机码查找出对应的目标 URL,接着将请求 302重定向到目标 URL(长链)

关于重定向有 301 和 302两种,如何选择?

  • 302,代表临时重定向:每次请求短链,请求都会先到达短链系统,然后重定向到目标 URL(长链),这样,方便短链系统做一些统计点击数等操作;通常采用 302

  • 301,代表永久重定向:第一次请求拿到目标长链接后,下次再次请求短链,请求不会到达短链系统,而是直接跳转到浏览器缓存的目标 URL(长链),短链系统只能统计到第一次访问的数据;一般不采用 301。

 如何生成短链?

从短链组成章节可知:短链=域名+随机码,因此,如何生成短链的问题变成了如何生成一个全局唯一的随机码,通常会有 3种做法:

1. Base62

Base62 表示法是一种基数为62的数制系统,包含26个英文大写字母(A-Z),26个英文小写字母(a-z)和10个数字(0-9)。这样,共有62个字符可以用来表示数值。如下代码:

import java.security.SecureRandom;

public class RandomCodeGenerator {
  private static final String CHAR_62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
  private static final SecureRandom random = new SecureRandom();

  public static String generateRandomCode(int length) {
    StringBuilder sb = new StringBuilder(length);
    for (int i = 0; i < length; i++) {
      int rndCharAt = random.nextInt(CHAR_62.length());
      char rndChar = CHAR_62.charAt(rndCharAt);
      sb.append(rndChar);
    }
    return sb.toString();
  }
}

对于 Base62算法,如果是生成 6位随机数有 62^6 – 1 = 56800235583(568亿多),如果是生成 7位随机数有 62^7 – 1 = 3521614606208(3.5万亿多),足够使用。

2. Hash算法

Hash算法是我们最容易想到的办法,比如 MD5, SHA-1, SHA-256, MurmurHash, 但是这种算法生成的 Hash值还是比较长,常用的做法是把这个 Hash值进行 62/64进行压缩。

如下代码,通过 Google的 MurmurHash算法把长链 Hash成一个 32位的 10进制正数,然后再转换成62进制(压缩),这样就可以得到一个 6位随机数,

import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import java.nio.charset.StandardCharsets;

public class MurmurHashToBase62 {

    private static final String BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    public static String toBase62(int value) {
        StringBuilder sb = new StringBuilder();
        while (value > 0) {
            sb.insert(0, BASE62.charAt(value % 62));
            value /= 62;
        }
        return sb.toString();
    }
    public static void main(String[] args) {
        // 长链
        String input = "https://yuanjava.cnposts/short-link-system/design?code=xsd&page=1";
        // 长链利用 MurmurHash算法生成 32位 10进制数
        HashFunction hashFunction = Hashing.murmur3_32();
        int hash = hashFunction.hashString(input, StandardCharsets.UTF_8).asInt();
        if (hash < 0) {
            hash = hash & 0x7fffffff// Convert to positive by dropping the sign bit
        }
        // 将 32位 10进制数 转换成 62进制
        String base62Hash = toBase62(hash);
        System.out.println("base62Hash:" + base62Hash);
    }
}


3. 全局唯一 ID

比如,很多大中型公司都会有自己全局唯一 ID 的生成服务器,可以使用这些服务器生成的 ID来保证全局唯一,也可以使用雪花算法生成全局唯一的ID,再经过 62/64进制压缩。

 如何解决冲突?

对于上述 3种方法的前 2种:Base62 或者 Hash算法,因为本质上都是哈希函数,所以,不可避免地会产生哈希冲突,尽管冲突的概率已经很低很低了,so,万一冲突了,该如何解决呢?

要解决冲突,首先需要检测 Hash冲突,通常来说有 2种检测方法。

数据库锁

这里以 MySQL数据库为例,表结构如下:

CREATE TABLE `short_url_map` (
  `id` int(11unsigned NOT NULL AUTO_INCREMENT,
  `long_url` varchar(160DEFAULT NULL COMMENT '长链',
  `short_url` varchar(10DEFAULT NULL COMMENT '短链',
  `gmt_create` int(11DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`),
  UNIQUE INDEX 'short_url' ('short_url')
ENGINE=InnoDB DEFAULT CHARSET=utf8;

首先创建一张长链和短链的关系映射表,然后通过给 short_url字段添加唯一锁,这样,当数据插入时,如果存在 Hash冲突(short_url值相等),数据库就会抛错,插入失败,因此,可以在业务代码里捕获对应的错误,这样就能检测出冲突。

也可以先用 short_url去查询,如果能查到数据,说明 short_url存在 Hash冲突了。

对于这种通过查询数据库或者依赖于数据库唯一锁的机制,因为都涉及DB操作,对数据库是一个额外的开销,如果流量比较大的话,需要保证数据库的性能。

布隆过滤器

在 DB操作的上游增加一个布隆过滤器,在长链生成短链后, 先用短链在布隆过滤器中进行查找,如果存在就代表冲突了,如果不存在,说明 DB里不存在此短链,可以插入。对于布隆过滤器的选择,单机可以采用 Google的布隆过滤器,分布式可以使用 RedisBloom。

整体流程可以抽象成下图:

腾讯女后端设计了一套短链系统,当场就想给她offer!

检测出了冲突,需要如何解决冲突?

再 Hash,可以在长链后面拼接一个 UUID之类的随机字符串,然后再次进行 Hash,用得出的新值再进行上述检测,这样 Hash冲突的概率又大大大降低了。

并发场景的架构

在流量不大的情况,上述方法怎么折腾似乎都合理,但是,为了架构的健壮性,很多时候需要考虑高并发,大流量的场景,因此架构需要支持水平扩展,比如:

  • 采用微服务

  • 功能模块分离,比如,短链生成服务和长链查询服务分离

  • 功能模块需要支持水平扩容,比如:短链生成服务和长链查询服务能支持动态扩容

  • 缓解数据库压力,比如,分区,分库分表,主从,读写分离等机制

  • 服务的限流,自保机制

  • 完善的监控和预警机制

这里给出一套比较完整的设计思路图:

腾讯女后端设计了一套短链系统,当场就想给她offer!

总结

本文通过一个客服评价的短信开始,分析了短链的构成,短链跳转的原理,同时也给出了业内的一些实现算法,以及一些架构上的建议。

对于业务体量小的公司,可以根据成本来搭建服务(单机或者少量服务器做负载),对于业务体量比较大的公司,更多需要考虑到高并发的场景,如何保证服务的稳定性,如何支持水平扩展,当服务出现问题时如何具备一套完善的监控和预警服务器。

其实,很多系统都是在一次又一次的业务流量挑战下成长起来的,我们需要不断打磨自己宏观看架构,微观看代码的能力,这样自己也就着业务,系统一起成长起来了


最后,希望本文可以给你带来收获和思考,如果有任何疑问,欢迎评论区留言讨论。如果本文对你有帮助,请帮忙点赞或转发,想获取更多技术好文和面试经,请关注公众号

-THE End-

  

点击下方名片,阅读更多技术好文


原创好文推荐
美团 2面:为什么 G1能够替代 CMS回收器?看完这篇你就懂了!
肝了一周,彻底弄懂了 CMS收集器原理,这个轮子造的真值!
9款常见的 JVM垃圾回收器
美团一面:Git 是如何工作的?(推荐阅读)
阿里 P7二面:Redis 执行 Lua,能保证原子性吗?
工作不好找,简历该怎么写?

原文始发于微信公众号(猿java):腾讯女后端设计了一套短链系统,当场就想给她offer!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/257135.html

(0)
李, 若俞的头像李, 若俞

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!