防止缓存击穿-单飞模式 golang

缓存击穿是指在缓存失效的时刻,大量的请求过来,回被同时打到下游,给下游造成瞬时压力,解决的办法就是只允许少量的请求打到下游,然后回填到缓存,其他请求还是从缓存拿数据。

解决办法一,使用分布式锁,确保只有一个请求获取到这个锁,其他请求去轮训缓存。特点是,为了保护下游,伤害了自己和redis,因为大量的请求停留在这里造成内存和CPU升高,但是大家都知道,接口服务器的性能不是问题,所以这个方案可行。

此处主要介绍方案二,那就是单飞模式 SingleFlight,在 golang 中有对应的实现 golang/groupcache ,它跟方案一的区别是讲分布式的锁改成了内存锁,没有了轮训redis这个操作;其次就是打到下游的请求要多一点,但是不构成压力。

go get -u github.com/golang/groupcache

先来模拟缓存击穿的情况

package main

import (
	"fmt"
	"sync"
	"time"

	"github.com/golang/groupcache/singleflight"
)

var cache sync.Map
var wg sync.WaitGroup
var cachekey = "key"
var cacheval = "test"
var g singleflight.Group

func main() {
    
    
	for i := 0; i < 20; i++ {
    
    
		go action()
	}
	wg.Wait()
}

func action() {
    
    
	wg.Add(1)
	c, ok := cache.Load(cachekey)
	if !ok {
    
    
		SetCache(cachekey, cacheval)
		c = cacheval
	}
	fmt.Println(c)
	wg.Done()
}

func SetCache(key string, val string) bool {
    
    
	defer fmt.Println("SetCache...")
	time.Sleep(10 * time.Millisecond)
	cache.Store(key, val)
	return true
}

go run main.go

从打印信息可以看出SetCache被调用了多次。

下面使用单飞模式

func action2() {
    
    
	wg.Add(1)
	c, ok := cache.Load(cachekey)
	if !ok {
    
    
		fn := func() (interface{
    
    }, error) {
    
    
			SetCache(cachekey, cacheval)
			return cacheval, nil
		}
		cc, _ := g.Do(cachekey, fn)
		c = cc.(string)
	}
	fmt.Println(c)
	wg.Done()
}

从打印信息可以看出SetCache被调用了一次。

源码部分

// Package singleflight provides a duplicate function call suppression
// mechanism.
package singleflight

import "sync"

// call is an in-flight or completed Do call
type call struct {
    
    
	wg  sync.WaitGroup
	val interface{
    
    }
	err error
}

// 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
}

// 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.
func (g *Group) Do(key string, fn func() (interface{
    
    }, error)) (interface{
    
    }, error) {
    
    
	g.mu.Lock()
	if g.m == nil {
    
    
		g.m = make(map[string]*call)
	}
	if c, ok := g.m[key]; ok {
    
    
		g.mu.Unlock()
		c.wg.Wait()
		return c.val, c.err
	}
	c := new(call)
	c.wg.Add(1)
	g.m[key] = c
	g.mu.Unlock()

	c.val, c.err = fn()
	c.wg.Done()

	g.mu.Lock()
	delete(g.m, key)
	g.mu.Unlock()

	return c.val, c.err
}

猜你喜欢

转载自blog.csdn.net/raoxiaoya/article/details/123842683
今日推荐