将长地址缩短到一个很短的地址, 用户点击短地址会重定向到长地址
Go参考项目 https://github.com/missops/go-shortlink

基本原理

可以使用发号器(原理参考雪花算法,能保证唯一性即可),给每一个长链接发一个号码。这样子,第一个来的长网址对应的短链接是 https://domain/1,第二个长链接对应的短链接是 https://domain/2,依次递增。拿到号码之后,我们可以将该号码作为主键存储该长网址记录,同时因为 MySQL 根据主键去获取记录的速度是超级快的,所以这种方式我们不需要担心查询的性能问题。
这里我们唯一需要关心的就是发号策略了,业务量不大的话,我们可以直接用 MySQL 的自增序列来实现;如果是大型应用,就需要考虑高并发了。我们可以多部署一些节点,比如部署 1000 个节点,每个发号器发完一个号之后不在自增 1 ,而是自增 1000。比如对于 1 号发号器,其发送的号码依次是:1、1001、2001、3001…,如对于 2 号发号器,其发送的号码依次是:2、1002、2002、3002,如此一来,即保证了我们的发号器永远不会发出重复的号码,也保证了号码是递增的,主键递增对于数据库来说太重要了。如果不想直接用1,2,3,4这样显示可以将其转成62进制再发给用户

那如果遇到相同的长链接如何处理呢,直接查表吗?
直接查表怕是会浪费太多时间,我们可以做一个 LRU 缓存,当有请求过来时,我们只需要判断该网址是否在 LRU 缓存中即可,若存在,则直接返回,否则直接生成对应关系后将该记录加入缓存。
只不过这样一来的话,对于那些不热门的网址,可能会生成多个对应关系,事实上我们也没必要非得做到一一对应,一个长链接对应多个短链接对业务来说没什么影响,而且不热门网址出现频率本来就很低,不会浪费多少空间。

由上图我们可以得知,浏览器请求短链接的时候服务器返回了 302 状态码,然后浏览器重新发起了一次请求到长链接,主要原理就是用到了重定向,我们知道 302 状态码和 301 状态码都是表示重定向,那么为啥返回 302 而不是 301 呢。
301 是永久重定向,浏览器第一次拿到长链接后,后面再去请求短链接都不会在请求短链接服务器了,而是直接从本地缓存获取,正常来说短链接一经生成是不会在变化的了,那么使用 301 状态码不管在正常逻辑方面还是 http 语义方面都是合情合理的呀,同时对短链接服务器的压力也会小很多。但是如果使用 301 状态码我们是无法统计到该链接的点击次数的,如果我们想分析活动的效果的话,点击次数可是一个很重要的指标哦,返回 302 增加点服务器压力是值得的。

系统设计

数据库设计

字段:id token(短网址)(加索引) url(长网址) creat_time

API 接口

1. Post /api/shorten

将长地址转为短地址
body参数:

url string 需要转换的长地址
minutes int 过期时间(分钟)

响应:

shortlink
string 短地址

2. Get /api/info?shortlink=shortlink

获取短地址详细信息
url参数:

shortlink
string 短地址


响应:
长地址
创建时间
过期时间

3. Get /:shortlink - return 302

传短地址,重定向到长地址
302: 临时重定向 301 是永久重定向,不能保存用户行为

流程

image.png

主服务模块

image.png
image.png

中间件模块
存储模块
https://www.imooc.com/video/19706

编码方案

  1. import (
  2. "crypto/md5"
  3. "encoding/base64"
  4. "encoding/hex"
  5. "fmt"
  6. "math/rand"
  7. )
  8. const maxTry = 1024
  9. const urlLen = 8
  10. func Encode(url string) (tiny string, err error) {
  11. for i := 0; i < maxTry; i++ {
  12. tiny = EncodeWithSalt(url, i)
  13. if err = m.DB.FindOrCreate(tiny); err == nil {
  14. return
  15. }
  16. }
  17. return
  18. }
  19. func EncodeWithSalt(url string, salt int) string {
  20. var urlWithSalt = url
  21. if salt > 0 {
  22. urlWithSalt = fmt.Sprintf("%d%s", salt, url)
  23. }
  24. return hashToKCChars(urlWithSalt, urlLen)
  25. }
  26. func hashToKChars(url string, k int) string {
  27. // 1. md5 hex 32个字符
  28. tiny := getMD5String(url)
  29. // 2. base64 43个字符
  30. tiny = base64.URLEncoding.EncodeToString([]byte(tiny))
  31. // 3. random get k chars, ignore last char "="
  32. start := rand.Intn(len(tiny) - k - 1)
  33. return tiny[start : start+k]
  34. }
  35. func getMD5String(text string) string {
  36. h := md5.New()
  37. h.Write([]byte(text))
  38. return hex.EncodeToString(h.Sum(nil))
  39. }

base64 编码后,长度为 k 的路径存在 64^k 种可能性:

k = 6, 64^6 = 68,719,476,736 = 687 亿
k = 8, 64^8 = 281,474,976,710,656 = 281 万亿

基本可以满足当前需求