一文理解基于MD5的URL短地址处理

LeetCode 上有这么一个题:

设计一个 TinyURL 的加密 encode 和解密 decode 的方法。你的加密和解密算法如何设计和运作是没有限制的,你只需要保证一个URL可以被加密成一个TinyURL,并且这个TinyURL可以用解密方法恢复成原本的URL。

这个题一般来说最简单的做法就是直接给传入的 url 生成一个标识符然后存到哈希表里,然后用标识符再从哈希表取出来。官方的题解1正是如此:

public class Codec {
    Map<Integer, String> map = new HashMap<>();
    int i = 0;

    public String encode(String longUrl) {
        map.put(i, longUrl);
        return "http://tinyurl.com/" + i++;
    }

    public String decode(String shortUrl) {
        return map.get(Integer.parseInt(shortUrl.replace("http://tinyurl.com/", "")));
    }
}

这个算法存在什么缺点呢,官方题解也给出了答复:

  • 可以加密解密的 URL 数目受限于 int 所能表示的范围。

  • 如果超过 int 个 URL 需要被加密,那么超过范围的整数会覆盖之前存储的 URL,导致算法失效。

  • URL 的长度不一定比输入的 longURL 短。它只与加密的 URL 被加密的顺序有关。

  • 这个方法的问题是预测下一个会产生的加密 URL 非常容易,因为产生几个 URL 后很容易推测出生成的模式。

那么我们也用这种 生成标识符,并用标识符作为短URL 的思路来做这个题。

一、处理思路

官方给了 5 种处理方法,使用简单的计数、使用出现次序加密、使用hashcode、使用随机数、随机固定长度加密。在评论区里我们看到这样一个评论:

“相同 longUrl 应该返回相同的 shortUrl 吧,这些题解都没这样做”

我们的处理方法是 —— 利用 32 位的 MD5 对源地址进行处理。MD5 的编码总是固定的,这样一来既满足了题目的要求,又完成了评论区的想法。


处理思路展示

假设我们要处理的网址 URL = https://blog.csdn.net/qq_24251607

既然是加密,那就应该有 的样子,这里引入 key 的设计。我们让 key = Tamayo0914当然单纯的短URL实现也可以不做这个设计。然后把他们拼接起来:

在这里插入图片描述

之后我们对 realURL 进行 32位 的 MD5 编码处理:

在这里插入图片描述

然后把 md5 划分成 4个 长度均为8的字符串,将他们视为 16进制 数值,与 0x3FFFFFFF 做与运算得到长30位数值:

在这里插入图片描述

接下来只展示操作其中一段数值,实际上本方法最终会产生4个短URL

将与 0011 1101 进行6次与运算。之所以选择这个数值,是因为我们后面要将运算的结果和62个字符对应,而 0011 1101 的十进制值为 61 ,这样能够恰好保证计算的结果范围是 0~61 共62个值。

在这里插入图片描述

这样处理之后,原来的30位数值就拆成了6个数字:52,1,52,9,28,20。我们将其与 62位 字符数组相匹配,获得对应的字符即可。62位字符指的是0-9a-zA-Z,如图所示:

在这里插入图片描述

在这里插入图片描述

那么我们就得到了 经过short处理后的 URL —— Q1Q9sk,将其存放到 HashMap里就可以了。本质上还是官方题解的思路,只是在加密转换上有了更清晰的处理方式。

二、代码分析

  1. 首先我们要定义存储结构和字符集以及字符集的取值方法:
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];
}
  1. 编写主体函数(MD5 代码在后面提供)
public static String[] toShortAndSave(String str, String key) {
    // 拼接后的 url
    String realUrl = key + str;
    // md5 长度为 32 位
    String md5 = MD5Utils.code(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;
}
  1. 实际上写完1和2就已经实现了所需要的功能,为了展现出 网址 的样子,我们再编写一个方法进行拼接
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;
}
  1. 编写测试方法查看输出结果
public static void main(String[] args) {
    String url = "https://blog.csdn.net/qq_24251607";
    String key = "Tamayo0914";
    System.out.println("目标url:" + url);
    System.out.println("临时key:" + key);
    String[] res = 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("短网址 = " + e + "   ->   实际网址 = " + urls.get(e));
    }
}
  1. 输出结果

目标url:https://blog.csdn.net/qq_24251607
临时key:Tamayo0914
短网址处理结果:
Q1Q9sk
8AZZBl
RBlAVl
kIdUFl
短URL展示:
http://t.cn/Q1Q9sk
http://t.cn/8AZZBl
http://t.cn/RBlAVl
http://t.cn/kIdUFl
网址映射:
短网址 = Q1Q9sk -> 实际网址 = https://blog.csdn.net/qq_24251607
短网址 = 8AZZBl -> 实际网址 = https://blog.csdn.net/qq_24251607
短网址 = RBlAVl -> 实际网址 = https://blog.csdn.net/qq_24251607
短网址 = kIdUFl -> 实际网址 = https://blog.csdn.net/qq_24251607

三、完整代码

URL短处理代码:

import java.util.*;

public class study {

    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) {
        String realUrl = key + str;
        String md5 = MD5Utils.code(realUrl);
        String[] shortUrl = new String[4];
        for (int beginIndex = 0; beginIndex < 4; beginIndex++) {
            String out = "";
            int endIndex = beginIndex + 1;
            String strPart = md5.substring(beginIndex * 8, endIndex * 8);
            long index = Long.valueOf("3FFFFFFF", 16) & Long.valueOf(strPart, 16);
            for (int i = 0; i < 6; i++) {
                int idx = (int) (Long.valueOf("0000003D", 16) & index);
                out += getChar(idx);
                index = index >> 5;
            }
            urls.put(out, str);
            shortUrl[beginIndex] = out;
        }
        return shortUrl;
    }


    public static void main(String[] args) {
        String url = "https://blog.csdn.net/qq_24251607";
        String key = "Tamayo0914";
        System.out.println("目标url:" + url);
        System.out.println("临时key:" + key);
        String[] res = 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("短网址 = " + e + "   ->   实际网址 = " + urls.get(e));
        }
    }


    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;
    }

}

MD5Utils代码(直接引用的他人作品):

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class MD5Utils {

    /**
     * MD5加密类
     *
     * @param str 要加密的字符串
     * @return 加密后的字符串
     */
    public static String code(String str) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(str.getBytes());
            byte[] byteDigest = md.digest();
            int i;
            StringBuffer buf = new StringBuffer("");
            for (int offset = 0; offset < byteDigest.length; offset++) {
                i = byteDigest[offset];
                if (i < 0)
                    i += 256;
                if (i < 16)
                    buf.append("0");
                buf.append(Integer.toHexString(i));
            }
            //32位加密
            return buf.toString();
            // 16位的加密
            //return buf.toString().substring(8, 24);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        }
    }

    public static void main(String[] args) {
        System.out.println(code("1"));
    }
}

四、短地址冲突

1. 会出现短地址冲突吗

实际上将长的完整信息编译处理成短的信息,本身就是一个删除了大量原有信息的操作,就像 hash 函数一样,总是会有冲突的。本位示例的源地址长度为33,可以包含数字+符号+大小写字母,我们将他强行变更为6位的长度,意味着有27位的数值信息被我们抹去了。在大量的长地址转换成短地址的时候,自然会出现短地址冲突的问题。

2. 怎么避免短地址冲突

本文提到的处理方案同时存在3个方案来避免短地址的冲突问题。

  1. 我们不只是单纯的对原URL进行短地址处理,我们还对其附加了一个字段 key,在一定程度上使得生成的短地址会有所不同,如果每次操作的 key 不一样,那产生的短地址理论上也不一样。
  2. 使用 MD5 对数值进行编码。数学证明,MD5编码产生的结果一共有 2128,即便是有限的个数,但是也尽可能地让编码出的结果发散开来。
  3. 对同一个MD5值编译出 4 个短地址,我们在进行 HashMap 的 put 操作时,其实可以先尝试 get 我们产生的短地址,如果重复就换一个,毕竟我们产生了4个短地址,总会有1个短地址是没有被使用的。
  4. 在这种情况下如果还是发生了短地址冲突,除了考虑其他的短地址算法,还应当考虑是否需要设置短地址的过期策略,定时清空存储介质中短地址的映射关系。
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值