首页 > 其他分享 >使用Go语言开发一个短链接服务:六、链接跳转

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

时间:2024-03-26 12:12:12浏览次数:20  
标签:缓存 return nil err code 跳转 Go 链接

章节

使用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,数据竞争小。

 

总结

  写完了。

 

标签:缓存,return,nil,err,code,跳转,Go,链接
From: https://www.cnblogs.com/ALXPS/p/18094662

相关文章

  • 使用Go语言开发一个短链接服务:三、项目目录结构设计
    章节 使用Go语言开发一个短链接服务:一、基本原理 使用Go语言开发一个短链接服务:二、架构设计 使用Go语言开发一个短链接服务:三、项目目录结构设计 使用Go语言开发一个短链接服务:四、生成code算法 使用Go语言开发一个短链接服务:五、添加和获取短链接 使用Go语言开......
  • golang模板库之fasttemplate
    简介fasttemplate是一个比较简单、易用的小型模板库。fasttemplate的作者valyala另外还开源了不少优秀的库,如大名鼎鼎的fasthttp,前面介绍的bytebufferpool,还有一个重量级的模板库quicktemplate。quicktemplate比标准库中的text/template和html/template要灵活和易用很多,后面会专......
  • Go的可变参数函数
    可变函数是指可以接收可变数量的参数的函数。在Golang中,可以传递与函数签名中引用的类型相同的不同数量的参数。在声明可变函数时,最后一个参数的类型前会有一个省略号"...",这表明该函数可以用任意数量的该类型参数来调用,可以是0个、1个或者多个。这种类型的函数在不知道传递给......
  • Django之权限管理
    一,引入1.为什么要有权限?2.为什么要开发权限的组件?3.在web开发中,什么是权限?4.表结构的设计权限表IDURL1/user_list/2/customer_list/用户表IDUSER_NAME1root2root2角色/用户组表ID组1销售2开发用户与角色的......
  • CPPB 表中的TXN_CATEGORY
    cst_pac_period_balanceTXN_CATEGORY1    期初    2    成本更新:新成本或百分比变动    3    自有成本事务处理    4    非返工完成    5    成本更新:值变动    6    返工发放    7    返工完成    8    组......
  • google搜索如何修改搜索结果地区
    本文写于2024年03月25日,阅读时请注意时效。解决使用谷歌搜索引擎的时候经常会因为IP地址位置导致搜索结果的语言出现偏差,尤其是使用汉语进行搜索时常常会导致搜索结果中出现很多日语等语言结果造成干扰的问题。设置位置在:首页→右下角Settings→Searchsettings→左上角......
  • c语言中的goto语句,goto语句的使用
    在c语言中,goto语句与分支语句if,switch不同,也和循环语句while,for,do...while不同,goto语句被称为无条件转移语句,也被称为转向语句,其实和break,return语句是同一个类型。goto语句的使用一般都需要一个again进行配合,当使用goto语句时,程序会转跳回again处重新运行again后的程序。got......
  • 获取Book里所有sheet的名字,且带上超链接
     应用背景:        当一个excel有很多sheet的时候,来回切换sheet会比较复杂,所以我希望excel的第一页有目录,可以随着sheet的增加,减少,改名而随时可以去更新,还希望有超链接可以直接跳到该sheet。可以采用以下代码OptionExplicitSubList_Hyperlink()Dimk!,shk=0......
  • Typora结合PicGo + Github搭建个人图床
    目录一、GitHub仓库设置1、新建仓库2、创建Token并复制保存二、PicGo客户端配置1、下载&安装2、配置图床三、Typora配置一、GitHub仓库设置1、新建仓库点击主页右上角的+号创建 Newrepository填写仓库信息2、创建Token并复制保存点击右上角用户头像......
  • Django框架之中间件引入
    【一】中间件介绍之前我们通过给视图函数加上装饰器来判断用户是否登录,把没有登陆的用户请求跳转到登录页面。但是如果有很多的功能都需要在登录之后才能使用,那么每个功能对应的视图函数都需要加上装饰器,这样的话就略显繁琐通过django中间件就可以实现控制全局的效果【1】什......