短链接的原理以及其实现

短链接的场景:短链接现在屡见不鲜,用于做促销、引流和数据统计等等,在一些限制字数的场景也很方便,看起来也是简洁,便于输入。

原理

短链接的原理

下面以短链接:wsc.fit/FZ(B站短链接)为例

  • 首先,在浏览器输入短链接,DNS先解析获得其IP地址;
  • 通过短链接,发送HttpGet请求,通过短链接,获取到原始的长连接;、
  • 请求通过重定向,跳转到长链接。302重定向,对服务器压力更大,但是优势在于,可以通过重定向进行流量统计。

短链接生成算法

知道了基本的原理,那短链接是通过什么生成的。短链接的组成分为:域名(区分Http和Https)+当前链接的短码。以bit.ly/2XyAbC为例,域名是bit.ly,短码是2XyAbC。一般来说,短码通过010,az,A~Z这六十二个数字和字母组成,四到八位短码,六位短码就有500多亿的组成方式,已经足够使用。常见的生成短码的方式有三种:

  • 通过自增ID。

在查询上一次的ID,在ID的基础上的进行MAX+1,然后然后将这十进制的ID转变为六十二为进制的转化成一个62进制的字符串。这样生成短码的优势在于,不会出现冲突,劣势在于,生成的短码是有序且安全性差,并且初始值不设置好,短码长度不一。可以结合其他数据进行MD5加密。

  • 通过随机数生成

将62位的数字和字母,随机排列组合作为短码,然后在数据库中查询是否已经存在。如果已经存在,则再次生成。优势就是逻辑简单,但是计算机生成的随机数是伪随机,如果需要生成大量短链接,可能出现重复需要再次生成。

  • 摘要算法(推荐)

摘要算法就是,通过一个函数,把任意的数据长度的数据转换成长度固定的数据。比如说,MD5,SHA1,SHA512等等。摘要算法,不是用于加密算法,因为不能通过摘要反推明文,而是用于防篡改。

通过加盐操作,很可以有效的防止MD5值被碰撞出来。

实现方案

数据库存储方案

sql 复制代码
 # 短链表
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);

缓存方案

如果所有的请求直接打在数据库上,无疑会影响到数据库的性能。但是如果直接将所有的短链信息直接存储在缓存中,也不太现实,当然数据量小也是可以的。

如果数据量大,可以考虑根据LRU(最近最少使用页面置换算法),在缓存中存放热点数据,并且设置合适的过期时间,将缓存进行热更新。

长链接转短链接的工具类:

案例一:
arduino 复制代码
import org.apache.commons.lang.StringUtils;
import com.google.common.hash.Hashing;

/**
 * 短网址工具类
 * 
 * @author Roc-xb
 *
 */
public class urlUtil {
	/**
	 * 10个数字+26个小写字母+26个大写字母
	 */
	private static String chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
	/**
	 * 62进制
	 */
	private static int scale = 62;
	/**
	 * 最小长度
	 */
	private static int minLength = 5;
	/**
	 * 种子
	 */
	private static int seed = 0;

	/**
	 * 长链接转短链接
	 * 
	 * @param longUrl 长链接
	 * @return 短链接
	 */
	public static String shortUrl(String longUrl) {
		// 用MurmurHash算法得出长链接的hash值
		int hash = Hashing.murmur3_32(seed).hashBytes(longUrl.getBytes()).asInt();
		return hex10To62(hash);
	}

	/**
	 * 10进制转62进制
	 * 
	 * @param hash
	 * @return 短链接
	 */
	public static String hex10To62(long hash) {
		// 解决hash值小于0时,会触发异常。
		hash = Math.abs(hash);
		StringBuilder sb = new StringBuilder();
		int remainder;
		while (hash > scale - 1) {
			// 对 scale 进行求余,然后将余数追加至 sb 中,由于是从末位开始追加的,因此最后需要反转字符串
			remainder = Long.valueOf(hash % scale).intValue();
			sb.append(chars.charAt(remainder));
			// 除以进制数,获取下一个末尾数
			hash /= scale;
		}
		sb.append(chars.charAt(Long.valueOf(hash).intValue()));
		String value = sb.reverse().toString();
		// value长度小于minLength时,在左侧填充字符'0'
		return StringUtils.leftPad(value, minLength, '0');

	}

}

测试类:

typescript 复制代码
@Test
void test(){
    // 长链接
	String longUrl = "https://www.zhangsan.com/community/test_service";
    // 短链接
    String shortUrl = urlUtil.shortUrl(longUrl);
    System.out.println(String.format("长链接:%s", longUrl));
    System.out.println(String.format("短链接:https://www.zhangsan.com/%s", shortUrl));
}

测试结果:

案例二

工具类:

arduino 复制代码
import org.apache.commons.codec.digest.DigestUtils;
import java.util.HashMap;

public class UrlUtil {
    public static HashMap<String, String> urls = new HashMap<>();
    static String[] chars = new String[]{
            "0", "1", "2", "3", "4", "5",
            "6", "7", "8", "9", "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"};

    public static String getChar(int index) {
        return chars[index];
    }

    public static String[] toShortAndSave(String str, String key) {
        // 拼接后的 url
        String realUrl = key + str;
        // md5 长度为 32 位
        String md5 = DigestUtils.md5Hex(realUrl);
        // md5 会被拆分为 4 部分 , 每部分均被转译成一种处理结果
        String[] shortUrl = new String[4];
        // 对 md5 的4个部分分别处理
        for (int beginIndex = 0; beginIndex < 4; beginIndex++) {
            // 预定义该部分的处理结果
            String out = "";
            // 字符串截取的两个坐标
            int endIndex = beginIndex + 1;
            // 裁剪 8 位
            String strPart = md5.substring(beginIndex * 8, endIndex * 8);
            // 与0x3FFFFFFF 进行位运算 获得30位长度的数字编码
            long index = Long.valueOf("3FFFFFFF", 16) & Long.valueOf(strPart, 16);
            // 将30位的数字编码拆分成6个部分 每次处理5位
            for (int i = 0; i < 6; i++) {
                // 与 0x0000003D 与运算,相当于与运算后5位,获得对应的字符下标
                int idx = (int) (Long.valueOf("0000003D", 16) & index);
                // 获得下标对应字符
                out += getChar(idx);
                // 数字右移5位 继续处理
                index = index >> 5;
            }
            // out 即为处理后的一种short方案,可以存储到数据库等存储介质中,这里为了方便展示直接存储到 hashMap 里
            urls.put(out, str);
            // 一共返回4种方案 分别赋值
            shortUrl[beginIndex] = out;
        }
        return shortUrl;
    }

    public static String[] head(String[] shortUrls) {
        String[] res = new String[4];
        int idx = 0;
        for (String e : shortUrls) {
            res[idx++] = "http://t.cn/" + e;
        }
        return res;
    }

}

测试类:

ini 复制代码
    @Test
    void test1(){
        String url = "https://blog/zhangsan";
        String key = "test";
        System.out.println("目标url:" + url);
        System.out.println("临时key:" + key);
        String[] res = UrlUtil.toShortAndSave(url, key);
        System.out.println("短网址处理结果:");
        for (String e : res) {
            System.out.println(e);
        }
        String[] head = head(res);
        System.out.println("短URL展示:");
        for (String e : head) {
            System.out.println(e);
        }
        System.out.println("网址映射:");
        for (String e : res) {
            System.out.println("短网址 : " + "https://blog/"+e + "   ->   实际网址 : " + urls.get(e));
        }
    }

结果:

相关推荐
ikun·39 分钟前
初识 Flask 框架
后端·python·flask
代码的余温1 小时前
ActiveMQ多消费者负载均衡优化指南
java·后端·负载均衡·activemq
薯条不要番茄酱3 小时前
【SpringBoot】从零开始全面解析Spring Ioc&DI (一)
spring boot·后端·spring
奋斗的袍子0073 小时前
Spring Boot 拦截器:解锁5大实用场景
java·spring boot·后端·拦截器·interceptor
Java&Develop7 小时前
springboot 集成kerberos 用户认证 获取域账号
java·spring boot·后端
会飞的架狗师7 小时前
【SpringBoot实战指南】使用 Spring Cache
spring boot·后端·spring·缓存
努力也学不会java9 小时前
【RabbitMQ】 RabbitMQ高级特性(二)
java·分布式·后端·中间件·rabbitmq
头发那是一根不剩了11 小时前
Spring Boot 注解 @ConditionalOnMissingBean是什么
java·spring boot·后端
进击的愤怒12 小时前
GIM发布新版本了 (附rust CLI制作brew bottle流程)
开发语言·后端·rust
x-cmd13 小时前
x-cmd install | cargo-selector:优雅管理 Rust 项目二进制与示例,开发体验升级
开发语言·后端·rust·cargo·示例