实现一个简单的短链服务

需求

功能需求: 内部使用的短链,平时做推广时使用,能多短就多短,最好能自定义。
非功能需求: 安全要求不能被猜到,否则容易被枚举。

总结一下就是:

  • 尽量短
  • 可自定义
  • 量不大
  • 不可以连续

分析

  • 通过代理
    通过短链接访问代理服务,代理服务转发到长链接的源服务器。好处就是能完全隐藏长链接,缺点就是成本高。

  • 通过客户端重定向
    通过短链接访问重定向服务,返回一个重定向,客户端再重定向到长链接。好处就成本更低。

301, 永久重定向,因为是永久的所以浏览器会做缓存,第一次获取重定向链接后就缓存起来下次不直接用本地缓存,好处是减轻服务负担,但是无法知道用户的蒂点击行为,比如要做统计啥的就没办法。
302, 临时重定向,临时的所以浏览器不做缓存,每次都请求服务获取重定向链接,优缺点和301相反。

所以一般都用客户端重定向的方式。都是域名访问,短链接只是需要一个更短的域名后缀,所以还需要一个将短链接域名后缀映射成长链接域名后缀的映射服务,映射服务入参是短链接,出参是长链接,而且短链接必须是唯一的即不应该重复。

将长链接转成短连接的方法又有两种,一种是哈希法,一种是发序号法

  • 哈希
    将长连接字符串进行哈希生成更短的字符串,常见的哈希MD5、SHA等,还有再网上看好多人做锻炼都在使用的MurmurHash算法。使用哈希算法的好处是长链接和短链接存在关联,这个关联的好处是可以通过长链接很快的定位到短链接,很容易判断长链接是否已经映射到短链接,存在判断时就比较方便,而且很分散不连续,安全性好,但是坏处就是不能自定义短链接,而且需要解决哈希冲突问题。

  • 发序号
    顾名思义就是给长链接分发一个短链接,好处是能自定义锻炼,坏处是生成短链接的性能可能不太好,而且如果判断一个长链接是否已经被映射需要额外的操作。

因为需要自定义短链接,所以最后选择使用发序号的方式实现。实际上内部使用的,短链接的生成需求没那么多。最后再确保发的号不重复不连续即可。

发序号又分成好多种,因为要求不重复的,所以就考虑唯一ID生成。比如通过数据库自增、uuid、通过redis自增、通过雪花算法等。

考虑到项目使用量不大,使用数据库自增的方式生成即可。

生成的ID是连续的整形,但是想到URL可以由大小写和数字构成,也就是可以使用字符有62个,再将ID换成62进制进一步缩短链接。

至于连续的问题再做进制转换时将字母打乱就不连续啦。

实现

首先新建一个表用来保存短链接和长链接的对应关系,

create table `short_link`(
                        `id` bigint primary key auto_increment,
                        `short_url` varchar(10) comment '短链接',
                        `long_url` varchar(2048) comment '长链接',
                        `create_time` timestamp default current_timestamp comment '创建时间'
)default charset=utf8mb4 collate=utf8mb4_0900_ai_ci comment '短链接长链接映射信息表'; 

这里偷懒一下,自增ID直接使用这个表的ID做为短链。

定义一个进制转换的方法

import java.util.HashMap;
import java.util.Map;

/**
 * @author edui 2022/9/19
 */
public class RadixUtil {

    //顺序打乱的编码,这样不连续,不容易被枚举
    final  static char[] ch = new char[]{
            'q','a','z','w','s','x','e','d','c','v','f','r','t','g','b','y','h','n','m','j','u','i','k','l','o','p',
            'Q','A','Z','X','S','W','E','D','C','V','F','R','T','G','B','N','H','Y','U','J','M','K','I','O','L','P',
            '1','2','3','4','5','6','7','8','9','0'
    };
    final static Map<Character, Integer> map = new HashMap<>(ch.length);
    static {
        for (int i = 0; i < ch.length; i++) {
            map.put(ch[i], i);
        }
    }

    /**
     * lD转成URL
     * @param id 目标ID
     */
    public static String id2Url(Long id){
        StringBuilder re = new StringBuilder("");
        while (id > 0){
            re.append(ch[(int) (id % 62)]);
            id = id / 62;
        }
        return re.toString();
    }

    /**
     * lURL转成ID
     */
    public static Long url2Id(String url){
        long re = 0L;
        for (int i = 0; i < url.length(); i++) {
            re += (long)Math.pow(62L,i) * map.get(url.charAt(i));
        }
        return re;
    }
}

这样查询一条短链接对应的长链接直接将短链通过上面进制转换成工具转成ID,之后直接通过ID查询数据即可。

插入自定义短链时,通过上面进制转换成工具转成ID,之后通过ID插入数据即可,如果不是自定义就插入一条数据生成ID再由ID转成短链接更新到数据库。

扩展

上面的实现是在项目不大,短链接服务只是自己项目用的情况下实现的。还有很多的优化空间。

功能上
还可以有查重服务,查询一个长链接是否已经存在,这种可以通过增加哈希索引等方式进行改进优化。
还可以增加短链接过期功能,增加一个过期时间过期了就不能访问

性能上
增加查询缓存,将热点的数据缓存在redis增加查询的性能
优化ID生成机制,可以通过成熟的分布式唯一ID生成工具生成ID。

不过项目量小,顶多一万个短链接,流量也没多少数据库完全没压力,纯路人 觉得没必要[呲牙笑]


版权声明:本文为I_am_hardy原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。