Node.js躬行記(6)——自製短鏈系統

  短鏈顧名思義是一種很短的地址,應用廣泛,例如頁面中有一張二維碼圖片,包含的是一個原始地址(如下所示),如果二維碼中的鏈接需要修改,那麼就得發程式碼替換掉。

  但如果二維碼圖包含的是一條短鏈,那麼只要修改短鏈中的映射關係,就能不發程式碼了。當然了,前提是有一套短鏈系統維護著他們之間的關係,下圖是列表和新增的介面。

  

  

  前端介面的程式碼省略了,直接看短鏈用Node.js實現的後端程式碼。

一、MySQL

  在 web_short_chain 表中,主鍵 id 是一個自增的整數,short 欄位存儲著短鏈中的 key,也就是 //t.cn/4fYKXF 中的 4fYKXF 之類的數據,並且是全表唯一的,目前還未對其建索引。

CREATE TABLE `web_short_chain` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `short` varchar(10) COLLATE utf8mb4_bin NOT NULL COMMENT '短鏈地址中的key',
  `url` varchar(200) COLLATE utf8mb4_bin NOT NULL COMMENT '原始地址',
  `ctime` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `mtime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '狀態',
  PRIMARY KEY (`id`),
  UNIQUE KEY `short_UNIQUE` (`short`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='短鏈存儲'

二、計算 short 的值

  需要兩步才能將原始地址映射成短鏈地址,第一步是使用 MurmurHash(么么哈希)演算法,由Austin Appleby在2008年發明,可將原始地址轉換成一個哈希值,演算法如下(最新版本 MurmurHash3)。

function MurmurHashV3(key, seed) {
  if (typeof key === "string") key = createBuffer(key);
  var remainder, bytes, h1, h1b, c1, c1b, c2, c2b, k1, i;
  remainder = key.length & 3; // key.length % 4
  bytes = key.length - remainder;
  h1 = seed;
  c1 = 0xcc9e2d51;
  c2 = 0x1b873593;
  i = 0;
  while (i < bytes) {
    k1 =
      (key[i] & 0xff) |
      ((key[++i] & 0xff) << 8) |
      ((key[++i] & 0xff) << 16) |
      ((key[++i] & 0xff) << 24);
    ++i;
    k1 = ((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff;
    k1 = (k1 << 15) | (k1 >>> 17);
    k1 = ((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff;
    h1 ^= k1;
    h1 = (h1 << 13) | (h1 >>> 19);
    h1b = ((h1 & 0xffff) * 5 + ((((h1 >>> 16) * 5) & 0xffff) << 16)) & 0xffffffff;
    h1 = (h1b & 0xffff) + 0x6b64 + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16);
  }
  k1 = 0;
  switch (remainder) {
    case 3:
      k1 ^= (key[i + 2] & 0xff) << 16;
    case 2:
      k1 ^= (key[i + 1] & 0xff) << 8;
    case 1:
      k1 ^= key[i] & 0xff;
      k1 = ((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff;
      k1 = (k1 << 15) | (k1 >>> 17);
      k1 = ((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff;
      h1 ^= k1;
  }
  h1 ^= key.length;
  h1 ^= h1 >>> 16;
  h1 = ((h1 & 0xffff) * 0x85ebca6b + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff;
  h1 ^= h1 >>> 13;
  h1 = ((h1 & 0xffff) * 0xc2b2ae35 + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16)) & 0xffffffff;
  h1 ^= h1 >>> 16;
  return h1 >>> 0;
}

  在得到一個整型的哈希值後,就得轉換成字元,像上面短鏈中的字元是 6 個,也就是將10進位轉換成62進位,如下所示。

function string10to62(n) {
  if (n === 0) {
    return "0";
  }
  var digits = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
  var result = "";
  while (n > 0) {
    result = digits[n % digits.length] + result;
    n = parseInt(n / digits.length, 10);
  }
  return result;
}

三、快取

  在將映射關係存入資料庫時,可將其直接存入 redis 快取中,採用哈希的數據結構,也就是將計算出的 short 作為 key,原始地址作為 value。

  假設每條關係所佔空間是50位元組,那麼2000W條記錄大概佔用 1G左右,為了節省空間,快取的超時時間會設為 7 天。

  每次在訪問短鏈時,首先從快取中讀取,若有,就直接跳轉;若無,則查詢資料庫,再將映射關係存入快取中。

//讀取redis
let url = await services.common.redisShortChainGet(short);
ctx.status = 302;     //臨時跳轉
if(url) {
  ctx.redirect(getCompleteUrl(url, querystring));
  return;
}
//快取中不存在,則讀取資料庫
const data = await services.common.getOneShortChain({ short });
if(!data) {
  ctx.body = "短鏈不存在";
  return;
}
//將資料庫中讀取的短鏈快取起來
await services.common.redisShortChainSet(short, data.url);
ctx.redirect(getCompleteUrl(data.url, querystring));

  網上的一些文章在判斷短鏈是否存在時,會採用布隆過濾器

  它實際上是一個很長的二進位向量和一系列隨機映射函數。布隆過濾器可以用於檢索一個元素是否在一個集合中。它的優點是空間效率和查詢時間都遠遠超過一般的演算法,長度是 10 億的布隆過濾器,也只需要 125MB左右的記憶體空間。

  布隆過濾器的缺點是有一定的誤識別率和刪除困難,例如下圖中的 A 和 E 是存在於布隆過濾器中的,它們的映射位置都設成了 1,而 B 並不存在,但它的映射指向了兩個是 1 的位置,從而就造成了誤識別。