前言

短链是我们业务中不陌生的一个场景,也是生活中常见的场景,比如我前几天续费车险涉及到的相关操作,保险公司给我发了一个短信,里面就是一个短链,点击之后会跳转到一个 APP 页面。正好最近的项目有短链场景,分享一下可能的实现方案。

源码

源码已分享到 Github 短链生成项目 下载即用

为什么需要短链

为什么要用短链来代替原始的长链呢?可能得原因是以下几点

  • 原始的长链会暴露我们的 uri 资源信息,对服务器来说会有一些不安全
  • 原始的长链长度太长了,会占用太多短信字数,服务商的短信字数长度是有限制的,并且太长的链接看起来也不友好
  • 原始的长链里面可能会包含一些敏感信息,如果随意明文暴露可能也会造成安全问题

实现概述

我们要实现的功能是将原本想要用户点击访问的长链接,变成一个短链接来代替,并且用户访问这个短链接要和访问这个长链接是一样的效果。那么我们可以得出一些结论

  • 短链要有过期时间,不能长期有效
  • 原始长链和短链要有映射关系,通过短链能够查询到对应的长链
  • 需要一个服务处理访问的短链资源,将请求重定向到原始长链
  • 短链要唯一,不能重复

表设计

我们需要一张表来存储短链和长链的映射关系,为了方便查询,最好是留一个 md5 加密过的长链信息字段

CREATE TABLE `ShortUrlRecord`  (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `long_url` varchar(2500) NOT NULL COMMENT '长链接地址',
  `domain` varchar(64) NOT NULL COMMENT '短链域名',
  `service_id` varchar(50) NOT NULL COMMENT '微服务id',
  `long_url_md5` varchar(36) NOT NULL COMMENT '长链接md5加密字符串',
  `short_url` varchar(20) NULL COMMENT '短链接 uri',
  `compensated` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否哈希冲突补偿',
  `expire_time` datetime NULL COMMENT '过期时间',
  `crt_time` datetime NOT NULL DEFAULT now() COMMENT '创建时间',
  `upt_time` datetime NOT NULL DEFAULT now() ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '删除标识',
  PRIMARY KEY (`id`)
);

生成短链代码

/**
 * 生成短链
 */
public String generateShortUrl(ShortUrlGenerateRequest request) {
    String md5 = EncryptUtil.md5(request.getLongUrl());
    //先查询长链是否已存在,存在直接返回数据库中的短链
    ShortUrlRecord exist = shortUrlRecordMapper.selectOne(Wrappers.<ShortUrlRecord>lambdaQuery().eq(ShortUrlRecord::getLongUrlMd5, md5));
    if (exist != null) {
        return exist.getShortUrl();
    }
    //生成短链
    Set<String> shortUrlList = randomShortUrl(request.getLongUrl());
    List<ShortUrlRecord> existsRecordList = shortUrlRecordMapper.selectList(Wrappers.<ShortUrlRecord>lambdaQuery().in(ShortUrlRecord::getShortUrl, shortUrlList));
    //移除掉数据库中已经存在的
    shortUrlList.removeIf(item -> existsRecordList.stream().anyMatch(x -> x.getShortUrl().equals(item)));
    //随机选一个
    return shortUrlList.stream().findAny().map(item -> {
        //保存短链记录
        ShortUrlRecord record = new ShortUrlRecord();
        record.setLongUrl(request.getLongUrl());
        record.setDomain(request.getDomain());
        record.setServiceId(request.getServiceId());
        record.setLongUrlMd5(md5);
        record.setShortUrl(item);
        record.setCompensated(0);
        record.setExpireTime(LocalDateTime.now().plus(request.getExpireDuration()));
        shortUrlRecordMapper.insert(record);
        return item;
    }).orElseGet(() -> {
        //走到这说明产生出来的短链全都已经存在了,使用兜底方案,创建一个必定唯一的短链
        return compensateGenerate(request);
    });
}

随机短链函数

/**
 * 生成短链后缀集合
 */
private Set<String> randomShortUrl(String longUrl) {
    int num = 10;//短链创建个数
    Set<String> result = new HashSet<>();
    for (int i = 0; i < num; i++) {
        String suffix = randomString(new String(CHAR_ARRAYS), 5);
        String url = longUrl + "_" + suffix; 
        //使用 MurmurHash3 哈希算法计算字符串的哈希值(从网上找的算法)
        HashCode hashCode = Hashing.murmur3_32_fixed().hashString(url, UTF_8);
        //Base62编码并添加到结果集,示例结果:2fLhXj
        result.add(Base62.encode(hashCode.asBytes()));
    }
    return result;
}

兜底防重复

/**
 * 补偿兜底生成短链,当 randomShortUrl 方法产生的短链全都存在于数据库中的时候调用该方法
 */
private String compensateGenerate(ShortUrlGenerateRequest request) {
    long seqId = nextId();
    String shortUrl = Base32.encode(parseBytes(seqId));
    ShortUrlRecord record = new ShortUrlRecord();
    record.setLongUrl(request.getLongUrl());
    record.setDomain(request.getDomain());
    record.setServiceId(request.getServiceId());
    record.setLongUrlMd5(EncryptUtil.md5(request.getLongUrl()));
    record.setShortUrl(shortUrl);
    record.setCompensated(1);
    record.setExpireTime(LocalDateTime.now().plus(request.getExpireDuration()));
    shortUrlRecordMapper.insert(record);
    return shortUrl;
}

这里的 nextId() 我们还是采用号段的方式产生的序列号,对号段不熟悉的可以参考 业务交易号的生成方式 —— 号段

/**
 * 下一个id
 */
public long nextId() {
    //生成唯一序列号 yyyyMMddHHmmSS+5
    String currentTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
    long suffix = sequenceService.next(SequenceTypes.SHORT_URL_SEQ_ID);
    //当前时间+唯一序列号截取后五位,不足补 0 (当一秒钟产生超过 10w 次短链冲突的时候会出现问题,概率很小不予考虑)
    String seqId = currentTime + StringUtil.subOrLefPad(String.valueOf(suffix), 5);
    return Long.parseLong(seqId);
}

短链重定向

我们需要单独部署一个代理例如 nginx,处理我们短链的域名,将请求代理到短链解析服务,然后在短链解析服务中,获取到请求的短链路径变量,然后查询对应的长链去重定向请求

@GetMapping("/{shortUrl}")
public String resolveShortUrl(@PathVariable("shortUrl") String shortUrl) {
    ShortUrlRecord record = shortUrlRecordMapper.selectOne(Wrappers.<ShortUrlRecord>lambdaQuery().eq(ShortUrlRecord::getShortUrl, shortUrl));
    //过期判断...
    //重定向
    return "redirect:"+record.getLongUrl();
}

短链过期

有个场景,短链是会过期的,假如产品的需求是当天 23:59:59 过期,然后第二天用户访问短链的时候,通过服务代理之后发现这个链接已经过期,给用户一个默认的兜底页面。

然后用户反馈过来,我们重新发送短信短链给用户,但是长链地址是不变的。MD5 之后的长链结果也是不变的,根据上面的业务逻辑,会返回已存在的过期短链。那么用户再次访问这个短链还是会过期,这就死循环了。

有两种方式来解决这个问题

  • 长链末尾拼接一个随机字符串。
"http:xxx.com/resource?key=value&rand"+UUID.random().toString

这样就能保证我们过期后,再次生成短链的时候会直接创建新的,而不会查询数据库,所以就能创建新的短链。

  • 短链续期

再次创建时,发现长链对应的短链已存在,抛出指定异常,对应业务端调用短链生成接口时捕获这个异常,在 catch 代码块中给短链续期。修改生成逻辑

/**
 * 生成短链
 */
public String generateShortUrl(ShortUrlGenerateRequest request) {
    String md5 = EncryptUtil.md5(request.getLongUrl());
    //先查询长链是否已存在,存在直接返回数据库中的短链
    ShortUrlRecord exist = shortUrlRecordMapper.selectOne(Wrappers.<ShortUrlRecord>lambdaQuery().eq(ShortUrlRecord::getLongUrlMd5, md5));
    if (exist != null) {
        if(exist.getExpireTime().isBefore(LocalDateTime.now())) {
            throw new ShortUrlExpireException("短链已过期");
        }
        return exist.getShortUrl();
    }
    //生成短链......

续期接口

/**
 * 短链续期
 * */
public void renewal(ShortUrlRenewalRequest request){
    String md5 = EncryptUtil.md5(request.getLongUrl());
    ShortUrlRecord exist = shortUrlRecordMapper.selectOne(Wrappers.<ShortUrlRecord>lambdaQuery().eq(ShortUrlRecord::getLongUrlMd5, md5));
    exist.setExpireTime(LocalDateTime.now().plus(request.getDuration()));
    shortUrlRecordMapper.updateById(exist);
}

结语

短链的生成方案实现是我入职上家公司的面试题,当时没有做过,当时的面试官也就是我的前领导也挺好的,只让我描述了一下大概的思路,我回答了映射关系、加密算法、重定向等几个关键点,算我过了

如果这篇文章对你有帮助,记得点赞加关注!你的支持就是我继续创作的动力!

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com