使用Go語言開發短地址服務

SnDragon發表於2022-12-14

思維導圖

簡介

什麼是短地址(短鏈)服務?

可以將原本一大串很長的地址縮短到一個很短的地址,使用者訪問這個短地址可以重定向到原本的長地址

比較著名的短鏈域名有騰訊的url.cn,微博的t.cn等,也有很多公司提供了免費/收費的短連結開放API,感興趣的可以自行網上搜尋。

使用場景

  • 提升使用者體驗, 例如淘寶商品詳情url不可避免地會跟著很多引數,相較繁長的字串, 使用簡短的連結分享對使用者來說觀感更好
  • 體現品牌專業度,就像QQ號@qq.com會顯得不夠專業一樣,使用短地址而非長地址進行營銷推廣在一定程度上能提升使用者對品牌的好感
  • 避免url被截斷,例如當呼叫第三方平臺介面傳送訊息時,如果訊息內容中包含url,裡面的#、?等特殊字元可能在客戶端被截斷,導致收到訊息的使用者打不開該連結

弊端

使用短鏈服務可能會有哪些弊端呢?

  1. 成本,不使用第三方服務的話,需要自行搭建一套服務來實現長鏈轉短鏈,以及支援短鏈跳轉,使用第三方服務則可能需要收費
  2. 安全,如果短鏈服務是對外開放的,可能會被黑產或不法分子利用
  3. 時效,例如短鏈設定的過期時間較長,可能原本的長鏈已經失效了,這時短鏈就會打不開
  4. 速度, 因為有個短鏈重定向到長鏈的過程

拋開以上弊端不講,本著學習的心態,本文將介紹如何使用Go語言開發一個簡單的短鏈服務,簡述背後的原理, 末尾會有專案程式碼。

實現原理

流程

實現短鏈服務並不複雜,我們先看看使用短鏈的流程,如圖所示:

流程圖

  1. 呼叫短鏈服務轉換介面,輸入長鏈
  2. 短鏈服務生成對應的短鏈,並儲存對映關係
  3. 客戶端(一般是瀏覽器)訪問短鏈
  4. 短鏈服務查詢對映關係
  5. 如果能找到對應的長鏈且在有效期內,則重定向到長鏈,否則返回404
  6. 客戶端訪問長鏈

可見,這裡的短鏈服務核心是如何將長鏈對映成短鏈並儲存,以及根據短鏈查詢對應的長鏈並重定向給客戶端,即短鏈生成與查詢

短鏈生成

需要保證的點:

  1. 全域性唯一
  2. 儘可能短
  3. 利於查詢

常用的短連結演算法主要有兩種:

演算法 簡述 優點 缺點
雜湊 原地址透過雜湊函式生成雜湊值 本地計算,無需依賴第三方元件 可能存在雜湊碰撞,當雜湊衝突時需要rehash或其他處理,且雜湊值一般不會很短
分散式ID 藉助一定演算法或外部元件生成全域性唯一ID 可保證全域性唯一,其中自增ID的形式較短 通常需要依賴外部元件如MySQL,Redis,Zookeeper等

短鏈對映儲存

選擇較多,基本要求:

  • 快速查詢
  • 可以設定過期時間
  • 持久化

開發思路

由於Redis具有豐富的資料結構,支援設定過期時間,持久化,高效能等特點,這裡我們使用Redis來生成自增id,並儲存對映關係,但在設計會考慮可擴充套件為其他儲存的能力。

額外說明的一點: 這裡自增id會轉成62進位制的字串(A-Za-z0-0),作為短鏈id(下文都稱sid), 可以讓整體字串更短些

不使用base64是因為base64包含+、/兩個特殊符號, 對URL不友好

專案用到的庫:

實現介面

為方便後續擴充套件,我們可以定義一個抽象的Storage介面,並提供一個Redis的實現,後續也可替換成其他實現

// Storage 短鏈服務抽象介面
type Storage interface {
 Shorten(url string, expSecond int64) (string, error)     // 將長鏈轉成短鏈,並設定過期時間
 ShortLinkInfo(sid string) (*entity.UrlDetailInfo, error) // 根據短鏈id獲取詳情
 UnShorten(sid string) (string, error)                    // 根據短鏈id轉成原始長鏈
}
// RedisStorage Redis實現短鏈服務
type RedisStorage struct {
    redisCli *redis.Client
}

func (r *RedisStorage) Shorten(url string, expSecond int64) (string, error) {
  // 1. 獲取自增id
  id, err := r.redisCli.Incr(RedisKeyUrlGlobalId).Result()
  if err != nil {
  return "", errors.Wrap(err, "[Shorten] incr global id err")
  }
  // 2. 轉成base62(base64包含`+`、`/`字元,對URL不友好)
  sid := base62.EncodeInt64(Offset + id)
  // 3. 設定短url對應的原始url
  if err := r.redisCli.Set(fmt.Sprintf(RedisKeyShortUrl, sid), url,
  time.Second*time.Duration(expSecond)).Err(); err != nil {
  return "", errors.Wrap(err, "[Shorten] set RedisKeyShortUrl err")
  }
  // 4. 設定詳情
  urlDetail := &entity.UrlDetailInfo{
  OriginUrl: url,
  CreatedAt: time.Now().Unix(),
  ExpiredAt: time.Now().Unix() + expSecond,
  }
  if err := r.redisCli.Set(fmt.Sprintf(RedisKeyUrlDetail, sid),
  encoding.JsonMarshalString(urlDetail), 0).Err(); err != nil {
  return "", errors.Wrap(err, "[Shorten] set RedisKeyUrlDetail err")
  }
  return config.AppConfig.BaseUrl + sid, nil
}

func (r *RedisStorage) ShortLinkInfo(sid string) (*entity.UrlDetailInfo, error) {
  // 1. 獲取詳情
  data, err := r.redisCli.Get(fmt.Sprintf(RedisKeyUrlDetail, sid)).Result()
  if err != nil {
  return nil, errors.Wrap(err, "[ShortLinkInfo] get url detail err")
  }
  // 2. 反序列化
  info := &entity.UrlDetailInfo{}
  if err := encoding.JsonUnMarshalString(data, info); err != nil {
  return nil, errors.Wrapf(err, "[ShortLinkInfo] JsonUnMarshalString err: %v", data)
  }
  // 3. 獲取計數器
  countRet, err := r.redisCli.Get(fmt.Sprintf(RedisKeyUrlCounter, sid)).Result()
  if err == redis.Nil {
  countRet = "0"
  } else if err != nil {
  return nil, errors.Wrapf(err, "[ShortLinkInfo] get RedisKeyUrlCounter err, sid: %v", sid)
  }
  info.Counter = cast.ToInt64(countRet)
  return info, nil
}

func (r *RedisStorage) UnShorten(sid string) (string, error) {
  // 1. 獲取對應長鏈
  val, err := r.redisCli.Get(fmt.Sprintf(RedisKeyShortUrl, sid)).Result()
  if err == redis.Nil {
  return "", &serrors.StatusError{
  Code: http.StatusNotFound,
  Err:  fmt.Errorf("unknown url: %v", sid),
  }
  } else if err != nil {
  return "", errors.Wrap(err, "get RedisKeyShortUrl err")
  }
  // 2. 訪問計數器+1
  if err := r.redisCli.Incr(fmt.Sprintf(RedisKeyUrlCounter, sid)).Err(); err != nil {
  // 隻影響統計,不影響主流程,列印錯誤日誌即可
  log.Printf("[UnShorten]Incr RedisKeyUrlCounter err, sid: %v\n", sid)
  }
  return val, nil
}

效果測試

配置

假設我們的短鏈域名為myurl.cn,配置host

127.0.0.1 myurl.cn

修改configs/shorturl/app.yaml的配置

base_url: http://myurl.cn/
redis_config:
 db_host: 127.0.0.1 db_port: 6379 db_passwd: db: 0

啟動服務

進入專案根目錄,啟動服務:

$ make run_short_url
go build -o build/shorturl ./cmd/shorturl  && ./build/shorturl
2022/12/14 20:58:47 resource.go:35: confFile: configs/shorturl/app.yaml
2022/12/14 20:58:47 resource.go:44: app config init succeed, conf: &{BaseHost:127.0.0.1 BasePort:80 RedisConfig:{DBHost:127.0.0.1 DBPort:6379 DBPasswd: DB:0}}
2022/12/14 20:58:47 app.go:40: App run in :80...

長鏈轉短連結口

curl -X POST \
  http://myurl.cn/api/shorten \
  -H 'cache-control: no-cache' \
  -H 'content-type: application/json' \
  -d '{"url":"https://www.baidu.com?name=SnDragon","expire_seconds":100}'

返回:

{
    "code": 0,
    "msg": "ok",
    "short_url": "http://myurl.cn/4C99"
}

重定向

瀏覽器開啟http://myurl.cn/4C99

重定向截圖

檢視詳情

curl -X GET \
  'http://myurl.cn/api/info?sid=4C99' \
  -H 'cache-control: no-cache' \
  -H 'postman-token: 82323d30-14b1-bac8-8198-2632fbe008e1'

返回:

{
    "code": 0,
    "msg": "ok",
    "info": {
        "origin_url": "https://www.baidu.com?name=SnDragon",
        "created_at": 1671023283,
        "expired_at": 1671023383,
        "counter": 1
    }
}

倉庫地址

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章