java實現一個短URL生成器


前言

短網址就是將一個長網址轉換成一個短網址,訪問短網址會重定向到原來的長網址,短網址會更利於傳播和推廣。微博就會將我們發的長網址轉換成短網址,

顯示效果

查看HTML源碼

微博將 https://www.cnblogs.com/strongmore/p/14520111.html 轉換成了http://t.cn/A6t8dUoR

實現

通過發號策略,每次來一個長網址,發一個號,這里我們使用SnowFlake(雪花算法)實現發號,然后轉成62進制。

雪花算法

/**
 * 分布式Id生成器-雪花算法
 */
public class SnowFlake {

  /**
   * 起始的時間戳
   */
  private final static long START_STMP = 1480166465631L;

  /**
   * 每一部分占用的位數
   */
  private final static long SEQUENCE_BIT = 12; //序列號占用的位數
  private final static long MACHINE_BIT = 5;   //機器標識占用的位數
  private final static long DATACENTER_BIT = 5;//數據中心占用的位數

  /**
   * 每一部分的最大值
   */
  private final static long MAX_DATACENTER_NUM = ~(-1L << DATACENTER_BIT);
  private final static long MAX_MACHINE_NUM = ~(-1L << MACHINE_BIT);
  private final static long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT);

  /**
   * 每一部分向左的位移
   */
  private final static long MACHINE_LEFT = SEQUENCE_BIT;
  private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
  private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;

  private long datacenterId;  //數據中心
  private long machineId;     //機器標識
  private long sequence = 0L; //序列號
  private long lastStmp = -1L;//上一次時間戳

  public SnowFlake(long datacenterId, long machineId) {
    if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
      throw new IllegalArgumentException(
          "datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
    }
    if (machineId > MAX_MACHINE_NUM || machineId < 0) {
      throw new IllegalArgumentException(
          "machineId can't be greater than MAX_MACHINE_NUM or less than 0");
    }
    this.datacenterId = datacenterId;
    this.machineId = machineId;
  }

  /**
   * 產生下一個ID
   */
  public synchronized long nextId() {
    long currStmp = getNewstmp();
    if (currStmp < lastStmp) {
      throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
    }

    if (currStmp == lastStmp) {
      //相同毫秒內,序列號自增
      sequence = (sequence + 1) & MAX_SEQUENCE;
      //同一毫秒的序列數已經達到最大4096
      if (sequence == 0L) {
        currStmp = getNextMill();
      }
    } else {
      //不同毫秒內,序列號置為0
      sequence = 0L;
    }

    lastStmp = currStmp;

    return (currStmp - START_STMP) << TIMESTMP_LEFT //時間戳部分
        | datacenterId << DATACENTER_LEFT       //數據中心部分
        | machineId << MACHINE_LEFT             //機器標識部分
        | sequence;                             //序列號部分
  }

  private long getNextMill() {
    long mill = getNewstmp();
    while (mill <= lastStmp) {
      mill = getNewstmp();
    }
    return mill;
  }

  private long getNewstmp() {
    return System.currentTimeMillis();
  }

  public static void main(String[] args) {
    SnowFlake snowFlake = new SnowFlake(2, 3);

    for (int i = 0; i < (1 << 12); i++) {
      System.out.println(snowFlake.nextId());
    }

  }
}

轉62進制

/**
 * 進制轉換工具,最大支持十進制和62進制的轉換
 * 1、將十進制的數字轉換為指定進制的字符串;
 * 2、將其它進制的數字(字符串形式)轉換為十進制的數字
 */
public class NumericConvertUtils {

  /**
   * 在進制表示中的字符集合,0-Z分別用於表示最大為62進制的符號表示
   */
  private static final char[] digits = {'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'};

  /**
   * 將十進制的數字轉換為指定進制的字符串
   *
   * @param number 十進制的數字
   * @param seed   指定的進制
   * @return 指定進制的字符串
   */
  public static String toOtherNumberSystem(long number, int seed) {
    if (number < 0) {
      number = ((long) 2 * 0x7fffffff) + number + 2;
    }
    char[] buf = new char[32];
    int charPos = 32;
    while ((number / seed) > 0) {
      buf[--charPos] = digits[(int) (number % seed)];
      number /= seed;
    }
    buf[--charPos] = digits[(int) (number % seed)];
    return new String(buf, charPos, (32 - charPos));
  }

  /**
   * 將其它進制的數字(字符串形式)轉換為十進制的數字
   *
   * @param number 其它進制的數字(字符串形式)
   * @param seed   指定的進制,也就是參數str的原始進制
   * @return 十進制的數字
   */
  public static long toDecimalNumber(String number, int seed) {
    char[] charBuf = number.toCharArray();
    if (seed == 10) {
      return Long.parseLong(number);
    }

    long result = 0, base = 1;

    for (int i = charBuf.length - 1; i >= 0; i--) {
      int index = 0;
      for (int j = 0, length = digits.length; j < length; j++) {
        //找到對應字符的下標,對應的下標才是具體的數值
        if (digits[j] == charBuf[i]) {
          index = j;
        }
      }
      result += index * base;
      base *= seed;
    }
    return result;
  }
}  

將一個long型數字轉成包含0-9,A-Z,a-z共62個字符的62進制字符串。將生成的字符串和短網址服務器域名連接即為最終的短網址,如https://t.cn/FWSc3ki8mY

public class Main {

  public static void main(String[] args) {
    SnowFlake snowFlake = new SnowFlake(0, 0);
    for (int i = 0; i < 10; i++) {
      System.out.println(NumericConvertUtils.toOtherNumberSystem(snowFlake.nextId(), 62));
    }
  }

}

輸出結果為

FWSc3ki8mY
FWSc3ki8mZ
FWSc3ki8n0
FWSc3ki8n1
FWSc3ki8n2
FWSc3ki8n3
FWSc3ki8n4
FWSc3ki8n5
FWSc3ki8n6
FWSc3ki8n7

整體流程

  1. 利用發號器創建短網址,保存長網址和短網址的映射關系到數據庫或Redis。
  2. 短網址服務器接收到請求,根據 FWSc3ki8mY 找到原來的長網址,返回302,告訴瀏覽器重定向到長網址。

待優化地方

現在的實現對於同一個長網址,每次創建的短網址也是不同的,如果每次創建前去數據庫查詢的話,效率太低,可以使用 LRU 緩存最近的N次映射結果,先查緩存,再查數據庫,兼顧了空間和性能。

參考

如何將一個長URL轉換為一個短URL?
短 URL 系統是怎么設計的?
新浪短網址服務


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM