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

说明: 最后引出的singleflight方案的优化示例没有完全搞清楚,这周搞清楚会更新出来。

为了减缓数据库的压力,往往在数据库前增加一个缓存:

1. 缓存穿透

在缓存中查不到key,只能去数据库查询;当有大量请求直接穿透了缓存打到数据库,就是缓存穿透。

解决

  • 系统写好参数校验
  • 缓存空值,过期时间短一些
  • 布隆过滤器

2. 缓存雪崩

同一时间大规模key同时失效,大量的请求直接打在数据库上面,导致数据库压力巨大,如果在高并发的情况下,可能瞬间就会导致数据库宕机。

原因

  • Redis宕机
  • 大规模key使用了相同的过期时间

解决

  • 原有失效时间加随机值
  • 熔断机制
  • 数据库容灾,分库分表、读写分离
  • 防止Redis宕机: Redis集群

3. 缓存击穿

1. 概念及解决方案

大并发集中对一个热点的Key进行访问,突然间这个Key失效了,导致大并发全部打在数据库上,导致数据库压力剧增。

解决

  • 如果业务允许的话,对于热点的key可以设置永不过期的key
  • 使用互斥锁。如果缓存失效的情况,只有拿到锁才可以查询数据库,降低了在同一时刻打在数据库上的请求,防止数据库打死。当然这样会导致系统的性能变差。

2. singleflight

singleflightsingleflight

模拟场景,请求先走Redis,发现没有key,全部都走到了数据库:

package main

import (
	"context"
	"errors"
	"log"
	"sync"
	"time"
)

var errorNotExist = errors.New("not exist")

func main() {
	// 模拟透传ctx 设定超时时间
	ctx, cancel := context.WithTimeout(context.TODO(), time.Second*3)
	defer cancel()
	//模拟10个并发
	var wg sync.WaitGroup
	wg.Add(10)
	for i := 0; i < 10; i++ {
		go func() {
			defer wg.Done()
			data, err := fetchData(ctx, "key")
			if err != nil {
				log.Print(err)
				return
			}
			log.Println(data)
		}()
	}
	wg.Wait()
}

// 获取数据
func fetchData(ctx context.Context, key string) (string, error) {
	data, err := fetchDataFromCache(key)
	if err == errorNotExist {
		data, err = fetchDataFromDB(key)
		if err != nil {
			log.Println(err)
			return "", err
		}

		//TOOD: set cache
	} else if err != nil {
		return "", err
	}
	return data, nil
}

// 模拟从缓存中获取值,缓存中无该值
func fetchDataFromCache(key string) (string, error) {
	return "", errorNotExist
}

// 模拟从数据库中获取值
func fetchDataFromDB(key string) (string, error) {
	log.Printf("get %s from database", key)
	return "data", nil
}

// 执行输出
2021/10/19 14:04:36 get key from database
2021/10/19 14:04:36 data
2021/10/19 14:04:36 get key from database
2021/10/19 14:04:36 get key from database
2021/10/19 14:04:36 get key from database
2021/10/19 14:04:36 get key from database
2021/10/19 14:04:36 get key from database
2021/10/19 14:04:36 get key from database
2021/10/19 14:04:36 get key from database
2021/10/19 14:04:36 get key from database
2021/10/19 14:04:36 get key from database
2021/10/19 14:04:36 data
2021/10/19 14:04:36 data
2021/10/19 14:04:36 data
2021/10/19 14:04:36 data
2021/10/19 14:04:36 data
2021/10/19 14:04:36 data
2021/10/19 14:04:36 data
2021/10/19 14:04:36 data
2021/10/19 14:04:36 data

从以上出书可以看出,并发请求先到缓存,发现没有值,于是都打到了数据库,假设在真实业务场景中,并发量非常大,数据库可能会瞬间宕机。因此我们需要想办法将并发的请求减少:

fetchDatasingleflight
import "golang.org/x/sync/singleflight"

var sfg singleflight.Group

// 获取数据
func fetchData(ctx context.Context, key string) (string, error) {
	data, err := fetchDataFromCache(key)
	if err == errorNotExist {
		v, err, _ := sfg.Do(key, func() (interface{}, error) {
			return fetchDataFromDB(key)
			//set cache
		})
		if err != nil {
			log.Println(err)
			return "", err
		}

		//TOOD: set cache
		data = v.(string)
	} else if err != nil {
		return "", err
	}
	return data, nil
}

// 输出
2021/10/19 14:09:25 get key from database
2021/10/19 14:09:25 data
2021/10/19 14:09:25 data
2021/10/19 14:09:25 data
2021/10/19 14:09:25 data
2021/10/19 14:09:25 data
2021/10/19 14:09:25 data
2021/10/19 14:09:25 data
2021/10/19 14:09:25 data
2021/10/19 14:09:25 data
2021/10/19 14:09:25 data

可以看到此时只有一个请求进入数据库,其他的请求也正常返回了值,从而保护了后端DB。但是这样是否真正合理呢?

模拟首次请求hang住,则所有请求都会hang住,程序报错退出:

// 获取数据
func fetchData(ctx context.Context, key string) (string, error) {
	data, err := fetchDataFromCache(key)
	if err == errorNotExist {
		v, err, _ := sfg.Do(key, func() (interface{}, error) {
			select {}
			return fetchDataFromDB(key)
			//set cache
		})
		if err != nil {
			log.Println(err)
			return "", err
		}

		//TOOD: set cache
		data = v.(string)
	} else if err != nil {
		return "", err
	}
	return data, nil
}

程序报错,发生死锁:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0x0)
        D:/Program Files/Go/src/runtime/sema.go:56 +0x25
sync.(*WaitGroup).Wait(0xa80bf0)
        D:/Program Files/Go/src/sync/waitgroup.go:130 +0x71
main.main()
        D:/Go/src/github.com/test/main.go:34 +0x10f

goroutine 19 [select (no cases)]:
main.fetchData.func1()
        D:/Go/src/github.com/test/main.go:42 +0x17
golang.org/x/sync/singleflight.(*Group).doCall.func2(0xc00004be66, 0xc000052060, 0xa534c0)
        D:/Go/pkg/mod/golang.org/x/sync@v0.0.0-20210220032951-036812b2e83c/singleflight/singleflight.go:193 +0x6f
golang.org/x/sync/singleflight.(*Group).doCall(0xa4c980, 0xc00001e030, {0xa5c137, 0x3}, 0x0)
        D:/Go/pkg/mod/golang.org/x/sync@v0.0.0-20210220032951-036812b2e83c/singleflight/singleflight.go:195 +0xad
golang.org/x/sync/singleflight.(*Group).Do(0xb076f0, {0xa5c137, 0x3}, 0x0)
        D:/Go/pkg/mod/golang.org/x/sync@v0.0.0-20210220032951-036812b2e83c/singleflight/singleflight.go:108 +0x154
main.fetchData({0x0, 0x0}, {0xa5c137, 0x3})
        D:/Go/src/github.com/test/main.go:41 +0xb8
main.main.func1()
        D:/Go/src/github.com/test/main.go:26 +0x6c
created by main.main
        D:/Go/src/github.com/test/main.go:24 +0x85
DoChan select 
// 获取数据
func fetchData(ctx context.Context, key string) (string, error) {
	data, err := fetchDataFromCache(key)
	if err == errorNotExist {
		result := sfg.DoChan(key, func() (interface{}, error) {
			// 模拟出现问题,hang 住
			select {}
			return fetchDataFromDB(key)
			//set cache
		})

		select {
		case r := <-result:
			return r.Val.(string), r.Err
		case <-ctx.Done():
			return "", ctx.Err()
		}

	} else if err != nil {
		return "", err
	}
	return data, nil
}

此时若首次请求超时则会出现超时消息:

2021/10/19 14:23:21 context deadline exceeded
2021/10/19 14:23:21 context deadline exceeded
2021/10/19 14:23:21 context deadline exceeded
2021/10/19 14:23:21 context deadline exceeded
2021/10/19 14:23:21 context deadline exceeded
2021/10/19 14:23:21 context deadline exceeded
2021/10/19 14:23:21 context deadline exceeded
2021/10/19 14:23:21 context deadline exceeded
2021/10/19 14:23:21 context deadline exceeded
2021/10/19 14:23:21 context deadline exceeded
singleflightForget
go func() {
    log.Printf("forget key: %v\n", key)
    time.Sleep(100 * time.Millisecond)
    // logging
    g.Forget(key)
}()