短链接的实现方案

​ 这个过程的核心逻辑是链接到短链接的一一对应,通过链接能够查询到这个唯一的短链接,通过短链接可以查询到唯一对应的链接。链接变短的方法可以通过hash实现,解决hash冲突可以通过一个全局自增id实现,通过链接加自增id之后的hash是唯一的。通过这个唯一值映链接的关系,通过hash值生成唯一短链接,将短链接和链接建议关系。其中自增id需要注意线程安全,可以通过redis实现,短链接到hash值以及链接的映射关系可以通过数据库或者redis存储实现。

主体之间的关系

image-20220726165806465
  • 分享的URL通过hash函数,获得唯一的hash值,通过唯一的hash值与短链接建立连接关系
  • 短链接的地址需要单独维护,要实现唯一性,可以通过redis实现自增id实现
  • hash函数可能出现hash冲突,可以通过多个hash函数组合成一个长hash值,也可通过增加hash函数的位数避免

数据库存储信息

image-20220726170046997
  • 通过hash值可以获取到短链接
  • 通过短链接可以获取到这个短链接的详情,例如URL信息,是否需要密码,过期时间
  • 通过短链接可以获取URL信息

在实现上可以考虑实际请求的性能要求和技术栈,在短链接唯一性上推荐用redis高性能NoSQL,其他的几个服务也可以使用这种键值对存储服务。

具体实现

创建短链接的过程

image-20220726171559125
  • 创建长连接,通过hash函数获取hash值,如果hash值已经存在,则直接返回对应短链接信息。
  • 如果hash值不存在,则获取短链接自增之后的新短链接
    • 建立hash和短链接的关系
    • 建立短链接和长连接的关系
    • 建立短链接和短链接详情的关系
  • 上述三个关系优先通过事务实现

数据存储方式,都可以通过k-v存储,如果使用redis,通过string即可。

访问分享的过程

image-20220726171837296

通过访问的短链接,查询是否有短链接到URL的关系,如果没有则返回404,如果存在,则返回HTTP code 307以及URL,临时跳转到URL。

代码实现

创建短链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
func (r *RedisClient) Shorten(url string, exp int64) (string, error) {
hashStr, err := gethash(url)
if err != nil {
return "", err
}
ctx := context.Background()

// 获取是否已经存在短地址
result, err := r.Cli.Get(ctx, fmt.Sprintf(URLHashKey, hashStr)).Result()
if err == nil {
// 有短地址,返回
return result, nil
} else if err != redis.Nil {
return "", err
}

// 没有短地址,创建短地址id
i, err := r.Cli.Incr(ctx, URLIDKEY).Result()
if err != nil {
return "", err
}
short := strconv.Itoa(int(i))
pipe := r.Cli.TxPipeline() // 这里使用pipeline
// 创建地址hash到短地址的映射
err = pipe.Set(ctx, fmt.Sprintf(URLHashKey, hashStr), short, time.Minute*time.Duration(exp)).Err()
if err != nil {
return "", err
}
// 创建短地址到地址的映射
err = pipe.Set(ctx, fmt.Sprintf(ShortlinkKey, short), url, time.Minute*time.Duration(exp)).Err()
if err != nil {
return "", err
}
// 创建短地址详情
details := ShortLinkInfo{
CreatedAt: time.Now().String(),
Url: url,
ExpirationInMinutes: int(exp),
}
marshal, _ := json.Marshal(details)
err = pipe.Set(ctx, fmt.Sprintf(ShortlinkDetailKey, short), marshal, time.Minute*time.Duration(exp)).Err()
if err != nil {
return "", err
}
res, err := pipe.Exec(ctx)
log.Println(res)
return short, nil
}

验证短链接

1
2
3
4
5
6
7
8
9
10
11
func (r *RedisClient) ShortLinkInfo(url string) (detail ShortLinkInfo, err error) {
ctx := context.Background()

// 获取是否已经存在短地址
result, err := r.Cli.Get(ctx, fmt.Sprintf(ShortlinkDetailKey, url)).Result()
if err != nil {
return
}
err = json.Unmarshal([]byte(result), &detail)
return
}

删除短链接

1
2
3
4
5
6
7
8
9
10
11
12
13
func (r *RedisClient) UnShort(url string) error {
ctx := context.Background()
_, err := r.Cli.Del(ctx, fmt.Sprintf(ShortlinkKey, url)).Result()
if err != nil {
return err
}
_, err = r.Cli.Del(ctx, fmt.Sprintf(URLHashKey, url)).Result()
if err != nil {
return err
}
_, err = r.Cli.Del(ctx, fmt.Sprintf(ShortlinkDetailKey, url)).Result()
return err
}

跳转

1
2
3
4
5
6
7
8
9
10
11
func (s *Storags) Redirect(c *gin.Context) {
id := c.Param("id")
info, err := s.ShortLinkInfo(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"msg": err.Error(),
})
return
}
c.Redirect(http.StatusTemporaryRedirect, info.Url)
}