Golang - 短链服务的设计与实现
短链服务的好处
- 缩短地址长度,留足更多空间的给有意义的内容
URL是没有意义的,有的原始URL很长,占用有效的屏幕空间。
- 可以很好的对原始URL内容管控。
有一部分网址可以会涵盖XX,暴力,广告等信息,这样可以通过用户的举报,完全管理这个连接将不出现在应用中,应为同样的URL通过加密算法之后,得到的地址是一样的。
- 可以很好的对原始URL进行行为分析
可以对一系列的网址进行流量,点击等统计,挖掘出大多数用户的关注点,这样有利于对项目的后续工作更好的作出决策。
- 短网址和短ID相当于间接提高了带宽的利用率、节约成本
- 链接太长在有些平台上无法自动识别为超链接
- 短链接更加简洁好看且安全,不暴露访问参数。而且,能规避关键词、域名屏蔽等手段
短链服务的原理
核心:将长的 URL 转化成短的 URL。
重定向301 和 302 的不同
- 301永久重定向:第一次请求拿到长链接后,下次浏览器再去请求短链的话,不会向短链服务器请求了,而是直接从浏览器的缓存里拿,减少对服务器的压力
- 302临时重定向:每次去请求短链都会去请求短网址服务器(除非响应中用 Cache-Control 或 Expired 暗示浏览器进行缓存)
使用 301 虽然可以减少服务器的压力,但是无法在 server 层获取到短网址的访问次数了,如果链接刚好是某个活动的链接,就无法分析此活动的效果以及用于大数据分析了。
而 302 虽然会增加服务器压力,但便于在 server 层统计访问数,所以如果对这些数据有需求,可以采用 302,因为这点代价是值得的,但是具体采用哪种跳转方式,还是要结合实际情况进行选型。
用Go实现一个简易版的短链服务(具有过期时间)
package main
import (
"database/sql"
"fmt"
"log"
"net/http"
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/gomodule/redigo/redis"
)
// ShortLink 表示短链结构体
type ShortLink struct {
ID int
ShortCode string
OriginalURL string
ExpireTime time.Time
}
// 生成短链
func generateShortCode() string {
// 这里可以使用更复杂的算法生成唯一短码
return fmt.Sprintf("%d", time.Now().UnixNano())
}
// 保存短链到 MySQL
func saveShortLinkToMySQL(db *sql.DB, shortLink ShortLink) error {
query := "INSERT INTO short_links (short_code, original_url, expire_time) VALUES (?,?,?)"
_, err := db.Exec(query, shortLink.ShortCode, shortLink.OriginalURL, shortLink.ExpireTime)
return err
}
// 从 MySQL 中获取短链
func getShortLinkFromMySQL(db *sql.DB, shortCode string) (ShortLink, error) {
query := "SELECT id, short_code, original_url, expire_time FROM short_links WHERE short_code =?"
row := db.QueryRow(query, shortCode)
var shortLink ShortLink
err := row.Scan(&shortLink.ID, &shortLink.ShortCode, &shortLink.OriginalURL, &shortLink.ExpireTime)
if err == sql.ErrNoRows {
return shortLink, fmt.Errorf("短链未找到")
} else if err!= nil {
return shortLink, err
}
return shortLink, nil
}
// 保存热点 URL 到 Redis
func saveHotURLToRedis(conn redis.Conn, shortCode string, originalURL string) error {
_, err := conn.Do("SET", shortCode, originalURL)
if err!= nil {
return err
}
// 设置过期时间
_, err = conn.Do("EXPIRE", shortCode, 3600) // 1 小时过期,可根据需求调整
return err
}
// 从 Redis 中获取热点 URL
func getHotURLFromRedis(conn redis.Conn, shortCode string) (string, error) {
originalURL, err := redis.String(conn.Do("GET", shortCode))
if err == redis.ErrNil {
return "", fmt.Errorf("Redis 中未找到热点短链")
} else if err!= nil {
return "", err
}
return originalURL, nil
}
// 处理短链请求
func handleShortLinkRequest(w http.ResponseWriter, r *http.Request) {
shortCode := r.URL.Path[1:]
// 尝试从 Redis 中获取
redisConn, err := redis.Dial("tcp", "localhost:6379")
if err!= nil {
log.Println("连接 Redis 失败:", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
defer redisConn.Close()
originalURL, err := getHotURLFromRedis(redisConn, shortCode)
if err == nil {
// 重定向到原始 URL
http.Redirect(w, r, originalURL, http.StatusFound)
return
}
// 从 MySQL 中获取
db, err := sql.Open("mysql", "username:password@tcp(localhost:3306)/database_name")
if err!= nil {
log.Println("连接 MySQL 失败:", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
defer db.Close()
shortLink, err := getShortLinkFromMySQL(db, shortCode)
if err!= nil {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "短链未找到")
return
}
// 检查是否过期
if time.Now().After(shortLink.ExpireTime) {
w.WriteHeader(http.StatusGone)
fmt.Fprintf(w, "短链已过期")
return
}
// 保存到 Redis 作为热点
err = saveHotURLToRedis(redisConn, shortCode, shortLink.OriginalURL)
if err!= nil {
log.Println("保存到 Redis 失败:", err)
}
// 重定向到原始 URL
http.Redirect(w, r, shortLink.OriginalURL, http.StatusFound)
}
func main() {
http.HandleFunc("/", handleShortLinkRequest)
log.Fatal(http.ListenAndServe(":8080", nil))
}
基本思路: