写点什么

短信中的短链设计

作者:勇敢的心
  • 2023-12-17
    北京
  • 本文字数:4724 字

    阅读完需:约 15 分钟

1、短链有啥好处,用长链不香吗

那么为啥要用短链表示,直接用长链不行吗,用短链的话有如下好处

1、链接变短,在对内容长度有限制的平台发文,可编辑的文字就变多了

  • 最典型的就是微博,限定了只能发 140 个字,如果一串长链直接怼上去,其他可编辑的内容就所剩无几了,用短链的话,链接长度大大减少,自然可编辑的文字多了不少。

  • 再比如一般短信发文有长度限度,如果用长链,一条短信很可能要拆分成两三条发,本来一条一毛的短信费变成了两三毛,何苦呢。另外用短链在内容排版上也更美观。

2、我们经常需要将链接转成二维码的形式分享给他人,如果是长链的话二维码密集难识别,短链就不存在这个问题了,如图示


3、链接太长在有些平台上无法自动识别为超链接

如图示,在钉钉上,就无法识别如下长链接,只能识别部分,用短地址无此问题


2、短链跳转的基本原理

从上文可知,短链好处多多,那么它是如何工作的呢。我们在浏览器抓下包看看


可以看到请求后,返回了状态码 302(重定向)与 location 值为长链的响应,然后浏览器会再请求这个长链以得到最终的响应,整个交互流程图如下


主要步骤就是访问短网址后重定向访问 B,那么问题来了,301 和 302 都是重定向,到底该用哪个,这里需要注意一下 301 和 302 的区别

  • 301,代表 永久重定向,也就是说第一次请求拿到长链接后,下次浏览器再去请求短链的话,不会向短网址服务器请求了,而是直接从浏览器的缓存里拿,这样在 server 层面就无法获取到短网址的点击数了,如果这个链接刚好是某个活动的链接,也就无法分析此活动的效果。所以我们一般不采用 301。

  • 302,代表 临时重定向,也就是说每次去请求短链都会去请求短网址服务器(除非响应中用 Cache-Control 或 Expired 暗示浏览器缓存),这样就便于 server 统计点击数,所以虽然用 302 会给 server 增加一点压力,但在数据异常重要的今天,这点代码是值得的,所以推荐使用 302!

3、短链生成的几种方法

1、哈希算法

怎样才能生成短链,仔细观察上例中的短链,显然它是由固定短链域名 + 长链映射成的一串字母组成,那么长链怎么才能映射成一串字母呢,哈希函数不就用来干这事的吗,于是我们有了以下设计思路


  • 那么这个哈希函数该怎么取呢,相信肯定有很多人说用 MD5,SHA 等算法,其实这样做有点杀鸡用牛刀了,而且既然是加密就意味着性能上会有损失,我们其实不关心反向解密的难度,反而更关心的是哈希的运算速度和冲突概率。

  • 能够满足这样的哈希算法有很多,这里推荐 Google 出品的 MurmurHash 算法,MurmurHash 是一种非加密型哈希函数,适用于一般的哈希检索操作。

  • 与其它流行的哈希函数相比,对于规律性较强的 key,MurmurHash 的随机分布特征表现更良好。

  • 非加密意味着着相比 MD5,SHA 这些函数它的性能肯定更高(实际上性能是 MD5 等加密算法的十倍以上),也正是由于它的这些优点,所以虽然它出现于 2008,但目前已经广泛应用到 Redis、MemCache、Cassandra、HBase、Lucene 等众多著名的软件中。

  • MurmurHash 提供了两种长度的哈希值,32 bit,128 bit,为了让网址尽可通地短,我们选择 32 bit 的哈希值,32 bit 能表示的最大值近 43 亿,对于中小型公司的业务而言绰绰有余。

  • 对上文提到的极客长链做 MurmurHash 计算,得到的哈希值为 3002604296,于是我们现在得到的短链为 固定短链域名+哈希值 = http://gk.link/a/3002604296

对于创业型的公司来说,并发量可能并不高,用数据库做一个短链和长链的映射即可,如果是高并发的话需要用 redis 缓存等

下面是一种数据库实现方案

1、先建立一张短链和长链的映射表

DROP TABLE IF EXISTS `short_url`;CREATE TABLE `short_url` (  `id` int(11) NOT NULL AUTO_INCREMENT,  `origin` varchar(1000) NOT NULL,  `current` varchar(100) NOT NULL,  `status` int(11) DEFAULT '1',  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,  `invalid_time` datetime DEFAULT NULL,  PRIMARY KEY (`id`),  UNIQUE KEY `idx_current` (`current`) USING BTREE) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
复制代码

2、工具类


package com.app.mvc.shortUrl;
/** * Created by jimin on 16/4/7. */
import lombok.extern.slf4j.Slf4j;
import java.security.MessageDigest;import java.util.Random;
@Slf4jpublic class ShortUrlUtil {
public static String generate(String url) { String key = "1q2w3e4r";
String[] chars = new String[] { "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", "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" };
String hex = md5ByHex(key + url);
String[] resUrl = new String[4]; for (int i = 0; i < 4; i++) { //将产生4组6位字符串 // 把加密字符按照 8 位一组 16 进制与 0x3FFFFFFF 进行位与运算 String sTempSubString = hex.substring(i * 8, i * 8 + 8);
// 这里需要使用 long 型来转换,因为 Integer.parseInt() 只能处理 31 位 , 首位为符号位 , 如果不用long ,则会越界 long lHexLong = 0x3FFFFFFF & Long.parseLong(sTempSubString, 16); String outChars = ""; for (int j = 0; j < 6; j++) { // 把得到的值与 0x0000003D 进行位与运算,取得字符数组 chars 索引 long index = 0x0000003D & lHexLong; // 把取得的字符相加 outChars += chars[(int) index]; // 每次循环按位右移 5 位 lHexLong = lHexLong >> 5; } // 把字符串存入对应索引的输出数组 resUrl[i] = outChars; } return resUrl[new Random().nextInt(4)]; }
/** * MD5加密(32位大写) * * @param src * @return */ public static String md5ByHex(String src) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] b = src.getBytes(); md.reset(); md.update(b); byte[] hash = md.digest(); String hs = ""; for (int i = 0; i < hash.length; i++) { String tmp = Integer.toHexString(hash[i] & 0xFF); if (tmp.length() == 1) hs = hs + "0" + tmp; else { hs = hs + tmp; } } return hs.toUpperCase(); } catch (Exception e) { return ""; } }
}
复制代码

3、实现

package com.app.mvc.shortUrl;
import com.app.mvc.acl.enums.Status;import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/** * Created by jimin on 16/4/7. */@Servicepublic class ShortUrlService {
@Resource private ShortUrlDao shortUrlDao;
public String generate(String url) { String newUrl = ShortUrlUtil.generate(url); ShortUrl shortUrl = shortUrlDao.findByNewUrl(url); if (shortUrl != null) { return shortUrl.getCurrent(); } shortUrl = ShortUrl.builder().origin(url).current(newUrl).status(Status.AVAILABLE.getCode()).build(); shortUrlDao.save(shortUrl); return newUrl; }
public String getOriginUrl(String url) { ShortUrl shortUrl = shortUrlDao.findByNewUrl(url); if (shortUrl == null) { throw new RuntimeException("未查到该短链接"); } if (shortUrl.getStatus() != Status.AVAILABLE.getCode()) { throw new RuntimeException("该短链接已失效"); } if (shortUrl.getInvalidTime() != null && shortUrl.getInvalidTime().getTime() < System.currentTimeMillis()) { throw new RuntimeException("该短链接已过期"); } return shortUrl.getOrigin(); }
public String getOriginUrlWithoutException(String url) { try { return getOriginUrl(url); } catch (Throwable t) { return "/index.jsp"; } }}

/** * Created by jimin on 16/4/7. */@DBRepositorypublic interface ShortUrlDao {
void save(ShortUrl shortUrl);
ShortUrl findByNewUrl(@Param("url") String url);}

<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.app.mvc.shortUrl.ShortUrlDao">
<sql id="shortUrlColumns"> id, origin, current, status, create_time as createTime, invalid_time as invalidTime </sql>
<insert id="save" parameterType="com.app.mvc.shortUrl.ShortUrl"> INSERT INTO short_url (origin, current, status, create_time, invalid_time) VALUES (#{origin}, #{current}, #{status}, now(), #{invalidTime}) </insert>
<select id="findByNewUrl" parameterType="string" resultType="com.app.mvc.shortUrl.ShortUrl"> SELECT <include refid="shortUrlColumns" /> FROM short_url WHERE current = #{url} limit 1 </select></mapper>
复制代码

controller 部分

package com.app.mvc.shortUrl;
import com.app.mvc.beans.JsonData;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;import javax.servlet.http.HttpServletResponse;
/** * Created by jimin on 16/4/7. */@Slf4j@Controllerpublic class ShortUrlController {
@Resource private ShortUrlService shortUrlService;
@ResponseBody @RequestMapping("/gen") public JsonData generate(@RequestParam("url") String url) { return JsonData.success(shortUrlService.generate(url)); }
@RequestMapping("/t/{url}") public void toOrigin(@PathVariable("url") String url, HttpServletResponse response) { try { response.sendRedirect(shortUrlService.getOriginUrlWithoutException(url)); } catch (Throwable t) { log.error("根据短链接跳转出现异常, url=" + url, t); } }}
复制代码


用户头像

勇敢的心

关注

终身学习、研究java架构、ai大模型 2019-12-06 加入

商业合作: wytwhdwdd

评论

发布
暂无评论
短信中的短链设计_勇敢的心_InfoQ写作社区