- 缓存穿透、缓存击穿、缓存雪崩解决方案
缓存处理
缓存击穿
缓存击穿是指缓存中没有但数据库存在的对应key的值,由于缓存中key的过期时间到期,转而去数据库读取。当并发请求多大时会引发数据库压力,瞬间倍增造成崩溃。
缓存击穿是由于对于设置过期时间的key在某时刻被超高并发地访问形成“热点”,请求全部转发到数据库造成数据库压垮。
并发访问
- 使用Golang的
map
和WaitGroup
特性实现并发控制
例如:并发获取数据,先尝试从缓存获取数据,若缓存不存在则从数据库获取数据。
$ vim main.go
package main
import (
"errors"
"example/core"
"fmt"
"sync"
)
var (
wg sync.WaitGroup
sf core.SingleFlight
errorNotExist = errors.New("not exist")
)
//get DataFromDB 从数据库获取数据
func getDataFromDB(key string, i int) (string, error) {
fmt.Printf("worker %v: get data from database...\n", i)
return "db data success", nil
}
//getDataFromCache 从缓存中获取数据
func getDataFromCache(key string, i int) (string, error) {
return "", errorNotExist
}
//getData 获取数据
func getData(key string, i int) (string, error) {
//从缓存中获取数据
data, err := getDataFromCache(key, i)
if err != errorNotExist {
return "", err
} else if data != "" {
return data, nil
}
//从数据库获取数据
data, err = getDataFromDB(key, i)
if err != nil {
fmt.Printf("worker %v: db error: %v\n", i, err.Error())
return "", err
}
return data, nil
}
func main() {
count := 3
wg.Add(count)
//模拟多个并发请求
for i := 0; i < count; i++ {
go func(i int) {
defer wg.Done()
data, err := getData("key", i)
if err != nil {
return
}
fmt.Printf("worker %v: %v\n", i, data)
}(i)
}
wg.Wait()
}
$ go run main.go
worker 2: get data from database...
worker 2: db data success
worker 0: get data from database...
worker 0: db data success
worker 1: get data from database...
worker 1: db data success
此时发现,3个并发请求,缓存中不存在数据,都走的是数据库。数据库瞬时会压力过大,存在崩溃的机会。
防击穿
SingleFlight(单飞)针对多个并发请求对一个失效的key
进行获取源数据时,只让其他一个请求得到执行,其余均会阻塞等待执行的那个请求完毕后,将结果传递给阻塞的其他请求,达到防止击穿的效果。
- 保护下游,针对下游的同一批请求,只有一个负责去请求,其他等待结果。
$ vim ./core/singleflight.go
package core
import "sync"
//call 表示一个正在执行或已经完成的函数调用,即需要被执行的函数。
type call struct {
wg sync.WaitGroup //用于阻塞调用这个call的其他请求
value interface{} //记录回调函数返回值
err error //记录回调函数返回的错误
}
//SingleFlight 任务分组 防击穿
type SingleFlight struct {
mutex sync.Mutex //为map添加互斥锁以保证并发安全
callmap map[string]*call //对于每个需要获取的key存在一个对应的call
}
//Do 执行任务
func (sf *SingleFlight) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
//添加互斥锁
sf.mutex.Lock()
//初始化
if sf.callmap == nil {
sf.callmap = make(map[string]*call)
}
//若当前key对应的回调函数正在被执行
val, ok := sf.callmap[key]
if ok {
sf.mutex.Unlock() //解锁
val.wg.Wait() //阻塞等待执行中,等待其执行完毕后获取执行结果。
//返回结果
return val.value, val.err
}
//创建call
obj := new(call)
obj.wg.Add(1) //添加goroutine
//写入map后解锁
sf.callmap[key] = obj //写入callmap
sf.mutex.Unlock() //解锁
//执行获取key的回调函数
obj.value, obj.err = fn()
obj.wg.Done()
//重新上锁后删除key
sf.mutex.Lock()
delete(sf.callmap, key)
sf.mutex.Unlock()
//返回结果
return obj.value, obj.err
}
Do方法通过传入的key和回调函数,判断若key相同则回调函数只调用一次,同时同步阻塞。
Do方法通过WaitGroup来控制
- 在SingleFlight结构中设置一个map,若map中的key不存在则实例化call来包含值信息,同时将
key=>call
的对应关系存入map,存入map时使用mutex互斥锁以保证并发安全。 - 如果map中已经存在key则直接执行WaitGroup.Wait()
- 但回调函数fn()执行完毕之后,执行WaitGroup.Done()。
- 此时卡在第2步的方法得以执行返回结果
缓存防穿透
为数据库访问添加并发访问保护机制以防止穿透
$ vim main.go
package main
import (
"errors"
"example/core"
"fmt"
"sync"
)
var (
wg sync.WaitGroup
sf core.SingleFlight
errorNotExist = errors.New("not exist")
)
//get DataFromDB 从数据库获取数据
func getDataFromDB(key string, i int) (string, error) {
fmt.Printf("worker %v: get data from database...\n", i)
return "db data success", nil
}
//getDataFromCache 从缓存中获取数据
func getDataFromCache(key string, i int) (string, error) {
return "", errorNotExist
}
//getData 获取数据
func getData(key string, i int) (string, error) {
//从缓存中获取数据
data, err := getDataFromCache(key, i)
if err != errorNotExist {
return "", err
} else if data != "" {
return data, nil
}
//从数据库获取数据
val, err := sf.Do(key, func() (interface{}, error) {
return getDataFromDB(key, i)
})
if err != nil {
fmt.Printf("worker %v: db error: %v\n", i, err.Error())
return "", err
}
data = val.(string)
return data, nil
}
func main() {
count := 3
wg.Add(count)
//模拟多个并发请求
for i := 0; i < count; i++ {
go func(i int) {
defer wg.Done()
data, err := getData("key", i)
if err != nil {
return
}
fmt.Printf("worker %v: %v\n", i, data)
}(i)
}
wg.Wait()
}
$ go run main.go
worker 2: get data from database...
worker 2: db data success
worker 1: db data success
worker 0: db data success
请求去重
SingleFlight除了应用于缓存防击穿之外,还可以用于请求资源去重复。
package main
import (
"example/core"
"fmt"
"time"
)
var (
sf core.SingleFlight
chs []chan int
count = 3
)
//process 验证并发重复请求
func process(ch chan int, key string, index int) {
for i := 0; i < count; i++ {
val, err := sf.Do(key, func() (interface{}, error) {
time.Sleep(time.Millisecond * 100)
return "ok", nil
})
fmt.Printf("index=%v, i=%v, val=%v, err=%v, ch=%v\n", index, i, val, err, ch)
got := fmt.Sprintf("%v %T", val, val)
want := "ok string"
if got != want {
fmt.Printf("%v, %v\n", got, want)
return
}
if err != nil {
panic(err)
}
}
ch <- 1
}
func main() {
key := "key"
chs = make([]chan int, count)
for i := 0; i < count; i++ {
ch := make(chan int)
chs[i] = ch
go process(ch, key, i)
}
for i, ch := range chs {
<-ch
fmt.Printf("goroutine %v quit\n", i)
}
}
有疑问加站长微信联系(非本文作者)