将长地址缩短到一个很短的地址, 用户点击短地址会重定向到长地址
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 是永久重定向,不能保存用户行为
流程
主服务模块
中间件模块
存储模块
https://www.imooc.com/video/19706
编码方案
import (
"crypto/md5"
"encoding/base64"
"encoding/hex"
"fmt"
"math/rand"
)
const maxTry = 1024
const urlLen = 8
func Encode(url string) (tiny string, err error) {
for i := 0; i < maxTry; i++ {
tiny = EncodeWithSalt(url, i)
if err = m.DB.FindOrCreate(tiny); err == nil {
return
}
}
return
}
func EncodeWithSalt(url string, salt int) string {
var urlWithSalt = url
if salt > 0 {
urlWithSalt = fmt.Sprintf("%d%s", salt, url)
}
return hashToKCChars(urlWithSalt, urlLen)
}
func hashToKChars(url string, k int) string {
// 1. md5 hex 32个字符
tiny := getMD5String(url)
// 2. base64 43个字符
tiny = base64.URLEncoding.EncodeToString([]byte(tiny))
// 3. random get k chars, ignore last char "="
start := rand.Intn(len(tiny) - k - 1)
return tiny[start : start+k]
}
func getMD5String(text string) string {
h := md5.New()
h.Write([]byte(text))
return hex.EncodeToString(h.Sum(nil))
}
base64 编码后,长度为 k 的路径存在 64^k 种可能性:
k = 6, 64^6 = 68,719,476,736 = 687 亿
k = 8, 64^8 = 281,474,976,710,656 = 281 万亿
基本可以满足当前需求