PHP短网址

本文由 简悦 SimpRead) 转码, 原文地址 www.php.cn)

1. 背景介绍

这些网址往往都是非常短,但是当我们打开之后,如果你仔细观察,中间会有跳转,最终浏览器地址栏显示的网址并不是你短信里面看到的网址,这就是短网址!

短网址 - 图1

2. 原理和应用

短网址一般是采用一个非常短域名下,路径参数一般只有 3-6 个字符组成,非常简洁!

使用短网址的前提是先生成短网址,主要是采用某种算法让一段短的字符对应一个长的字符,比如说从常用的 0-9、a-z、A-Z 共 62 个字符中选择 6 个字符,那意味着有 62 的 6 次方种组合,大概有 568 亿不重复的短网址可用!

服务器通过路径参数查询到真实的长网址,然后使用 301/302 跳转到真实的网址即可!

关于跳转,301 是永久重定向,302 是临时重定向。短地址一经生成就不会变化,所以用 301 是符合 http 语义的,浏览器会记录跳转地址,同时对服务器压力也会有一定减少。但是如果使用了 301,我们就无法统计到短地址被点击的次数了,如果对数据统计有要求的话,使用 302 跳转可能比较好一些!

短网址的主要好处是方便传输记忆,特别是在短信里面使用的时候,短信对内容字数有限制,还有比如说微博分享也使用了短网址!

3. 市面现有案例

(1) 百度的短链接 (dwz.cn/),百度不仅仅提供了网页入口,也提供了接口和开发文档,简单易用!

(2) 新浪的短链接 (sina.lt/),目前仅提供网页入口,未发现接口服务!

(3) 淘宝的短链接 (tb.am/),目前仅提供网页入口,未发现接口服务!

4. 常用算法

网上比较流行的算法有进制算法、摘要(Hash)算法、随机数算法,下面简单介绍一下:

进制算法

这个算法网上也有叫作自增序列算法,特点就是永不重复,设置 id 自增,一个 10 进制 id 对应一个 62 进制的数值,1 对 1,也就不会出现重复的情况,这个利用的就是低进制转化为高进制时,字符数会减少的特性。

计算机中常见的进制有 2 进制,8 进制,10 进制,16 进制,进制越大,能够表示的数越大,占用的字数也越少。下面举个例:

10 进制的 1000,在 8 进制里面是 1750,在 16 进制里面就是 3E8,那在 62 进制里面呢?有人说,计算机里面没有 62 进制。。。虽然没有,但是我们可以造一个,进制的转换算法是固定的,最常见的就是 “除基取余法”!

我们假设 62 进制的字符序列为 0-9 a-z A-Z,顺序可以打乱,但是应该固定下来,是一个从 0 角标开始的到 61 的数组,我们暂且称之为字母表!

====> 1000/62 = 16,余 8

====> 16/62 = 0,余 16

余数得到的数字是 16、8,然后找到字母表里面角标为 16 和 8 的字符拼起来,就是 g8,非常短,只有 2 位数!假如说我们想至少产生 6 位字符,那么我们可以从一个比较大的数字开始,具体可以看下图:

1 位 62 0 - 61
2 位 3844 62 - 3843
3 位 约 23 万 3844 - 238327
4 位 约 1400 万 238328 - 14776335
5 位 约 9.1 亿 14776336 - 916132831
6 位 约 568 亿 916132832 - 56800235583

Hash 算法

第一种方式:
简单的对长链接进行加盐 md5,会生成一个 32 位的字符串,随机从里面取 6 个字符,或者简单粗暴取最后 6 位,但是 md5 只包含 0-9 A-F a-f, 比字母表的里面字符还少,冲突几率更大!

第二种方式:
1. 将长网址 md5 生成 32 位签名串, 分为 4 段, 每段 8 个字节

  1. 对这四段循环处理, 取 8 个字节, 将他看成 16 进制串与 0x3fffffff(30 位 1) 与操作, 即超过 30 位的忽略处理

  2. 这 30 位分成 6 段, 每 5 位的数字作为字母表的索引取得特定字符, 依次进行获得 6 位字符串

  3. 总的 md5 串可以获得 4 个 6 位串, 取里面的任意一个就可作为这个长 url 的短 url 地址

生成的方式更加复杂,重复的几率低,但是依然会出现冲突!

随机数算法

这个更简单,直接对这个 62 个字符数组做随机选择,选择其中 6 个字符当作短链接码,简单易用,但是难免会出现重复冲突!

算法对比

第一种算法只要解决自增 id 问题就可以避免冲突,自增 id 可以采用数据库自增主键,每次生成短码只需一次数据库操作(insert 操作,获取主键 id,然后算出短码即可)

第二种和第三种算法其实都差不多,都是依赖于程序随机,容易出现冲突,这就需要每次在插入数据库的时候判重,效率低一些!

安全

短链接虽然方便了传输和记忆,但是由于链接组成的字符个数少,更容易被爆破、猜测攻击,攻击者可以轻松遍历所有字符组成的链接!

所以不建议使用短链接发送具有私密性的网址,比如说重置密码链接,对一些权限、敏感信息的链接要做好二次鉴权!

推荐教程:Laravel 实战开发短链生成器视频教程

如何设计与实现短 URL 服务?

本文由 简悦 SimpRead) 转码, 原文地址 mp.weixin.qq.com)

https://juejin.im/post/6844903873950269454

短 URL 的好处

  1. 短信和许多平台 (微博) 有字数限制 ,太长的链接加进去都没有办法写正文了.
  2. 好看。 比起一大堆不知所以的参数, 短链接更加简洁友好.
  3. 方便做一些统计。 你点了链接会有人记录然后分析的.
  4. 安全。 不暴露访问参数.

服务设计

如果你在往长短 URL 真实的对应关系上想, 那么就走远了.

最理想的情况是: 我们用一种算法, 对每一个长 URL, 唯一的转换成短 URL. 还能保持反向转换的能力.

但是这是不可能的, 如果有这样的算法, 世界上的所有压缩算法都可以原地去世了.

正确的思路是建立一个发号器, 每次有一个新的长 URL 进来, 我们就增加一, 并且将新的数值返回. 第一个来的 URL 返回 “www.x.cn/0“, 第二个返回”www.x.cn/1“.

对应关系如何存储?

这个对应数据肯定是要落盘的, 不能每次系统重启就重新排号, 所以可以采用 mysql 等数据库来存储. 而且如果数据量小且 qps 低, 直接使用数据库的自增主键就可以实现.

如何保证长短链接一一对应?

按照上面的发号器策略, 是不能保证长短链接的一一对应的, 你连续用同一个 URL 请求两次, 结果值都是不一样的.

为了实现长短链接一一对应, 我们需要付出很大的空间代价, 尤其是为了快速响应, 我们可以需要在内存中做一层缓存, 这样子太浪费了.

但是可以实现一些变种的, 来实现部分的一一对应, 比如将最近 / 最热门的对应关系存储在 K-V 数据库中, 这样子可以节省空间的同时, 加快响应速度.

短 URL 的存储

我们返回的短 URL 一般是将数字转换成 32 进制, 这样子可以更加有效的缩短 URL 长度, 那么 32 进制的数字对计算机来说只是字符串, 怎么存储呢? 直接存储字符串对等值查找好找, 对范围查找等太不友好了.

其实可以直接存储 10 进制的数字, 这样不仅占用空间少, 对查找的支持较好, 同时还可以更加方便的转换到更多 / 更少的进制来进一步缩短 URL.

高并发

如果直接存储在 MySQL 中, 当并发请求增大, 对数据库的压力太大, 可能会造成瓶颈, 这时候是可以有一些优化的.

缓存

上面保证长短链接一一对应中也提到过缓存, 这里我们是为了加快程序处理速度. 可以将热门的长链接 (需要对长链接进来的次数进行计数), 最近的长链接(可以使用 redis 保存最近一个小时的) 等等进行一个缓存, 保存在内存中或者类似 redis 的内存数据库中, 如果请求的长 URL 命中了缓存, 那么直接获取对应的短 URL 进行返回, 不需要再进行生成操作.

批量发号

每一次发号都需要访问一次 MySQL 来获取当前的最大号码, 并且在获取之后更新最大号码, 这个压力是比较大的.

我们可以每次从数据库获取 10000 个号码, 然后在内存中进行发放, 当剩余的号码不足 1000 时, 重新向 MySQL 请求下 10000 个号码. 在上一批号码发放完了之后, 批量进行写入.

这样可以将对数据库持续的操作移到代码中进行, 并且异步进行获取和写入操作, 保证服务的持续高并发.

分布式

上面设计的系统是有单点的, 那就是发号器是个单点, 容易挂掉.

可以采用分布式服务, 分布式的话, 如果每一个发号器进行发号之后都需要同步给其他发号器, 那未必也太麻烦了.

换一种思路, 可以有两个发号器, 一个发单号, 一个发双号, 发号之后不再是递增 1, 而是递增 2.

类比可得, 我们可以用 1000 个服务, 分别发放 0-999 尾号的数字, 每次发号之后递增 1000. 这样做很简单, 服务互相之间基本都不用通信, 做好自己的事情就好了.

实现

由于我懒得写 JDBC 代码, 更懒得弄 Mybatis, 所以代码中使用到 MySQL 的地方都使用了 Redis.

  1. package util;
  2. import redis.clients.jedis.Jedis;
  3. /**
  4. * Created by pfliu on 2019/06/23.
  5. */
  6. public class ShortURLUtil {
  7. private static final String SHORT_URL_KEY = "SHORT_URL_KEY";
  8. private static final String LOCALHOST = "http://localhost:4444/";
  9. private static final String SHORT_LONG_PREFIX = "short_long_prefix_";
  10. private static final String CACHE_KEY_PREFIX = "cache_key_prefix_";
  11. private static final int CACHE_SECONDS = 1 * 60 * 60;
  12. private final String redisConfig;
  13. private final Jedis jedis;
  14. public ShortURLUtil(String redisConfig) {
  15. this.redisConfig = redisConfig;
  16. this.jedis = new Jedis(this.redisConfig);
  17. }
  18. public String getShortURL(String longURL, Decimal decimal) {
  19. // 查询缓存
  20. String cache = jedis.get(CACHE_KEY_PREFIX + longURL);
  21. if (cache != null) {
  22. return LOCALHOST + toOtherBaseString(Long.valueOf(cache), decimal.x);
  23. }
  24. // 自增
  25. long num = jedis.incr(SHORT_URL_KEY);
  26. // 在数据库中保存短-长URL的映射关系,可以保存在MySQL中
  27. jedis.set(SHORT_LONG_PREFIX + num, longURL);
  28. // 写入缓存
  29. jedis.setex(CACHE_KEY_PREFIX + longURL, CACHE_SECONDS, String.valueOf(num));
  30. return LOCALHOST + toOtherBaseString(num, decimal.x);
  31. }
  32. /**
  33. * 在进制表示中的字符集合
  34. */
  35. final static char[] digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8',
  36. '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
  37. 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',
  38. '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'};
  39. /**
  40. * 由10进制的数字转换到其他进制
  41. */
  42. private String toOtherBaseString(long n, int base) {
  43. long num = 0;
  44. if (n < 0) {
  45. num = ((long) 2 * 0x7fffffff) + n + 2;
  46. } else {
  47. num = n;
  48. }
  49. char[] buf = new char[32];
  50. int charPos = 32;
  51. while ((num / base) > 0) {
  52. buf[--charPos] = digits[(int) (num % base)];
  53. num /= base;
  54. }
  55. buf[--charPos] = digits[(int) (num % base)];
  56. return new String(buf, charPos, (32 - charPos));
  57. }
  58. enum Decimal {
  59. D32(32),
  60. D64(64);
  61. int x;
  62. Decimal(int x) {
  63. this.x = x;
  64. }
  65. }
  66. public static void main(String[] args) {
  67. for (int i = 0; i < 100; i++) {
  68. System.out.println(new ShortURLUtil("localhost").getShortURL("www.baidudu.com", Decimal.D32));
  69. System.out.println(new ShortURLUtil("localhost").getShortURL("www.baidu.com", Decimal.D64));
  70. }
  71. }
  72. }