redis分布式缓存(十二)一一短链接解决方案短链接解决

短链接解决方案

「这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战

电商短链接业务场景

场景1:阿里健康大药房短信

image.png
浏览器打开链接 c.tb.cn/d5.dJPad

跳转到
detail.tmall.com/item.htm?id…

场景2:京东短信

image.png

浏览器打开链接 3.cn/1iGyAa-x 就转变为如下:
pro.m.jd.com/mall/active…

场景3:美团优选短信

image.png

还有淘宝商品链接分享等等场景。。。

什么是短链接

就是把普通网址,转换成比较短的网址。

我们先回到生成好的短链上c.tb.cn/d5.dJPad,虽然这个链接看起来有点奇怪,但他终究还是一个链接,从URL的特征我们可以分出:

  • c.tb.cn是域名
  • d5.dJPad是参数

为什么需要短链接

  • 节省网址长度,便于社交化传播
  • 控制成本,提升体验:比如,文案总字数超过70个字,那就算两条短信计费,超过140个字就算三条短信计费
  • 匿名访问:隐藏访问来源,在最终页面通过HTTP的Refer字段获取不到跳转前的来源。
  • 密码访问:对某些链接想设置密码访问,但是不能实现直接对原来的链接进行密码控制,就可以通过对原始链接转换后的短链接进行访问控制来实现目的(此时原始链接是没有访问控制的,但是链接的生成没有规律可言,便可以认为没有人能访问到原始链接),同密码访问类似,也可以对某个短链接限定访问次数、访问时段、访问黑白名单来控制访问。
  • 访问统计:通过统计对短链接的访问数便可以得到原始链接的访问量,在移动互联网下,可针对不同的客户端(Android、IOS、PC、MAC)生成不同的短链接来统计不同客户端的访问量。
  • 简化二维码:二维码图案的复杂度和原始信息的大小成正相关,过长的链接会导致生成的二维码图片过于复杂,降低二维码识别的成功率。
  • 降低权重传递:在搜索引擎领域,存在链接权重传递的现实。即如果链接A有很多人访问,通过搜索引擎搜索链接A相关内容时,会把链接A放在搜索结果的前面,若是在一个没有很多访问量的链接B中引入链接A,就让搜索引擎在扫描链接关系时赋予链接B稍高的权重,导致搜索链接B相关内容时会把链接B放在搜索结果的前面。使用短链接就可以打破原有的依赖链(从链接A>链接B变成链接A>短链接>链接B)。

短链接原理

  • 将长链接通过一定的手段生成一个短链接
  • 访问短链接时实际访问的是短链接服务器,然后根据短链接的参数找回对应的长链接
  • 服务器返回302状态码,将响应头中的Location设置为:长链接
  • 浏览器重定向:长链接
  • 长链接服务器返回响应

客户端访问短链接c.tb.cn/d5.dJPad

c.tb.cn服务器

根据d5.dJPad找到原始链接

长链接返回给浏览器

浏览器重定向

短链接服务核心部分包括:

  • 长链接转短链接的算法
  • 对应关系持久化,方便长链接和短链接互相查询

短链接服务提供商

目前有很多提供短链接生成的服务,需要使用短链接的时候,直接调用相关服务进行转化即可。

设计短链接生成服务

首先,也是最重要的,申请一个尽量短的域名。
后续的过程本质就是一个发号的过程,每当对一个长链接生成短链接时,就生成一个唯一标识,附加于短域名后,然后记录下对应关系并返回生成的短链接。

后续有对原始链接的请求时,查询对应表,若是查询到就返回301302并返回查询到的原始链接即可。
可见整个系统中关键的两点是,唯一标识生成对应表存储

关于存储对应表的服务,由于只是存储简单的K-V关系,所以可以采用类似Redis的K-V数据库。

短链接算法

算法特性

  1. 高可用,服务可以部署到多台服务器上不会出现单点故障问题。
  2. 长链接和短链接1:1映射。一方面可以解决存储资源,另外也可以方便点击统计等功能实现。
  3. 少碰撞。生成短链接性能不会随着已生成短链接数量增加而线性增加。

业内常用3种算法

1.发号器 & 进制转化

实现:

发号器(ID自增)+62进制编码

算法思路:

  1. 首先请求发号器生成不重复十进制数字
  2. 十进制数字转化成62进制

为什么要用62进制转换?64进制转换倒是听得多了

  • 62进制转换是因为62进制转换后只含数字+小写+大写字母。而64进制转换会含有/,+这样的符号(不符合正常URL的字符)
  • 10进制转62进制可以缩短字符,如果我们要6位字符的话,62^6=56800235584,已经有560亿个组合了。

算法特点:

  • 该算法能够支持6位字符组合62^6=500多亿条长链接。
  • 发号器可以选择数据库自增ID,snake算法,redis等诸多实现。

举例:

https://juejin.cn/user/474627279698541/posts长链接发给它的号id是唯一的10000,然后将10000进行62进制编码得到的结果是:2Bi

那我的短链URL就可以弄成https://3.cn/2Bi,其中3.cn是域名,2Bi是经过62进制转换后的参数。我们保存好2Bi和长链接的关系,下次用短链接请求就能找到长链接。

进制转换-在线工具

总结:

使用redis自增命令获取id,转成62进制,在redis用hash保存短链接和长链接的映射关系

https://juejin.cn/user/474627279698541/posts

获取自增id值为10000

10000的62进制为2Bi

对应关系持久化2Bi映射https://juejin.cn/user/474627279698541/posts

2.随机算法&hash算法(MD5)

算法思路:

随机算法原理是根据长链接随机生成字符串,然后将字符串映射到[0-9][a-z][A-Z]集合上。

算法一

  1. 将长网址md5生成32位签名串,分为4段, 每段8个字节;
  2. 对这四段循环处理, 取8个字节, 将它转换成16进制串与0x3FFFFFFF(30位1)操作, 即超过30位的忽略处理;
  3. 这30位分成6段, 每5位的数字作为字母表的索引取得特定字符, 依次进行获得6位字符串;
  4. 总的md5串可以获得4个6位串; 取里面的任意一个就可作为这个长url的短url地址;

这种算法,虽然会生成4个,但是仍然存在重复几率.

算法二

62个可选元素再随机选两个组成长度为64的字符数组。

  1. 随机生成36位 0/1的随机数
  2. 36位分成6份,通过randomValue>>6以此获取低6位元素。
  3. (randomValue>>6) & 0x3F 获取对应的字符

算法二冲突的概率为1/64^6=1/2^36。

3.预生成 & 访问分配

算法思路:

[0-9][a-z][A-Z]集合 这62位取6位组合,可产生500多亿个组合数量.

数字字符组合(短链接)做一定的映射,就可以产生唯一的字符串

如第62个组合就是aaaaa9,第63个组合就是aaaaba,再利用洗牌算法,把原字符串打乱后保存,那么对应位置的组合字符串就会是无序的组合。

把长网址存入数据库,取返回的id,找出对应的字符串,例如返回ID为1,那么对应上面的字符串组合就是bbb,

同理ID为2时,字符串组合为bba,依次类推,直至到达64种组合后才会出现重复的可能,

算法特点:

  • 如果用上面的62个字符,任意取6个字符组合成字符串的话,你的数据存量达到500多亿后才会出现重复的可能。

重定向301还是302?

  1. 301:重定向是永久的重定向,搜索引擎在抓取新的内容时,同时也将旧的网站进行替换,替换为重定向后的地址。 搜索引擎只会将新链接加到索引里。会使用浏览器缓存,下次访问老链接时会直接访问新链接,因为不经过服务器解析短链接,不能做短链接访问统计。
  2. 302:重定向只是暂时的重定向,搜索引擎会认为重定向后的地址为临时的,并会保存新的地址.搜索引擎会同时索引老链接和新链接. 浏览器会认为临时跳转,新老链接都会被访问到。

因此,为了统计到短链接访问次数,一般使用302状态码进行跳转

短信的链接直接跳转到APP

一切为了运营!如何从推广短信链接唤起 App

实战:SpringBoot+Redis高并发《短链接转换器》

《短链接转换器》的原理:

  1. 长链接转换为短链接

实现原理:长链接转换为短链接加密串key,然后存储于redis的hash结构中。

  1. 重定向到原始的url

实现原理:通过加密串key到redis找出原始url,然后重定向出去

本例子基于随机算法一实现短链接转换器

短链接转换器

import org.apache.commons.codec.digest.DigestUtils;
/**
 * 将长网址 md5 生成 32 位签名串,分为 4 段, 每段 8 个字节
 * 对这四段循环处理, 取 8 个字节, 将他看成 16 进制串与 0x3fffffff(30位1) 与操作, 即超过 30 位的忽略处理
 * 这 30 位分成 6 段, 每 5 位的数字作为字母表的索引取得特定字符, 依次进行获得 6 位字符串
 * 总的 md5 串可以获得 4 个 6 位串,取里面的任意一个就可作为这个长 url 的短 url 地址
 */
public class ShortUrlGenerator {
    //26+26+10=62
    public static  final  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"};


    /**
     * 一个长链接URL转换为4个短KEY
     */
    public static String[] shortUrl(String url) {
        String key = "";
        //对地址进行md5
        String sMD5EncryptResult = DigestUtils.md5Hex(key + url);
        System.out.println(sMD5EncryptResult);
        String hex = sMD5EncryptResult;
        String[] resUrl = new String[4];
        for (int i = 0; i < 4; i++) {
            //取出8位字符串,md5 32位,被切割为4组,每组8个字符
            String sTempSubString = hex.substring(i * 8, i * 8 + 8);

            //先转换为16进账,然后用0x3FFFFFFF进行位与运算,目的是格式化截取前30位
            long lHexLong = 0x3FFFFFFF & Long.parseLong(sTempSubString, 16);

            String outChars = "";
            for (int j = 0; j < 6; j++) {
                //0x0000003D代表什么意思?他的10进制是61,61代表chars数组长度62的0到61的坐标。
                //0x0000003D & lHexLong进行位与运算,就是格式化为6位,即61内的数字
                //保证了index绝对是61以内的值
                long index = 0x0000003D & lHexLong;

                outChars += chars[(int) index];
                //每次循环按位移5位,因为30位的二进制,分6次循环,即每次右移5位
                lHexLong = lHexLong >> 5;
            }

            // 把字符串存入对应索引的输出数组
            resUrl[i] = outChars;
        }
        return resUrl;
    }

    /**
     * 566ab90fc1bb2c6544ac4353f0d0a364
     * niIvAj
     * B7jQza
     * ryqyia
     * AzE7ny
     * @param args
     */
    public static void main(String[] args) {
        // 长连接
        String longUrl = "https://juejin.cn/user/474627279698541/posts";
        // 转换成的短链接后6位码,返回4个短链接
        String[] shortCodeArray = shortUrl(longUrl);

        for (int i = 0; i < shortCodeArray.length; i++) {
            // 任意一个都可以作为短链接码
            System.out.println(shortCodeArray[i]);
        }
    }
}
复制代码

控制器

@RestController
@Slf4j
public class ShortUrlController {

    @Autowired
    private HttpServletResponse response;

    @Autowired
    private RedisTemplate redisTemplate;
    private  final static  String SHORT_URL_KEY="short:url";

    /**
     * 长链接转换为短链接
     * 实现原理:长链接转换为短加密串key,然后存储在redis的hash结构中。
     */
    @GetMapping(value = "/encode")
    public String encode(String url) {
        //一个长链接url转换为4个短加密串key
        String [] keys= ShortUrlGenerator.shortUrl(url);
        //任意取出其中一个,我们就拿第一个
        String key=keys[0];
        //用hash存储,key=加密串,value=原始url
        this.redisTemplate.opsForHash().put(SHORT_URL_KEY,key,url);
        log.info("长链接={},转换={}",url,key);
        return "http://127.0.0.1:9090/"+key;
    }

    /**
     * 重定向到原始的URL
     * 实现原理:通过短加密串KEY到redis找出原始URL,然后重定向出去
     */
    @GetMapping(value = "/{key}")
    public void decode(@PathVariable String key) {
        //到redis中把原始url找出来
        String url=(String) this.redisTemplate.opsForHash().get(SHORT_URL_KEY,key);
        try {
            //重定向到原始的url
            response.sendRedirect(url);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
复制代码

拾遗-MD5算法特点

  1. 压缩性:任意长度的数据,算出的MD5值长度都是固定的。
  2. 容易计算:从原数据计算出MD5值很容易。
  3. 抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。
  4. 强抗碰撞:已知原数据和其MD5值,想找到一个具有相同MD5值的数据(即伪造数据)是非常困难的。

redis分布式缓存系列