使用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语言开发一个短链接服务:六、链接跳转的更多相关文章
- Knative 实战:三步走!基于 Knative Serverless 技术实现一个短网址服务
短网址顾名思义就是使用比较短的网址代替很长的网址.维基百科上面的解释是这样的: 短网址又称网址缩短.缩短网址.URL 缩短等,指的是一种互联网上的技术与服务,此服务可以提供一个非常短小的 URL 以代 ...
- 最近做了一个短网址服务 di81.com
最近做了一个短网址服务: di81.com 项目中有一处需求,需要把长网址缩为短网址,把结果通过短信.微信等渠道推送给客户.刚开始直接使用网上现成的开放服务,然后在某个周末突然手痒想自己动手实现一 ...
- 使用go语言开发一个后端gin框架的web项目
用liteide来开发go的后端项目,需要注意的是环境变量要配置正确了 主要是GOROOT, GOPATH, GOBIN, PATH这几个, GOPATH主要用来存放要安的包,主要使用go get 来 ...
- ubuntu下使用C语言开发一个cgi程序
主要步骤是: 1. 开发一个C程序(在标准输出中输出HTML字符串) 2. 复制到apache2的cgi-bin目录去 3. 在httpd.conf中开启cgi功能(我似乎没用到,也可以使用cgi) ...
- 用 Go 快速开发一个 RESTful API 服务
何时使用单体 RESTful 服务 对于很多初创公司来说,业务的早期我们更应该关注于业务价值的交付,而单体服务具有架构简单,部署简单,开发成本低等优点,可以帮助我们快速实现产品需求.我们在使用单体服务 ...
- 开发一个成功APP的六个技巧
越来越多的人开始使用智能手机,平板电脑或其他的移动设备.出于这个原因,移动APP开发已成为当今软件开发中增长最快的领域之一.本文提供九个简单而有效的提示,可帮助您规划和实施成功的移动APP. 1.目标 ...
- 02 . 02 . Go之Gin+Vue开发一个线上外卖应用(集成第三方发送短信和xorm生成存储数据库表)
集成第三方发送短信 介绍 用户登录 用户登录有两种方式: 短信登录,密码登录 短信登录是使用手机号和验证码进行登录 短信平台 很多云平台,比如阿里云,腾讯云,七牛云等云厂商,向程序开发者提供了短信验证 ...
- Java 网址短链接服务原理及解决方案
一.背景 现在在各种圈的产品各种推广地址,由于URL地址过长,不美观.不方便收藏.发布.传播以及各种发文字数限制等问题,微信.微博都在使用短链接技术.最近由于使用的三方的生成.解析短链接服务开始限制使 ...
- 用solidity语言开发代币智能合约
智能合约开发是以太坊编程的核心之一,而代币是区块链应用的关键环节,下面我们来用solidity语言开发一个代币合约的实例,希望对大家有帮助. 以太坊的应用被称为去中心化应用(DApp),DApp的开发 ...
- .net core实践系列之短信服务-架构优化
前言 通过前面的几篇文章,讲解了一个短信服务的架构设计与实现.然而初始方案并非100%完美的,我们仍可以对该架构做一些优化与调整. 同时我也希望通过这篇文章与大家分享一下,我的架构设计理念. 源码地址 ...
随机推荐
- NC50505 二叉苹果树
题目链接 题目 题目描述 有一棵二叉苹果树,如果数字有分叉,一定是分两叉,即没有只有一个儿子的节点.这棵树共N个节点,标号1至N,树根编号一定为1. 我们用一根树枝两端连接的节点编号描述一根树枝的位置 ...
- SavedStateHandle的介绍----ViewModel不具备保存状态数据的功能
LiveData本身不能在进程销毁中存活,当内存不足时,Activity被系统杀死,ViewModel本身也会被销毁. 为了保存LiveData的数据,使用SavedStateHandle. 事故场景 ...
- MySQL 幻象行
当同一个查询在不同的时间产生不同的行集时,就会出现所谓的幻像问题.例如,如果执行了两次SELECT,但是第二次返回了第一次没有返回的行,那么该行就是一个"幻象"行. 假设在表chi ...
- java ArrayList排序不区分大小写
最近在做代码勇士codewars的题目,顺便记录一下自己的解决方案. 1.排序类 1.1 不使用预定义比较器 package com.dylan.practice; import java.util. ...
- Java并发编程实例--11.在线程组中处理未检查异常
第8个例子讲了如何在线程中捕捉未检查异常,本例将介绍如何在线程组中处理未检查异常. Task.java package com.dylan.thread.ch1.c11.task; import ja ...
- Spring源码之容器的功能拓展-ApplicationContext
目录 一.解析预备 刷新上下文环境 例如对系统属性或者环境变量进行校验和准备 二.初始化 BeanFactory 并进行 Xml 配置文件的读取 三.对BeanFactory 各种功能填充 四.激活以 ...
- win32-读取控制台中所有的字符串
我们可以先读取字符串所占的行数,再乘以控制台的实际宽度 bool ReadFromConsole() { HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDL ...
- 【Android 逆向】【攻防世界】人民的名义-抓捕赵德汉1-200
1. 这一题下载下来是个jar文件,感觉很android关系不大,但还是放在了mobile这个分类下了 2. 直接java jar运行,提示需要输入密码 # java -jar 169e139f152 ...
- java轻量级规则引擎easy-rules使用介绍
我们在写业务代码经常遇到需要一大堆if/else,会导致代码可读性大大降低,有没有一种方法可以避免代码中出现大量的判断语句呢? 答案是用规则引擎,但是传统的规则引擎都比较重,比如开源的Drools,不 ...
- EasyExcel使用及自定义设置单元格样式
EasyExcel使用及自定义设置单元格样式 https://www.cnblogs.com/Hizy/p/11825886.html easyexcel 自动设置列宽 https://www.man ...