短链是什么原理?怎么实现呢?

简介: 内容营销中给用户推送营销消息最常见的方式就是发短信,比如三大运营商移动、联通、电信平时会发送一些诸如套餐办理、消费查询、话费充值这些短信,还有像银行、云服务厂商等等推送的各种包含查询服务的短信等等。

目录

一、为什么需要短链


内容营销中给用户推送营销消息最常见的方式就是发短信,比如三大运营商移动、联通、电信平时会发送一些诸如套餐办理、消费查询、话费充值这些短信,还有像银行、云服务厂商等等推送的各种包含查询服务的短信等等。


我们都知道单条短信发送的内容长度是有限制的,而如果要推送包含URL的消息,如果URL太长,不仅影响用户观感,而且会占用太多无用字数。


这时,我们需要将长的URL转换为短URL,也就是接下来我们要说的短链(短域名 + 短请求路径)。

二、短链跳转访问原理


原理还是很简单的,其实就是在后台保存有短链和长链的映射关系,然后进行重定向,让浏览器跳转到对应的长链接。

2.png

举个栗子,原始长链接为:https://www.baidu.com,通过某平台我生成了一个短链接:https://suo.nz/378IQe

3.png

我们可以看到当访问https://suo.nz/378IQe时,后端返回了302,同时多了一个Location响应头,值就是原始链接https://www.baidu.com.

这里有个小问题,关于重定向使用301还是302


4.png

301其实是比较符合HTTP协议语义的,但浏览器会缓存目标网址,下次访问时会直接跳过短链,跳转到目标网址,无法做一些统计,比如短链访问次数等。

5.png

  • 302:浏览器访问时,会先后访问短链代理服务和目标服务,对服务器的压力也就相应大些,但可以做一些统计。
  • 备注:很多短链生成平台其实都是走的302重定向。

三、短链生成实现方案


1、自增序列算法

常用的自增序列算法有雪花算法、Redis自增、MySQL主键自增等,生成唯一ID后,再转换为62进制字符串,转换后的62进制字符串可用作短链。

问题:为什么需要转换为62进制字符串呢?

因为自增ID会越来越长,经过62进制转换后可以变得更短。

现在说下关于自增序列生成短链的优缺点:

  • 优点:ID唯一,生成的短链不会重复和冲突。
  • 缺点:随着ID越来越大,短链长度也会随之变化,长度不固定。

再说下各种自增序列算法的优缺点:


算法 优点 缺点
雪花算法 高性能,不依赖任何中间件 存在系统时钟回拨问题,原始雪花算法长度为64位,生成的ID比较长。
Redis自增 高性能,高并发 既然是中间件,有维护成本,同时要考虑持久化、灾难恢复等。
MySQL主键自增 使用简单,易于扩展 高并发下有性能瓶颈 。

  • 301其实是比较符合HTTP协议语义的,但浏览器会缓存目标网址,下次访问时会直接跳过短链,跳转到目标网址,无法做一些统计,比如短链访问次数等。
  • 302:浏览器访问时,会先后访问短链代理服务和目标服务,对服务器的压力也就相应大些,但可以做一些统计。

2、Hash算法

简单来说就是对目标长链接进行hash,然后再对hash值进行62进制编码转换为短链接。Hash算法我们熟知的有MD5SHA等算法。

这两种算法为加密型hash算法,性能相对比较低,这里我们一般采用Google Guava中实现的Murmurhash算法,该算法为非加密型hash算法,相比MD5优点如下:


速度比MD5快。

哈希冲突的概率低,该算法支持32位和128位哈希值,MD5也是128位哈希值,基本不用担心哈希冲突。

离散度高,散列值比较均匀。

关于Murmurhash示例如下:

String url = "https://www.baidu.com/";
// 输出:e9ac4fbdc398e8c104d1b8415f42cbf8
System.out.println(Hashing.murmur3_128().hashString(url, StandardCharsets.UTF_8));
// 输出:06105412
System.out.println(Hashing.murmur3_32_fixed().hashString(url, StandardCharsets.UTF_8));
// 输出:bf447182
System.out.println(Hashing.murmur3_32_fixed().hashLong(Long.MAX_VALUE));
// 转成Long型
// 输出:307499014
System.out.println(Hashing.murmur3_32_fixed().hashString(url, StandardCharsets.UTF_8).padToLong());
// 输出:2188461247
System.out.println(Hashing.murmur3_32_fixed().hashLong(Long.MAX_VALUE).padToLong());

这里说下通过Hash算法生成短链的优缺点:


优点:去中心化,哈希后生成的短链长度基本固定。

缺点:有概率会发生哈希冲突,解决哈希冲突的方法主要有拉链法和重新哈希。由于短链是通过哈希值转62进制字符串生成,如果发生哈希冲突,得重新哈希生成。如果存库的话,每次生成短链至少会有一次查询和一次保存操作,有性能损耗。


四、代码示例


接下来的示例我们主要用Hash算法 + Base62 编码生成短链,流程图如下:

6.png

1、表结构及索引

# 短链信息表
create table `t_short_link`
(
    `id`             bigint primary key auto_increment comment '主键ID',
    `short_link`     varchar(32)  not null default '' comment '短链接',
    `long_link_hash` bigint       not null default 0 comment 'hash值',
    `long_link`      varchar(128) not null default '' comment '长链接',
    `status`         tinyint      not null default 1 comment '状态:1-可用,0-不可用',
    `expiry_time`    datetime     null comment '过期时间',
    `create_time`    datetime     not null default current_timestamp comment '创建时间'
) comment '短链信息表';
create index idx_sl_hash_long_link on t_short_link (long_link_hash, long_link);
create index idx_sl_short_link on t_short_link (short_link);

2、外部依赖

<!--Google Guava-->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version>
</dependency>

3、Base62Utils

public abstract class Base62Utils {
  private static final int SCALE = 62;
  private static final char[] BASE_62_ARRAY = {
    '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',
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
  };
  private static final String BASE_62_CHARACTERS = String.valueOf(BASE_62_ARRAY);
  /**
   * 将long类型编码成Base62字符串
   * @param num
   * @return
   */
  public static String encodeToBase62String(long num) {
    StringBuilder sb = new StringBuilder();
    while (num > 0) {
      sb.insert(0, BASE_62_ARRAY[(int) (num % SCALE)]);
      num /= SCALE;
    }
    return sb.toString();
  }
  /**
   * 将Base62字符串解码成long类型
   * @param base62Str
   * @return
   */
  public static long decodeToLong(String base62Str) {
    long num = 0, coefficient = 1;
    String reversedBase62Str = new StringBuilder(base62Str).reverse().toString();
    for (char base62Character : reversedBase62Str.toCharArray()) {
      num += BASE_62_CHARACTERS.indexOf(base62Character) * coefficient;
      coefficient *= SCALE;
    }
    return num;
  }
}

备注:BASE_62_ARRAY中的字符顺序可以随便打乱,不一定要按顺序排列,打乱安全性更高。

问题:能不能用Base64编码生成短链呢?


JDK8中Base64.getEncoder()获取到的编码器对应的编码字符会包含'+'、'/'等URL不允许包含的特殊字符,但Base64.getUrlEncoder()获取到的编码器对应的编码字符会把'+'、'/'分别替换成'-'、'_',所以其实也是可以的。如下:

7.png

4、DAO层

@Repository
public class ShortLinkManagerImpl implements ShortLinkManager {
  @Autowired
  private ShortLinkMapper shortLinkMapper;
  @Override
  public void saveShortLink(String shortLink, long longLinkHash, String longLink) {
    ShortLinkDO shortLinkDO = ShortLinkDO.builder()
      .shortLink(shortLink)
      .longLinkHash(longLinkHash)
      .longLink(longLink)
      .status(true)
      .build();
    shortLinkMapper.insert(shortLinkDO);
  }
  @Override
  public String getShortLink(long longLinkHash, String longLink) {
    Wrapper<ShortLinkDO> wrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
      .select(ShortLinkDO::getShortLink)
      .eq(ShortLinkDO::getLongLinkHash, longLinkHash)
      .eq(ShortLinkDO::getLongLink, longLink)
      .last(CommonConst.LIMIT_SQL);
    ShortLinkDO shortLinkDO = shortLinkMapper.selectOne(wrapper);
    return Optional.ofNullable(shortLinkDO).map(ShortLinkDO::getShortLink).orElse(null);
  }
  @Override
  public boolean isShortLinkRepeated(String shortLink) {
    Wrapper<ShortLinkDO> wrapper = Wrappers.lambdaQuery(ShortLinkDO.class).eq(ShortLinkDO::getShortLink, shortLink);
    return shortLinkMapper.selectCount(wrapper) > 0;
  }
}

5、业务层

@Service
public class ShortLinkServiceImpl implements ShortLinkService {
  @Autowired
  private ShortLinkManager shortLinkManager;
  @Override
  public String generateShortLink(String longLink) {
    long longLinkHash = Hashing.murmur3_32_fixed().hashString(longLink, StandardCharsets.UTF_8).padToLong();
    // 通过长链接Hash值和长链接检索
    String shortLink = shortLinkManager.getShortLink(longLinkHash, longLink);
    if (StringUtils.isNotBlank(shortLink)) {
      return shortLink;
    }
    // 如果Hash冲突则加随机盐重新Hash
    return regenerateOnHashConflict(longLink, longLinkHash);
  }
  private String regenerateOnHashConflict(String longLink, long longLinkHash) {
    // 自增序列作随机盐
    long uniqueIdHash = Hashing.murmur3_32_fixed().hashLong(SnowFlakeUtils.nextId()).padToLong();
    // 相减主要是为了让哈希值更小
    String shortLink = Base62Utils.encodeToBase62String(Math.abs(longLinkHash - uniqueIdHash));
    if (!shortLinkManager.isShortLinkRepeated(shortLink)) {
      shortLinkManager.saveShortLink(shortLink, longLinkHash, longLink);
      return shortLink;
    }
    return regenerateOnHashConflict(longLink, longLinkHash);
  }
}

五、测试用例


@SpringBootTest(classes = Application.class)
public class ApplicationTest {
  @Autowired
  private ShortLinkService shortLinkService;
  @Test
  public void generateShortLinkTest() {
    String shortLink = shortLinkService.generateShortLink("https://www.baidu.com/");
    System.err.println("生成的短链为:" + shortLink);
  }
}

控制台输出:

备注:生成的短链长度基本为6位字符串,还有记得短链代理服务选一个短域名

相关文章
|
1月前
|
存储 前端开发 JavaScript
【面试题】面试官问:如果有100个请求,你如何使用Promise控制并发?
【面试题】面试官问:如果有100个请求,你如何使用Promise控制并发?
|
8月前
|
存储 监控 小程序
《优化接口设计的思路》系列:第三篇—留下用户调用接口的痕迹
大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。 作为一名从业已达六年的老码农,我的工作主要是开发后端Java业务系统,包括各种管理后台和小程序等。在这些项目中,我设计过单/多租户体系系统,对接过许多开放平台,也搞过消息中心这类较为复杂的应用,但幸运的是,我至今还没有遇到过线上系统由于代码崩溃导致资损的情况。这其中的原因有三点:一是业务系统本身并不复杂;二是我一直遵循某大厂代码规约,在开发过程中尽可能按规约编写代码;三是经过多年的开发经验积累,我成为了一名熟练工,掌握了一些实用的技巧。
58 0
《优化接口设计的思路》系列:第三篇—留下用户调用接口的痕迹
|
5天前
|
缓存 NoSQL 前端开发
《优化接口设计的思路》系列:第六篇—接口防抖(防重复提交)的一些方式
本文探讨了后端开发中的接口防抖策略,作者是一名有六年经验的Java开发者,分享了如何防止重复提交导致的问题。防抖主要用于避免用户误操作或网络波动引起的多次请求,作者提出理想防抖机制应具备正确性、响应速度、易集成和用户反馈。文章详细分析了哪些接口需要防抖(如用户输入、按钮点击、滚动加载)以及如何识别重复接口,提出了使用共享缓存和分布式锁两种实现方式,并展示了基于Redis的Java代码示例。作者通过注解实现请求锁,并提供了测试截图证明防抖效果。然而,实现完全幂等性还需要业务层面的补充措施。
33 6
|
10月前
|
前端开发
【面试题】:前端怎么实现组件的封装和上传
前端如何实现组件的封装以及上传
155 0
|
1月前
|
移动开发 前端开发 JavaScript
面试官为啥总是喜欢问前端路由实现方式?
面试官为啥总是喜欢问前端路由实现方式?
|
1月前
|
移动开发 前端开发 JavaScript
【面试题】 面试官为啥总是喜欢问前端路由实现方式?
【面试题】 面试官为啥总是喜欢问前端路由实现方式?
|
1月前
|
Web App开发 JavaScript 安全
【面试题】 阿里面试官:请设计一个不能操作DOM和调接口的环境
【面试题】 阿里面试官:请设计一个不能操作DOM和调接口的环境
|
11月前
|
存储 NoSQL 数据库
深入浅出-Redis过期删除策略手术式源码刨析,小白也能看懂
>之前就说了要来西索Redis,现在来辣! >本文的部分基础内容参考自《小林Coding》,深入的地方根据源代码进行剖析。 >Redis源码地址:https://github.com/redis/redis.git ## 过期删除策略 基础的命令就不做过多解释了,如下 - `expire <key> <n>`:设置 key 在 n 秒后过期,比如 expire key 100 表示设置 key 在 100 秒后过期; - `pexpire <key> <n>`:设置 key 在 n 毫秒后过期,比如 pexpire key2 100000 表示设置 key2 在 100000 毫秒(10
56 5
深入浅出-Redis过期删除策略手术式源码刨析,小白也能看懂
|
Java 微服务
手撸一个动态Feign,实现一个“万能”接口调用
手撸一个动态Feign,实现一个“万能”接口调用
|
JavaScript 容器
一次性搞懂this
一次性搞懂this
78 0