短链接的实现

前往原站点查看

2023-05-13 21:18:17

    短链接,可以将一个较长的、携带参数的url简化成一个可以接受的长度。

    生活中,经常会在手机短信的广告中出现,因为短信服务本身对短信的长度有限制,如果使用一个非常长的链接,几百字符很快就能用完,关键信息的字符数被挤压,影响了服务方的广告价值同时也影响了消费者的观感,通过短链可以解决这个问题。

    短链也经常用在资源的分享链接上,比如常用的百度网盘资源分享,访问他人分享的资源链接,可以看到先经过了302重定向到一个其它的地址,这样分享资源的人就只要发/s/后面的一段字符串,接受分享的人也只要记住简单的百度网盘资源前缀pan.baidu.com/s/即可,简化了结构与流程,便于传递信息。



   当然百度的短链并非完全是这样的功能,如果访问了自己的资源,我们会发现,状态码是200,即表示直接将页面渲染了,这样巧妙的将不同逻辑的相似功能整合到了一个短链的访问上。



   今天通过百度下载了动漫资源《别当欧尼酱了!》,并且制作了一个简单的前端页面上传到服务器,从而可以在线查看该动漫。这样做的原因嘛,当然是因为太喜欢这部动漫了,所以打算珍藏起来反复回味!

   不过部署后,分享资源时的链接是这样的👉https://www.dreamcenter.top/extra/onimai/index.html ,好嘛,链接结构还是比较复杂的,长度也不是很友好,于是就想到了之前使用的别人的短链服务。但是一直使用别人的服务,倘若服务被关闭了,势必会造成一些难以挽救的局面,所以就想着自己设计实现一个短链服务。


基本原理

    短链的原理其实是非常简单的,关键就是 资源映射表 + 重定向

    资源映射表可以通过各种数据来记录,记录短链与实际资源地址的映射关系。

    重定向有两种方案,一种是使用301重定向,其特点是一次访问后,会在本地缓存,之后的访问会直接到达目标网址,不用再走一遍解析的流程;另一个种是使用302重定向,这种重定向是暂时的,下次访问短链还是会实际访问解析,这样的好处是可以对短链业务进行一些扩展与数据统计,不过会稍微增加一些服务压力。可以根据实际的需求来选择哪条路线。

设计实现

    我采用的路线是 mysql 记录映射关系,301重定向。

    短链接的一个比较重要的设计内容是 代表指定资源的特征字符串,常见的长度是6~8位,构成为英文数字组合,如 2H16sD 。当然也有一些比较特别的业务需求,该字符串会设计的更加复杂。该字符串我一开始想到的就是hash,倘若碰撞了,那么下位再次判定,但是参考了别人的设计方案,发现这种方案在短链很多的情况下,碰撞几率还是相当大的,数据越多,综合的生成效率越低。那么怎样才能稳定的效率生成呢?

    常用的方式是自增值62进制。为什么用62进制?仔细数0-9、a-z、A-Z,会发现正好有62位,倘若使用64进制的话,就势必有两个符号位加入生成,而很多符号在链接中都有特殊含义,不易选取。而62进制就不用担心那类问题了。将十进制转任意进制应该算是算法基本功了吧,简单贴一个实现。使用long入参是因为redis自增返回的是long类型,而且long也能存储更多的短链。

public class SpecificEncoding {

    private static final String STR_SER = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

    public static String base62(long val) {
        StringBuilder sb = new StringBuilder(); // 64
        while (val>0) {
            int tmp = (int)(val % 62); // [0,62) => i2 => 2 | => i0 => 1
            sb.append(STR_SER.charAt(tmp)); // 21
            val /= 62; // => val = 1 => 1 | 0
        }
        return sb.reverse().toString();
    }

}

    业务层面,自增变量一开始考虑到的是数据库映射表记录数量+1,但是后来仔细一想,如果删了中间的一条记录,那么下一个生成必然重复了。所以这个自增变量必须是绝对的不断向前滚动+1 的,我采用了一个redis来存储与操作该自增变量,实现如下。仔细阅读会发现在进行32进制编码前,传递的值是 Integer.MAX_VALUE + increment,为什么要加int的最大值呢?答案是显而易见的,为了让长度达到最少6位,当然达到6位的临界值没有细算,只是发现Integer.MAX_VALUE通过生成后是2开头的6位字符串,也就是说并没有浪费多少,还有很多很多的数据量可以存储。

  @Autowired
    private ShortLinkDao shortLinkDao;

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public RetResult<Integer> newShortLink(String raw) {
        ValueOperations ops = redisTemplate.opsForValue();
        Long increment = ops.increment(RedisConst.SHORT_LINK_ID);
        if (increment == null) return RetResult.fail("服务器出现异常...");
        String link = SpecificEncoding.base62(Integer.MAX_VALUE + increment);
        ShortLink shortLink = new ShortLink(link, raw);
        return RetResult.success(shortLinkDao.insert(shortLink));
    }

    最终的控制层面就更加简单了,除了基本的增删查服务调用外,额外的,就是通过路径变量比对映射表得到实际地址后进行重定向,一般无需更改服务,与其更改,不如再来一个新的来的方便(注意功能的访问权限控制)。如果经常要解析短链的,建议在解析服务与数据库之间加入一层redis缓存来处理热门解析。

package top.dreamcenter.dreamcenter.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import top.dreamcenter.dreamcenter.entity.ServerProperties;
import top.dreamcenter.dreamcenter.entity.ShortLink;
import top.dreamcenter.dreamcenter.ret.RetResult;
import top.dreamcenter.dreamcenter.service.ShortLinkService;

import java.util.List;

@Controller
public class ShortLinkController {

    @Autowired
    private ShortLinkService shortLinkService;

    @Autowired
    private ServerProperties serverProperties;

    @ResponseBody
    @PostMapping("/sl/new")
    public RetResult<Integer> newShortLink(String raw) {
        return shortLinkService.newShortLink(raw);
    }

    @ResponseBody
    @GetMapping("/sl/del")
    public RetResult<Integer> delShortLink(String link) {
        return shortLinkService.delShortLink(link);
    }

    @GetMapping("/s1/{link}")
    public String getRaw(@PathVariable String link) {
        String raw = shortLinkService.getRaw(link).getData();
        return "redirect:" + raw;
    }

    @ResponseBody
    @GetMapping("/sl/all")
    public RetResult<List<ShortLink>> getAllShortLinks() {
        return shortLinkService.getAllShortLinks();
    }
}

最终效果

    通过前端配合设计,并且把之前的资源生成短链后,得到的效果如下:

    可以发现之前的《别当欧尼酱了》短链形式是 https://www.dreamcenter.top/s1/2LKcb2 ,变得非常简洁。对于大家可以将中间路由设置成和百度网盘类似的/s/来区分业务请求,如果该短链接独占该ip的80、443端口,可以直接将短链字符串至于/根路径下,会变得更加简洁。

    为了方便短链复制,可以使用clipboard.js来处理内容一键复制功能。

    如果想要进一步增加需求,让数据更加安全,上面的这种连贯的方案肯定是不够安全的,可以增加一些随机位、位置交换、矩阵计算等方案让数据变得相对安全些。



下一篇:可以白嫖的AI绘画API