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))
}

基本思路:

  1. 数据存储
    • 选择合适的数据库,如 MySQL、Redis 等。
    • 创建表来存储短链的信息,包括短链字符串、原始长链、创建时间、过期时间等字段。
  2. 生成短链
    • 设计一个算法来生成唯一的短链字符串。
    • 获取用户提供的原始长链和期望的过期时间。
    • 将短链信息插入到数据库中,并设置正确的过期时间。
  3. 短链访问
    • 接收用户的短链访问请求。
    • 根据短链字符串从数据库中查询对应的信息。
    • 检查当前时间是否超过了短链的过期时间。
    • 如果未过期,进行重定向到原始长链;如果已过期,返回相应的提示信息,如“短链已过期”。
  4. 后台管理(可选)
    • 提供一个管理界面,用于查看短链的详细信息、修改过期时间、删除短链等操作。
  5. 并发处理
    • 如果多个请求同时访问短链服务,需要处理并发安全问题。可以使用锁或者数据库的事务来保证数据的一致性。
  6. 监控和日志
    • 记录关键的操作日志,如短链的生成、访问、过期等。
    • 监控数据库的性能、服务的响应时间等指标,以便及时发现和解决问题。
  7. 错误处理
    • 对数据库操作、网络请求等可能出现的错误进行全面的处理,返回友好的错误提示。
  8. 优化和扩展
    • 根据实际的使用情况,对服务进行性能优化,如缓存常用的短链信息。
    • 考虑支持分布式部署,以应对高并发和大流量的访问。
posted @ 2024-07-14 13:33  李若盛开  阅读(192)  评论(0)    收藏  举报