Go语言缓存穿透解题思路(singleflight)

Casso_W · · 1164 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

缓存穿透:
  • 指用户请求的资源在缓存(redis等缓存中间件)中未命中,按照正常的逻辑,未命中的数据会去持久层(mysql/pg等)去查询并获取返回。
  • 但是一半持久层能承载的访问量非常有限(如果没有按照索引查找或需要很多字段时)。 当一个缓存未命中,用户体量足够大的情况下,一瞬间会有大量的请求直接打到持久层,导致超时甚至崩溃。
  • 这就需要一个可行的方案来处理类似的突发情况,当然,超时控制也是必要的。如果出现超时需要及时返回,否则超时也会造成整条业务线阻塞崩溃。
  • 超时需要考虑三大层级的超时,大家熟知的Nginx 超时(网关超时)/ 进程内超时 (goroutine超时控制)/ 服务间超时控制 (分布式架构下由于网络抖动等原因,需要做超时处理,不然请求堆积导致内存爆炸)
  • 这里就不讨论 这些超时控制以及如何实现,这里主要谈论 缓存击穿的方案(Go语言方案,其他语言思路也是相似的)
示例代码如下:
package main

import (
    "fmt"
    "sync"

    "golang.org/x/sync/singleflight"
)

var sg singleflight.Group
var wg sync.WaitGroup

func main() {
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go Req() // 模拟同时有10个请求该资源的请求未命中缓存
    }
    wg.Wait()
}

// Req 模拟新进入的请求,并且缓存未命中才会执行的操作
func Req() (interface{}, error) {
    defer wg.Done()
    val, err, _ := sg.Do("Once", Jobs)
    fmt.Println("得到返回值:", val)
    return val, err
}

// 实现符合Do()第二参数的函数签名,实际上应该是对持久层的访问,此处可添加超时控制
func Jobs() (res interface{}, err error) {
    fmt.Println("模拟一些操作") // 这次模拟可能会执行一次以上的Jobs(),因为第一个执行的单飞已经返回结果并释放了key,此时还有goroutine 未执行,一执行发现key还处于释放状态。
    // 实际场景中,当结果返回,就会填充缓存,所以基本不会存在新的请求重新打到持久层的情况,次数在可接受范围内。
    return "casso", nil
}

最后,如果硬件条件比较宽裕的情况下,还宽裕做得更为保险。

  • 在kafka集群中使用特定的topic 来专门接收缓存填充的消息,在起专门的消费kafka消息的服务,在该服务内进行缓存填充
  • 这样基本上不会对持久层造成什么压力,但是也要注意可能有多条重复的kafka消息,在填充缓存的服务中需要进行一定的判断,即缓存是否已存在
  • 还是有很多地方可以优化的,想提高可用性,那肯定是需要牺牲一定复杂度的
  • singleflight

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:Casso_W

查看原文:Go语言缓存穿透解题思路(singleflight)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1164 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传