golang分布式缓存项目 Day6 防止缓存击穿

该项目原作者:https://github.com/geektutu/7days-golang。本文旨在记录本人做该项目时的一些疑惑解答以及部分的测试样例以便于本人复习。

1 缓存雪崩、缓存击穿与缓存穿透

概念解析

  • 缓存雪崩:缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。缓存雪崩通常因为缓存服务器宕机、缓存的 key设置了相同的过期时间等引起。
  • 缓存击穿:一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到 DB ,造成瞬时DB请求量大、压力骤增。
  • 缓存穿透:查询一个不存在的数据,因为不存在则不会写到缓存中,所以每次都会去请求 DB,如果瞬间流量过大,穿透到 DB,导致宕机。

2 singleflight介绍

singleflight 是一个非常有用的包,它提供了一种机制来抑制对某个函数的多次重复调用。这个包特别适用于避免在高并发场景下对同一资源的重复请求,比如在缓存击穿问题中,多个请求同时访问同一个资源时,singleflight 可以确保这些请求中只有一个实际执行,其他请求则等待这个结果,从而减少对后端服务的压力

核心概念

singleflight 包中定义了一个名为 Group 的结构体类型,它表示一类工作,并形成一个命名空间,在这个命名空间中,可以使用重复抑制来执行工作单元。当你调用 Do 方法时,它接收一个键(key)和一个函数(fn)。这个键是用来标识请求的唯一性,而函数则是实际要执行的操作。Do 方法首先会检查是否已经有相同的请求正在处理中。如果有,那么当前请求就会被放入一个等待队列,直到第一个请求完成并返回结果。这时,所有等待的请求都会收到相同的结果。

使用场景

singleflight 常用于以下场景:

  • 缓存击穿:在高并发的情况下,某个热点数据缓存失效后,大量的请求直接访问数据库,造成数据库的压力过大,甚至宕机的现象。使用singleflight 可以确保即使在缓存失效时,也只有一次数据库请求被执行,其他请求等待这次请求的结果 。
  • 避免重复计算:在需要进行复杂计算且结果可以被多个请求共享的场景中,singleflight 可以确保计算只执行一次,其他请求共享这个结果,从而提高效率。

基本使用

使用 singleflight 时,首先需要创建一个 Group 实例,然后通过调用 Do 方法来执行具体的函数。如果相同的键对应的函数已经

被调用,那么后续的调用将会等待第一个调用的结果,而不是重复执行函数

go 复制代码
var g singleflight.Group

// 模拟耗时操作
func getData(key string) (string, error) {
    // 模拟数据库查询耗时
    time.Sleep(2 * time.Second)
    return "data for " + key, nil
}

func main() {
    // 第一次调用
    result1, err, shared := g.Do("key", func() (interface{}, error) {
        return getData("key")
    })
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Result1: %s, Shared: %v\n", result1, shared)

    // 第二次调用,将共享第一次调用的结果
    result2, err, shared := g.Do("key", func() (interface{}, error) {
        return getData("key")
    })
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Result2: %s, Shared: %v\n", result2, shared)
}

在这个例子中,即使 getData 函数被调用了两次,但由于 singleflight 的机制,实际的数据库查询操作只执行了一次,两次调用共享了这个结果

3 运用singleflight到geecache中

go 复制代码
type Group struct {
	name      string
	getter    Getter
	mainCache cache
	peers     PeerPicker
	// use singleflight.Group to make sure that
	// each key is only fetched once
	loader *singleflight.Group
}

func NewGroup(name string, cacheBytes int64, getter Getter) *Group {
    // ...
	g := &Group{
        // ...
		loader:    &singleflight.Group{},
	}
	return g
}

func (g *Group) load(key string) (value ByteView, err error) {
	// each key is only fetched once (either locally or remotely)
	// regardless of the number of concurrent callers.
	viewi, err := g.loader.Do(key, func() (interface{}, error) {
		if g.peers != nil {
			if peer, ok := g.peers.PickPeer(key); ok {
				if value, err = g.getFromPeer(peer, key); err == nil {
					return value, nil
				}
				log.Println("[GeeCache] Failed to get from peer", err)
			}
		}

		return g.getLocally(key)
	})

	if err == nil {
		return viewi.(ByteView), nil
	}
	return
}
  • 修改 geecache.go 中的 Group,添加成员变量 loader,并更新构建函数 NewGroup。
  • 修改 load 函数,将原来的 load 的逻辑,使用 g.loader.Do 包裹起来即可,这样确保了并发场景下针对相同的 key,load 过程只会调用一次。
相关推荐
Hover_Z_快跑1 小时前
RabbitMQ 集群降配
分布式·rabbitmq
nlog3n2 小时前
RabbitMQ八股文
分布式·rabbitmq
熏鱼的小迷弟Liu2 小时前
【RabbitMQ】RabbitMQ中死信交换机是什么?延迟队列呢?有哪些应用场景?
分布式·rabbitmq·ruby
熏鱼的小迷弟Liu2 小时前
【RabbitMQ】RabbitMQ如何保证消息不丢失?
分布式·rabbitmq
Gold Steps.2 小时前
Zookeeper 集群部署与管理实践
linux·分布式·zookeeper·云原生
初学者杰克2 小时前
记录一次,rabbitmq开启stomp插件之后,还是连不上15674端口的问题
分布式·rabbitmq
Rverdoser2 小时前
RabbitMQ:业务幂等、死信交换机
分布式·rabbitmq
apeiMark2 小时前
Kafka-ConsumerRecord
分布式·kafka·linq
小L~~~3 小时前
LFU(最不经常使用) 缓存算法实现
c++·算法·缓存
dapeng-大鹏3 小时前
如何基于Gone编写一个Goner对接Apollo配置中心(下)—— 对组件进行单元测试
golang·单元测试·apollo·gone