高并发之-缓存击穿的解决方案

缓存击穿

学生在周末一起上编程课时,发现MySQL突然压力变大,项目中读的地方都加有Cache,按道理大部分数据都会被挡在Cache层,不会打到数据库,于是怀疑应该缓存击穿问题,查看编程游戏每关通用配置文件代码,果然发现问题。有问题,马上改进是我党一惯作风……

先说一下什么缓存击穿,缓存击穿是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。

解决方案:

1、方案一:通过锁,实现只有一个请求负责缓存更新,其他请求等待

2、方案二:由专门的定时脚本在缓存失效前对其进行更新

3、方案三:比较粗暴,设置缓存永不过期

由于这个API使用的是Golang写的,而Golang有一很好的组件来来处理缓存击穿问题:Golang singleflight源码 它能够做到对同一个失效key的多个请求,只有一个请求执行对key的更新操作,当然你也可以使用redis的 redlock来实现,以下是方案一的代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
/**
* @Author: entere
* @Description: 用singleflight解决缓存击穿问题
* @File: cachebreak_test.go
* @Version: 1.0.0
*/


package cachebreak

import (
"fmt"
"github.com/golang/groupcache/singleflight"
"sync"
"testing"
"time"
)

var (
mu sync.RWMutex
once sync.Once
wg sync.WaitGroup
cacheMap map[string]string // cacheMap简单模拟cache数据库
singleFlight = new(singleflight.Group)
singleFlightKey string
)

func init() {
cacheMap = make(map[string]string)
// cacheMap["name"] = "men"
singleFlightKey = "single_flight_key"
}

func TestCacheBreak(t *testing.T) {

// 模拟10个并发访问 CacheBreakImpl() 测试缓存击穿
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
CacheBreakImpl()
wg.Done()
}()
}
wg.Wait()

//fmt.Print(cacheMap)

}

func CacheBreakImpl() {

key := "name" // 缓存数据库key
cacheData, err := GetDataFromCache(key)
if err != nil {
fmt.Printf("Get Data From Cache err: %v", err)
}
// 如果缓存失效
if cacheData == "" {
// 去数据枯查询 并放到cache中
//dbData, err := GetDataFromDB(id)
//SetDataToCache(dbData)

_, err := singleFlight.Do(singleFlightKey, func() (i interface{}, err error) {

dbData, err := GetDataFromDB(key)
SetDataToCache(key, "db to cache:"+key)
return dbData, err

})
if err != nil {
fmt.Printf("Get Data From DB err: %v", err)
}

}

}

// 模拟从DB中获取数据
func GetDataFromDB(key string) (value string, err error) {
time.Sleep(time.Second * 1)
fmt.Print("get data from db\n")
return "db data " + key, nil
}

// 模拟从Cache中获取数据
func GetDataFromCache(key string) (value string, err error) {
mu.RLock()
defer mu.RUnlock()
fmt.Print("get data from cache\n")
value = cacheMap[key]
return

}

// 模拟把数据入入cache
func SetDataToCache(key string, value string) {
// map 是非线程安全的
mu.Lock()
defer mu.Unlock()
cacheMap[key] = value
}

测试结果,我们看到,Cache失效时,10个并发请求,只有一个打到数据库中。结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
=== RUN   TestCacheBreak
get data from cache
get data from cache
get data from cache
get data from cache
get data from cache
get data from cache
get data from cache
get data from cache
get data from cache
get data from cache
get data from db
--- PASS: TestCacheBreak (1.00s)
PASS

以上是测试代码,生产环境把测试数据替换就行~