章节

使用Go语言开发一个短链接服务:一、基本原理  

使用Go语言开发一个短链接服务:二、架构设计

使用Go语言开发一个短链接服务:三、项目目录结构设计

使用Go语言开发一个短链接服务:四、生成code算法

使用Go语言开发一个短链接服务:五、添加和获取短链接

使用Go语言开发一个短链接服务:六、链接跳转

  源码:https://gitee.com/alxps/short_link

  上一篇说了添加和获取短链接逻辑,这一篇将服务的核心,链接跳转。

代码

  咱先上代码,再说思路。

app/server/handler/redirect.go

package handler

import (
"log/slog" "github.com/gin-gonic/gin" "github.com/1911860538/short_link/app/component"
"github.com/1911860538/short_link/app/server/service"
) var redirectSvc = service.RedirectSvc{
Cache: component.Cache,
Database: component.Database,
} func RedirectHandler(c *gin.Context) {
code := c.Param("code") res, err := redirectSvc.Do(c.Request.Context(), code)
if err != nil {
slog.Error("GetLinkHandler错误", "err", err)
c.AbortWithStatusJSON(res.StatusCode, gin.H{
"detail": "服务内部错误",
})
return
} if !res.Redirect {
c.AbortWithStatusJSON(res.StatusCode, gin.H{
"detail": res.Msg,
})
return
} c.Redirect(res.StatusCode, res.LongUrl)
}

app/server/service/redirect.go

package service

import (
"context"
"fmt"
"net/http"
"time" "golang.org/x/sync/singleflight" "github.com/1911860538/short_link/app/component"
"github.com/1911860538/short_link/config"
) type RedirectSvc struct {
Cache component.CacheItf
Database component.DatabaseItf
} type RedirectRes struct {
StatusCode int
Msg string Redirect bool
LongUrl string
} var (
confRedirectStatusCode = config.Conf.Core.RedirectStatusCode
confCodeTtl = config.Conf.Core.CodeTtl
confCodeLen = config.Conf.Core.CodeLen
confCacheNotFoundValue = config.Conf.Core.CacheNotFoundValue sfGroup singleflight.Group
) func (s *RedirectSvc) Do(ctx context.Context, code string) (RedirectRes, error) {
if len(code) != confCodeLen {
return s.notFound(code)
} longUrl, err := s.Cache.Get(ctx, code)
if err != nil {
return s.internalErr(err)
} // confCacheNotFoundValue,用来在缓存标识某个code不存在,防止缓存穿透
// 防止当某个code在数据库和缓存都不存在,大量无用请求反复读取缓存和数据库
if longUrl == confCacheNotFoundValue {
return s.notFound(code)
} if longUrl != "" {
return s.redirect(longUrl)
} // 使用singleflight防止缓存击穿
// 防止某个code缓存过期,大量该code请求过来,造成全部请求去数据读值
result, err, _ := sfGroup.Do(code, func() (any, error) {
link, err := s.getLinkSetCache(ctx, code)
if err != nil {
return nil, err
}
return link, nil
}) if err != nil {
return s.internalErr(err)
}
if result == nil {
return s.notFound(code)
}
link, ok := result.(*component.Link)
if !ok {
err := fmt.Errorf("singleflight group.Do返回值%v,类型错误,非*component.Link", result)
return s.internalErr(err)
}
if link == nil {
return s.notFound(code)
} return s.redirect(link.LongUrl)
} func (s *RedirectSvc) getLinkSetCache(ctx context.Context, code string) (*component.Link, error) {
filter := map[string]any{
"code": code,
}
link, err := s.Database.Get(ctx, filter)
if err != nil {
return nil, err
} if link == nil || link.LongUrl == "" || link.Expired() {
if err := s.Cache.Set(ctx, code, confCacheNotFoundValue, confCodeTtl); err != nil {
return nil, err
} return nil, nil
} var ttl int
if link.Deadline.IsZero() {
ttl = confCodeTtl
} else {
if remainSeconds := int(link.Deadline.Sub(time.Now().UTC()).Seconds()); remainSeconds < confCodeTtl {
ttl = remainSeconds
} else {
ttl = confCodeTtl
}
}
if err := s.Cache.Set(ctx, code, link.LongUrl, ttl); err != nil {
return nil, err
} return link, nil
} func (s *RedirectSvc) notFound(code string) (RedirectRes, error) {
return RedirectRes{
StatusCode: http.StatusNotFound,
Msg: fmt.Sprintf("短链接(%s)无对应的长链接地址", code),
}, nil
} func (s *RedirectSvc) redirect(longUrl string) (RedirectRes, error) {
return RedirectRes{
StatusCode: confRedirectStatusCode,
Redirect: true,
LongUrl: longUrl,
}, nil
} func (s *RedirectSvc) internalErr(err error) (RedirectRes, error) {
return RedirectRes{
StatusCode: http.StatusInternalServerError,
}, err
}

  handler主要逻辑为:从url path获取code,假如我们一个短链接url为https://a.b.c/L0YdcA, code即为"L0YdcA",code作为参数调用跳转service,根据service返回结果响应错误信息,或者跳转到目标长链接。

  service主要逻辑为:先从缓存获取code对应的长链接url,如果缓存中存在,则跳转。否则读取数据库获取长链接,如果数据库存在,将code和长链接数据写入缓存并跳转;否则返回404。

  当然,通过上面的代码发现,除了前面service主要逻辑,加了两点料,如何应对缓存穿透和缓存击穿(什么是缓存雪崩、缓存击穿、缓存穿透?)。

缓存穿透

  概念:我们使用缓存大部分情况都是通过Key查询对应的值,假如发送的请求传进来的key是不存在缓存中的,查不到缓存就会去数据库查询。假如有大量这样的请求,这些请求像“穿透”了缓存一样直接打在数据库上,这种现象就叫做缓存穿透。

  在我们短链接业务中,某个客户申请了一个短链接,并设置有效期到72小时后。该客户将连接嵌入到微博内容中,假如72小时后,该微博突然火了。那么大量这个短链接请求进来,按照常规逻辑,这个短链接已过期,缓存中没有这个key,自然就都去请求数据库。我们就遇到缓存击穿。

  两种应对之道:1、设置请求过来的不存在的code在缓存中一个标志为不存在的零值;2、使用布隆过滤器

  说一下布隆过滤器,详细原理等可以看这篇,5 分钟搞懂布隆过滤器,亿级数据过滤算法你值得拥有。本文不展开细说,总之布隆过滤器作用相当于一个数据容器,它可以使用很小的内存,保存大量的数据,用来判断某个数据是否存在。Redis 4.0 的时候官方提供了插件机制,集成了布隆过滤器。Golang也有相应的布隆过滤器库(https://github.com/bits-and-blooms/bloom)。然而我们不使用布隆过滤器应对缓存穿透。理由如下:

    1、初始化时要将所有数据加载到布隆过滤器。想象一下,我们的服务非常火,数据库有上亿数据,要全表扫描。

    2、如果使用redis集成的布隆过滤器,我们要先查询对应的key,如果key不存在,再查询布隆过滤器,布隆过滤器也不存在,则无需请求数据库。而redis布隆过滤器只支持添加和查询操作,不支持删除操作。假如某个链接code已过期,数据库已删除这个数据,那么这个也应该从布隆过滤器删除,没法搞。

    3、假如我们使用Golang第三方库,也就是在程序内存中构建布隆过滤器。我们先查询布隆过滤器,布隆过滤器中不存在,则不需要请求缓存和数据库。首先,全量数据全部加载到内存布隆过滤器,程序启动时间无法估量。另一致命问题是,多个节点服务实例运行,内存的数据同步问题。假如有3个短链接服务实例运行A/B/C,实例B接收到添加短链接请求,添加这个短链接到自己内存的布隆过滤器,要怎么实时通知并更新另外两个服务实例的布隆过滤器呢,难搞。

  综上,我们使用应对缓存穿透策略为,设置请求过来的不存在的code在缓存中存一个标志为不存在的零值(代码中提到的confCacheNotFoundValue)。

缓存击穿

  概念:突然间这个Key失效了,导致大并发全部打在数据库上,导致数据库压力剧增。这种现象就叫做缓存击穿。

  我们短链接服务中,所有短链接在缓存全部设置了有效期,即使这个短链接永不过期。一个短链接code在缓存中失效,这个code非常热点,请求并发很高,则大量请求读数据库然后写缓存。

  两种应对之道:1、如果业务允许的话,对于热点的key可以设置永不过期的key。2、使用互斥锁。如果缓存失效的情况,只有拿到锁才可以查询数据库,降低了在同一时刻打在数据库上的请求,防止数据库打死。

  设置不过期的key不用考虑,有过期时间的code一定要设置过期时间,不过期的code,可能短链接创建前期会被频繁访问,到后期就很可能成为僵尸数据,一致占据缓存。

  使用互斥锁,golang有个非常适合解决这个问题的库,单飞:golang.org/x/sync/singleflight。不了解singleflight,可以先瞄一眼这里(golang防缓存击穿神器【singleflight】)。singleflight ,大量同一个code请求数据库(耗时io操作),那么在内存维护一个全局变量,它有一个互斥锁的map保存code,后续并发请求过来,发现code在map存在,说明已经有请求在获取这个code,那么只需等前面请求拿到结果,后面的请求都用这个结果就行。如此多个并发请求数据库,就合并为一个请求到数据库了,妙妙妙!

  singleflight源码就两个文件,其中一个为单元测试。看看代码singleflight.go

// call is an in-flight or completed singleflight.Do call
type call struct {
wg sync.WaitGroup // These fields are written once before the WaitGroup is done
// and are only read after the WaitGroup is done.
val interface{}
err error // These fields are read and written with the singleflight
// mutex held before the WaitGroup is done, and are read but
// not written after the WaitGroup is done.
dups int
chans []chan<- Result
} // Group represents a class of work and forms a namespace in
// which units of work can be executed with duplicate suppression.
type Group struct {
mu sync.Mutex // protects m
m map[string]*call // lazily initialized
} // Result holds the results of Do, so they can be passed
// on a channel.
type Result struct {
Val interface{}
Err error
Shared bool
} // Do executes and returns the results of the given function, making
// sure that only one execution is in-flight for a given key at a
// time. If a duplicate comes in, the duplicate caller waits for the
// original to complete and receives the same results.
// The return value shared indicates whether v was given to multiple callers.
func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
g.mu.Lock()
if g.m == nil {
g.m = make(map[string]*call)
}
if c, ok := g.m[key]; ok {
c.dups++
g.mu.Unlock()
c.wg.Wait() if e, ok := c.err.(*panicError); ok {
panic(e)
} else if c.err == errGoexit {
runtime.Goexit()
}
return c.val, c.err, true
}
c := new(call)
c.wg.Add(1)
g.m[key] = c
g.mu.Unlock() g.doCall(c, key, fn)
return c.val, c.err, c.dups > 0
}

  细心的大佬可能注意到,我们使用singleflight是在读取数据库这里,为什么不在读缓存时使用singleflight。如果缓存这里使用singleflight,可以有效减少对缓存的读取io啊,多好啊。

  我来反驳一下这个天真的想法:

    1、读取缓存很快

    2、singleflight要通过互斥锁sync.Mutex读写一个全局变量的map。假如不管有没有过期的所有code都来,都经过singleflight,互斥读写这个全局map,数据竞争大。而针对数据库singleflight,仅仅会是缓存过期的code互斥读写map,数据竞争小。

总结

  写完了。

使用Go语言开发一个短链接服务:六、链接跳转的更多相关文章

  1. Knative 实战:三步走!基于 Knative Serverless 技术实现一个短网址服务

    短网址顾名思义就是使用比较短的网址代替很长的网址.维基百科上面的解释是这样的: 短网址又称网址缩短.缩短网址.URL 缩短等,指的是一种互联网上的技术与服务,此服务可以提供一个非常短小的 URL 以代 ...

  2. 最近做了一个短网址服务 di81.com

    最近做了一个短网址服务:   di81.com 项目中有一处需求,需要把长网址缩为短网址,把结果通过短信.微信等渠道推送给客户.刚开始直接使用网上现成的开放服务,然后在某个周末突然手痒想自己动手实现一 ...

  3. 使用go语言开发一个后端gin框架的web项目

    用liteide来开发go的后端项目,需要注意的是环境变量要配置正确了 主要是GOROOT, GOPATH, GOBIN, PATH这几个, GOPATH主要用来存放要安的包,主要使用go get 来 ...

  4. ubuntu下使用C语言开发一个cgi程序

    主要步骤是: 1. 开发一个C程序(在标准输出中输出HTML字符串) 2. 复制到apache2的cgi-bin目录去 3. 在httpd.conf中开启cgi功能(我似乎没用到,也可以使用cgi) ...

  5. 用 Go 快速开发一个 RESTful API 服务

    何时使用单体 RESTful 服务 对于很多初创公司来说,业务的早期我们更应该关注于业务价值的交付,而单体服务具有架构简单,部署简单,开发成本低等优点,可以帮助我们快速实现产品需求.我们在使用单体服务 ...

  6. 开发一个成功APP的六个技巧

    越来越多的人开始使用智能手机,平板电脑或其他的移动设备.出于这个原因,移动APP开发已成为当今软件开发中增长最快的领域之一.本文提供九个简单而有效的提示,可帮助您规划和实施成功的移动APP. 1.目标 ...

  7. 02 . 02 . Go之Gin+Vue开发一个线上外卖应用(集成第三方发送短信和xorm生成存储数据库表)

    集成第三方发送短信 介绍 用户登录 用户登录有两种方式: 短信登录,密码登录 短信登录是使用手机号和验证码进行登录 短信平台 很多云平台,比如阿里云,腾讯云,七牛云等云厂商,向程序开发者提供了短信验证 ...

  8. Java 网址短链接服务原理及解决方案

    一.背景 现在在各种圈的产品各种推广地址,由于URL地址过长,不美观.不方便收藏.发布.传播以及各种发文字数限制等问题,微信.微博都在使用短链接技术.最近由于使用的三方的生成.解析短链接服务开始限制使 ...

  9. 用solidity语言开发代币智能合约

    智能合约开发是以太坊编程的核心之一,而代币是区块链应用的关键环节,下面我们来用solidity语言开发一个代币合约的实例,希望对大家有帮助. 以太坊的应用被称为去中心化应用(DApp),DApp的开发 ...

  10. .net core实践系列之短信服务-架构优化

    前言 通过前面的几篇文章,讲解了一个短信服务的架构设计与实现.然而初始方案并非100%完美的,我们仍可以对该架构做一些优化与调整. 同时我也希望通过这篇文章与大家分享一下,我的架构设计理念. 源码地址 ...

随机推荐

  1. NC50505 二叉苹果树

    题目链接 题目 题目描述 有一棵二叉苹果树,如果数字有分叉,一定是分两叉,即没有只有一个儿子的节点.这棵树共N个节点,标号1至N,树根编号一定为1. 我们用一根树枝两端连接的节点编号描述一根树枝的位置 ...

  2. SavedStateHandle的介绍----ViewModel不具备保存状态数据的功能

    LiveData本身不能在进程销毁中存活,当内存不足时,Activity被系统杀死,ViewModel本身也会被销毁. 为了保存LiveData的数据,使用SavedStateHandle. 事故场景 ...

  3. MySQL 幻象行

    当同一个查询在不同的时间产生不同的行集时,就会出现所谓的幻像问题.例如,如果执行了两次SELECT,但是第二次返回了第一次没有返回的行,那么该行就是一个"幻象"行. 假设在表chi ...

  4. java ArrayList排序不区分大小写

    最近在做代码勇士codewars的题目,顺便记录一下自己的解决方案. 1.排序类 1.1 不使用预定义比较器 package com.dylan.practice; import java.util. ...

  5. Java并发编程实例--11.在线程组中处理未检查异常

    第8个例子讲了如何在线程中捕捉未检查异常,本例将介绍如何在线程组中处理未检查异常. Task.java package com.dylan.thread.ch1.c11.task; import ja ...

  6. Spring源码之容器的功能拓展-ApplicationContext

    目录 一.解析预备 刷新上下文环境 例如对系统属性或者环境变量进行校验和准备 二.初始化 BeanFactory 并进行 Xml 配置文件的读取 三.对BeanFactory 各种功能填充 四.激活以 ...

  7. win32-读取控制台中所有的字符串

    我们可以先读取字符串所占的行数,再乘以控制台的实际宽度 bool ReadFromConsole() { HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDL ...

  8. 【Android 逆向】【攻防世界】人民的名义-抓捕赵德汉1-200

    1. 这一题下载下来是个jar文件,感觉很android关系不大,但还是放在了mobile这个分类下了 2. 直接java jar运行,提示需要输入密码 # java -jar 169e139f152 ...

  9. java轻量级规则引擎easy-rules使用介绍

    我们在写业务代码经常遇到需要一大堆if/else,会导致代码可读性大大降低,有没有一种方法可以避免代码中出现大量的判断语句呢? 答案是用规则引擎,但是传统的规则引擎都比较重,比如开源的Drools,不 ...

  10. EasyExcel使用及自定义设置单元格样式

    EasyExcel使用及自定义设置单元格样式 https://www.cnblogs.com/Hizy/p/11825886.html easyexcel 自动设置列宽 https://www.man ...