短URL服务设计

news2024/12/23 1:12:14

引言

在营销系统里,为了增加系统的活跃用户数,经常会有各种各样的营销活动。这类活动几乎都是为了充分利用存量用户的价值,促使他们分享产品或App以达到触达到更多用户的目的。又或者是出于营销目的,群发优惠券触达短信这种场景。

分享App活动页(或其他各种页面)时URL一般会带有各种参数,比如分享者fromUserId,涉及活动campaignId,分享源头类型sourceType等,还有其他各种和具体业务场景挂钩的参数,一个URL动辄300~400个字符。显然,这么长的URL看得头大,而且除了开发者或黑客可能没有人关心URL里具体有什么参数。期望分享出去的URL是一些更短、更易于阅读的短URL。

假设使用阿里云短信发送平台。在群发短信的场景下,假设需要发送10w条短信。短信付费有按数量付费,和套餐包两种形式,一般而言,在待发送的短信数量较大时,套餐包比按量付费来得优惠。大致看一下套餐包价格表:
在这里插入图片描述
还是一笔不小的开支。

因此,如果要把活动链接URL几百个字符通过短信来发送,明显会超过短信发送长度限制。阿里云的短信内容有长度限制:
在这里插入图片描述
就算不考虑短信付费问题。在短信内容超长后需拆分发送的规则下,长链接URL被拆分为多个短信发送,URL会被截断,就不是正确的URL。

上面洋洋洒洒说了这么多,甚至有愚蠢的嫌疑,无非是想引出本文的主题。

我们需要一个端URL服务。当短信触达用户(被分享用户)点击这个短URL时,可以重定向访问到原始的长URL地址。比如https://t.cn/A12afD就是我随便写的一个短链,其中https://t.cn/是新浪微博申请的域名。后面的6位随机字符就是精简后的短URL,区分大小写,使用a-z,A-Z,0-9共62个字符。严谨来说,长URL对应的短URL实际上指的是A12afD;域名(https://t.cn/)+精简后端URL字符(A12afD)共同组成一个可点击的链接地址。

简介

短URL服务,也有叫做短链服务,短链接生成器。当客户端(服务请求者,发起方,调用者)请求端URL服务时,短URL服务返回一个或多个短URL。用户浏览器(PC、App、H5等)点击URL时,可以自动访问原始长URL对应的目标服务。

一个简单的涉及到4个模块(子系统)的时序图如下:
在这里插入图片描述
流程分析:对于需要展示短URL的应用程序,由该应用调用短URL生成器生成短URL,并将该短URL展示给用户,用户在浏览器中点击该短URL时,请求发送到短URL生成器(短URL生成器以HTTP服务器的方式对外提供服务,短URL域名指向短URL生成器),短URL生成器返回HTTP重定向响应,将用户请求重定向到最初的原始长URL,浏览器访问长URL服务器,完成请求服务。

又是一个显而易见,短URL和长URL之间必须存在一一匹配的映射关系,只有在满足这种关系后,点击短URL才能跳转到想要的长URL。存储这种映射关系的数据结构就是键值对HashMap。

总结一下,短链的优势:

  1. 减少文本长度,常用于发布微博或Twitter(有字数限制),短信群发(减少短信发送成本)等场景
  2. 可阅读性:相比于长链接里一大堆不知所以的参数,短链接更加简洁友好
  3. 安全:不暴露访问参数。当然短链接转换为长链接后,小部分用户(开发者)还是能分析出参数的
  4. 长链接在有些平台上无法自动识别为超链接
  5. URL与二维码。长链接在生成二维码时更密集一些;短链接在生成二维码时更稀疏。一般而言,越稀疏的二维码,识别扫描效率更高,尤其是在光线不好、二维码部分遮挡或图像质量较差的情况下。

Hash算法

将一个原始的长URL经过某种计算生成对应的,唯一的短URL的过程就是Hash算法。所谓唯一,指的是长URL1和长URL2经过Hash计算后不会生成相同的短URL。因此短URL服务的核心之一是一款效率高(计算速度快)和冲突概率低(不冲突最好,但不太实际)的算法。

MurmurHash就是这样一种算法,Redis,Google Guava,Apache commons-codec内部都有这种算法的实现。

public class HashUtil {
    private static final int c1 = 0xcc9e2d51;
    private static final int c2 = 0x1b873593;
    private static final int r1 = 15;
    private static final int r2 = 13;
    private static final int m = 5;
    private static final int n = 0xe6546b64;

    private static final int DEFAULT_SEED = 0;

    /**
     * TODO:直接搬自Google Guava的MurmurHash 32bits生成算法(还有64位,128位)
     */
    public static int hash32(String str) {
        return hash32(str.getBytes());
    }

    private static int hash32(byte[] key) {
        int hash = HashUtil.DEFAULT_SEED;
        final int len = key.length;
        int i = 0;
        int k = 0;
        for (; i + 4 <= len; i += 4) {
            k = ((key[i + 3] & 0xff) << 24)
                    | ((key[i + 2] & 0xff) << 16)
                    | ((key[i + 1] & 0xff) << 8)
                    | (key[i] & 0xff);
            k *= c1;
            k = Integer.rotateLeft(k, r1);
            k *= c2;
            hash ^= k;
            hash = Integer.rotateLeft(hash, r2);
            hash = hash * m + n;
        }
        int k1 = 0;
        switch (len - i) {
            case 3:
                k1 = (key[i + 2] & 0xff) << 16;
            case 2:
                k1 |= (key[i + 1] & 0xff) << 8;
            case 1:
                k1 |= key[i] & 0xff;
                k1 *= c1;
                k1 = Integer.rotateLeft(k1, r1);
                k1 *= c2;
                hash ^= k1;
        }
        hash ^= len;
        hash ^= hash >>> 16;
        hash *= 0x85ebca6b;
        hash ^= hash >>> 13;
        hash *= 0xc2b2ae35;
        hash ^= hash >>> 16;
        return hash;
    }

    /**
     * 转换为62进制字符串, 即包含a-z, A-Z, 0-9共62个字符
     */
	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();
    }

    public static void main(String[] args) {
		int i = hash32("https://google.com/");
        String s = to62Hex(i);
		// 输出:1027772095 17yqth
        System.out.println(i + " " + s);
    }
}

https://google.com/经过hash32方法计算后的结果为1027772095,这是一串十进制的整型值,再经过to62Hex方法转化为62进制字符串得到17yqth。

Hash冲突

Hash冲突是无法避免的,但在业务上一定不能有多个长URL映射为同一个短URL的情况存在。

为了防止数据库中出现相同短链,用刚生成的短链作为查询条件去数据库中查询看是否有相同的短链,如果有则需要比对这个短链对应的长链与正在处理的长链地址是否一样。如果一样,说明传入的长链是重复的,则此短链可直接复用,不用再存储一次。

如果不一样,则说明发生冲突,此时可以给长链加一串特殊的字符,然后再进行Hash运算,如果此次不冲突则可以将长链和这个特殊字符拼接起来和短链一并存储到数据库中下次接受到短链请求后,把特殊字符截取掉,然后再进行重定向。

判断优化

长URL和短URL的映射关系是存储于数据库中,判断是否发生Hash冲突时需要查询数据库,这样数据库或许会成为性能评价。

很自然想到引入分布式缓存集群。此外还有布隆过滤器。

服务设计

一道频繁出现的面试题:如何设计一个十亿级、高性能、高可用的短URL服务?

需求分析:

  1. 使用几位随机字符
  2. 是否需要考虑URL的过期时间,即经过多长时间后,URL不能转换为对应的长URL
  3. 是否需要考虑URL(及对应的营销文案)的点击率
  4. 是否需要考虑支持用户自定义短URL?即是否支持用户自定义(调用方或客户端)短URL。用户可以指定一个长URL对应的短URL内容,只要这个短URL还没有被使用

字符个数

使用62进制字符串,1个字符可以表示62种结果,2个字符可以表示62*62=3844种结果。以此类推,5个字符可以表示916132832种结果,约9亿,即可以代表9亿种不同的长URL,理论上并且在实际上足够使用。目前应该没有哪家公司有这么大的实际需求量,并且短URL是可以回收的(技术可行性上)。

在做架构设计时,架构师们总是习惯于冗余设计;5个字符和6个字符,几乎没有什么技术上的区别。鉴于面试官的题目是十亿级别的短URL服务,因此5个字符不够用。6个字符可以表示56800235584种结果,约568亿个不同的URL。

所以,我们看到的短URL一般都是6个62位二进制字符。

分库分表

如果使用MySQL这类关系型数据库存储,则需要考虑分库分表。如果使用非关系型数据库,如HBase,也需要做集群化部署保证数据库系统高可用。

短URL有效期

数据库存储长、短URL映射关系时,除了这两个核心字段,还可以考虑增加最后访问时间字段,短URL被访问时,则更新此字段。然后可考虑以一个月为执行频率,在凌晨短URL服务集群负载低时,发起定时调度任务,分批查询数据库节点,扫描获取最近1年(或2年)没有被访问(最后访问时间是1/2年前)的短URL,做逻辑删除,回收利用。

短链跳转

如果需要对营销活动进行数据分析,则需要做页面埋点,页面埋点肯定只能对长链接进行埋点,因为长链接里URL携带有各种参数。

浏览器请求短URL,短URL到达短链服务器,短链服务器返回长URL后,浏览器访问长URL的动作叫重定向,重定向有301和302两类。

如果要做页面埋点,做数据分析,做营销活动效果可视化等一系列任务,则推荐使用302这种方式。

301和302的区别:

  • 301:代表永久重定向,也就是说第一次请求拿到长链接后,下次浏览器再去请求短链的话,不会向短网址服务器请求,而是直接从浏览器的缓存里拿,这样在server层面就无法获取到短网址的点击数,如果这个链接刚好是某个活动的链接,也就无法分析此活动的效果。
  • 302:代表临时重定向,也就是说每次去请求短链都会去请求短网址服务器(除非响应头中有Cache-Control或Expired指示使用浏览器缓存),这样就便于server统计点击数,用302会给server增加一点压力,但在数据异常重要的今天,这点统计代码和性能损失是值得的。

短URL生成方式

长URL通过某种函数,计算得到一个6个字符的短URL。

Hash

将长URL利用MD5或SHA256等单项散列算法,进行Hash计算,得到128bit或256bit的Hash值。然后对该Hash值进行Base64编码,得到22个或43个Base64字符,再截取前面的6个字符,就得到短URL。
在这里插入图片描述
但是这样得到的短URL,可能会发生Hash冲突(MD5或SHA256计算得到的Hash值几乎不会冲突,但Base64编码后再截断的6个字符有可能会冲突)。所以在生成时,需要先校验该短URL是否已经映射为其他的长URL,如果是,那么需要重新计算(换单向散列算法,或者换Base64编码截断位置)。重新计算得到的短URL依然可能冲突,需要再重新计算。但是这样的冲突处理需要多次到存储中查找 URL,性能损耗比较严重。

自增长

一种免冲突的算法是用自增长自然数来实现,即维持一个自增长的二进制自然数,然后将该自然数进行Base64编码即可得到一系列的短URL。这样生成的的短URL必然唯一,而且还可以生成小于6个字符的短URL,如自然数0的Base64编码是字符A,则用https://1.cn/A作为短URL。

但这种算法将导致短URL是可猜测的,如果某个应用在某个时间段内生成一批短URL,则这批短URL就会集中在一个自然数区间内。只要知道了其中一个短URL,就可以通过自增(以及自减)的方式请求访问其他URL。不允许短URL可预测。

预生成

预先生成一批没有冲突的短URL字符串,当外部请求输入长URL需要生成短URL时,直接从预先生成好的短URL字符串池中获取一个即可。

采用随机数来实现,6个字符每个字符都用随机数产生(用0~63的随机数产生一个Base64编码字符)。为了避免随机数产生的短URL冲突,需要在预生成的时候检查该URL是否已经存在(采用布隆过滤器检查)。因为预生成短URL是离线的,所以这时不会有性能方面的问题。

架构

一个可供参考的架构如下图所示:
在这里插入图片描述
短URL预加载服务器此前已经从短URL预生成文件服务器(HDFS)中加载一批短URL存放在自己的内存中,这时只需要从内存中返回一个短URL即可,同时将短URL与长URL的映射关系存储在HBase数据库中,时序图如下图所示:
在这里插入图片描述
对于用户通过客户端请求访问短URL的过程(即输入短URL,请求返回长URL),请求通过负载均衡服务器发送到短URL服务器集群,短URL服务器首先到缓存服务器中查找是否有该短URL,如果有,立即返回对应的长URL,短URL生成服务器构造重定向响应返回给客户端应用。

如果缓存没有用户请求访问的短URL,短URL服务器将访问HBase短URL数据库服务器集群。如果数据库中存在该短URL,短URL服务器会将该短URL写入缓存服务器集群,并构造重定向响应返回给客户端应用。若HBase中没有该短URL,短URL服务器将构造404响应返回给客户端应用,时序图如下图所示:
在这里插入图片描述
回到需求分析部分:

  • 为了保证系统高可用,上面的架构图里的应用服务器、文件服务器、数据库服务器都采用集群部署方案
  • 为了满足高性能要求,引入Redis缓存集群。80%以上的访问请求将被设计为通过缓存返回

标准Base64编码表

在这里插入图片描述
其中+/在URL中会被编码为%2B以及%2F,而%在写入数据库时又和SQL编码规则冲突,需要进行再编码,因此直接使用标准Base64编码进行短URL编码并不合适。URL 保留字符编码表如下
在这里插入图片描述
所以需要针对URL场景对Base64编码进行改造,使用URL保留字符表以外的字符对Base64编码表中的62,63进行改造:将+改为-,将/改为_

参考

  • 高性能短链设计

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1830035.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何将扫描的 PDF 转换为 Word

您是否正在寻找一种可靠且轻松的方式将扫描的 PDF 文档转换为可编辑的 Word 文件&#xff1f;要将 PDF 转换为可编辑的 Word 文档&#xff0c;神奇之处在于光学字符识别(OCR)。 使用 PDFgear&#xff0c;您可以无缝地将扫描的 PDF 转换为 Word&#xff0c;无论是在线还是离线。…

nginx+tomcat+nfs →web集群部署

nginxtomcatnfs →web集群部署 一.安装前介绍 NGINX是一个高性能的Web服务器和反向代理服务器。它能够处理静态内容&#xff0c;缓存请求结果&#xff0c;以及将请求转发给后端服务器。通过反向代理&#xff0c;NGINX能够实现请求的负载均衡、安全性增强、SSL加密等功能。此外…

k8s上使用ConfigMap 和 Secret

使用ConfigMap 和 Secret 实验目标&#xff1a; 学习如何使用 ConfigMap 和 Secret 来管理应用的配置。 实验步骤&#xff1a; 创建一个 ConfigMap 存储应用配置。创建一个 Secret 存储敏感信息&#xff08;如数据库密码&#xff09;。在 Pod 中挂载 ConfigMap 和 Secret&am…

youlai-boot项目的学习—工程构建与运行

开发环境 系统:mac OS Ventura 13.2.1 终端: item2 Homebrew: 4.3.5 IDE: IntelliJ IDEA 2024.1.1 (Ultimate Edition) 代码分支 仓库&#xff1a;https://gitee.com/youlaiorg/youlai-boot.git 分支&#xff1a; master commit: 9a753a2e94985ed4cbbf214156ca035082e02723 …

【工作】计算机行业相关的十六类工作简介

本文简单介绍了计算机行业相关的工作类别&#xff0c;共16种&#xff0c;包括常见招聘要求与平均工资。平均工资信息来源&#xff1a;米国企业点评职场社区glassdoor&#xff08;https://www.glassdoor.com/index.htm&#xff09; &#xff08;一&#xff09;软件工程师 软件…

FreeRtos-13资源管理

一、临界资源是什么 要独占式地访问临界资源,有3种方法: 1.公平竞争:比如使用互斥量,谁先获得互斥量谁就访问临界资源,这部分内容前面讲过。 谁要跟我抢,我就灭掉谁: 2.中断要跟我抢?我屏蔽中断 3.其他任务要跟我抢?我禁止调度器,不运行任务切换 二、暂停调度器…

STM32高级控制定时器(STM32F103):计数模式

目录 概述 1 计数模式介绍 2 加计数模式 2.1 加计数模式介绍 2.2 计数序列案例 3 减计数模式 3.1 减计数模式介绍 3.2 应用实例 4 居中对齐模式&#xff08;向上/向下计数&#xff09; 4.1 功能介绍 4.2 应用实例 概述 本文主要介绍STM32F10X定时器计数功能的相关知…

【iOS】自定义cell及其复用机制

文章目录 cell的复用注册非注册两者的区别 自定义cell cell的复用 当用户滚动 UITableView 或 UICollectionView 时,只有少量可见的 cell 会被实际创建和显示。对于那些暂时不可见的 cell,系统会将它们缓存起来以备将来复用。这就是所谓的 cell 复用机制。 为什么需要cell的复…

深度神经网络——深度学习中的 RNN 和 LSTM 是什么?

引言 自然语言处理和人工智能聊天机器人领域许多最令人印象深刻的进步都是由 递归神经网络&#xff08;RNN&#xff09; 和长短期记忆&#xff08;LSTM&#xff09;网络。 RNN 和 LSTM 是特殊的神经网络架构&#xff0c;能够处理顺序数据&#xff0c;即按时间顺序排列的数据。…

插入排序-C语言版本

前言 插入排序是很重要的排序&#xff0c;著名的希尔排序就是从插入排序演变过来的&#xff0c;所以我们需要并且很多时候有些面试也是会面试插入排序的&#xff0c;所以需要好好捋清楚插入排序的逻辑是什么 插入排序gif 插入排序单趟实现 1&#xff0c;插入排序我们需要假设最…

【高端精品】最新手机版微信小程序(拼多多+京东)全自动操作项目

现代互联网经济的发展带来了新型的盈利方式&#xff0c;通过微信小程序的拼多多和京东进行商品自动巡视&#xff0c;为商家带来增的流量&#xff0c;同时为使用者带来利润。这种盈利方式无需复杂操作&#xff0c;用户仅需启动相应程序&#xff0c;商品信息便会被系统自动收集。…

《现代通信原理与技术》码间串扰和​​​​​​​无码间串扰的眼图对比实验报告

实 验&#xff1a;码间串扰和无码间串扰的眼图对比实验报告 摘 要&#xff1a; 在数字通信系统中&#xff0c;码间串扰&#xff08;Inter-Symbol Interference, ISI&#xff09;是影响信号质量和系统性能的重要因素之一。本实验通过MATLAB软件生成并对比了受码间串扰影响和未…

记录一个flink跑kafka connector遇到的问题

【报错】 D:\Java\jdk1.8.0_231\bin\java.exe "-javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2022.2.3\lib\idea_rt.jar56647:D:\Program Files\JetBrains\IntelliJ IDEA 2022.2.3\bin" -Dfile.encodingUTF-8 -classpath D:\Java\jdk1.8.0_231\jre\lib\cha…

ubuntu搭建java开发环境IDEA版

一.安装 OpenJDK 更新包列表&#xff1a; sudo apt update安装 OpenJDK&#xff1a; 你可以选择安装不同版本的 OpenJDK&#xff0c;例如 11 或 17&#xff0c;这个是安装 OpenJDK 11 的命令&#xff1a; sudo apt install openjdk-11-jdk验证安装&#xff1a; 安装完成后…

C++ 59 之 纯虚函数和抽象类

#include <iostream> #include <string> using namespace std;class Cal { // 类中有纯虚函数&#xff0c;这个类也叫做抽象类&#xff0c;无法实现实例化 public:int m_a;int m_b;// 虚函数// virtual int getRes(){// return 0;// }// 纯虚函数 作用和虚函数…

机器学习-课程整理及初步介绍

简介: 机器学习是人工智能的一个分支&#xff0c;它使计算机系统能够从经验中学习并改进其在特定任务上的表现&#xff0c;而无需进行明确的编程。机器学习涉及多种算法和统计模型&#xff0c;它们可以从数据中学习规律&#xff0c;并做出预测或决策。机器学习的应用非常广泛&…

【Unity】如何做一个很平滑的行人动画,且可以根据行人速度动态调整动画速度?

首先我们定一下不同速度对应的行人动作状态&#xff0c;设计为四种状态&#xff1a; 静止站立Stand&#xff1a;0~maxStandSpeed走路Walk&#xff1a;minWalkSpeed~maxWalkSpeed慢跑Jog&#xff1a;minJogSpeed~maxJogSpeed快跑Run&#xff1a;大于MinRunSpeed 我们可以使用A…

自学SAP是学习ECC版本还是S4版本?

很多人想学SAP&#xff0c;问我应该学ECC版本还是S4版本&#xff0c;我的建议如果你是自学的话&#xff0c;我个人建议使用ECC版本就行&#xff0c;因为这两个版本前台业务和后台配置的操作差异并不大&#xff0c;主要差异在于数据库的差异&#xff0c;前台业务操作和后台系统配…

用户态协议栈02-arp reply实现

在上一节DODK的UDP收发中发送udp包的时候&#xff0c;需要向物理机的arp表中添加一个静态的arp记录。这在生产环境中显然是不可以的。在内核的协议栈中&#xff0c;会将自己的ip和mac在局域网中进行广播&#xff0c;并且记录其他电脑的ip和mac。在需要发送数据包的时候&#xf…

6月15号作业

使用手动连接&#xff0c;将登录框中的取消按钮使用第二中连接方式&#xff0c;右击转到槽&#xff0c;在该槽函数中&#xff0c;调用关闭函数 将登录按钮使用qt4版本的连接到自定义的槽函数中&#xff0c;在槽函数中判断ui界面上输入的账号是否为"admin"&#xff0…