6-防止缓存击穿与protobuf通信(Go实现分布式缓存)
防止缓存击穿
什么是缓存雪崩、击穿、穿透?
缓存雪崩
当大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。
缓存击穿
如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。
缓存穿透
当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。
使用 singleflight 防止缓存击穿
定义请求对象
1
2
3
4
5type call struct {
wg sync.WaitGroup // 控制线程是否等待
val interface{} // 请求返回结果
err error // 错误信息
}存储请求对象
1
2
3
4
5// Group 存储请求对象
type Group struct {
mu sync.Mutex // 线程安全必须上锁
m map[string]*call // server 端需要记录一下这次请求
}重新定义访问缓存的方法
- 查询是否有相同的请求在查询缓存
- 当前有请求就要排队,排队完, 拿到前面处理完成的请求结果就返回
- 创建一个请求对象,如果其他请求来了就先让其等等,自己请求完成再给他放行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
if c, ok := g.m[key]; ok {
c.wg.Wait() // 如果请求正在进行中,则等待
return c.val, c.err // 请求结束,返回结果
}
//创建一个请求对象
c := new(call)
// 发起请求前加锁
c.wg.Add(1)
// 添加到 g.m,表明 key 已经有对应的请求在处理
g.m[key] = c
c.val, c.err = fn() // 调用 fn,发起请求
c.wg.Done() // 请求结束
delete(g.m, key) // 更新 g.m
return c.val, c.err // 返回结果
}
使用 protobuf 进行节点间通信
把原先的 http 换成了 rpc, 目的就是提升性能
protobuf 即 Protocol Buffers,Google 开发的一种数据描述语言,是一种轻便高效的结构化数据存储格式,与语言、平台无关,可扩展可序列化。protobuf 以二进制方式存储,占用空间小。
定义 protpbuf 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18syntax="proto3";
package geecachepb;
option go_package = "./"; // 指定生成的go文件所在path
message Request {
string group = 1;
string key = 2;
}
message Response {
bytes value = 1;
}
service GroupCache {
rpc Get(Request) returns (Response);
}重新定义原有的方法
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
44type PeerGetter interface {
Get(in *pb.Request, out *pb.Response) error
}
func (g *Group) getFromPeer(peer PeerGetter, key string) (ByteView, error) {
req := &pb.Request{
Group: g.name,
Key: key,
}
res := &pb.Response{}
err := peer.Get(req, res)
if err != nil {
return ByteView{}, err
}
return ByteView{b: res.Value}, nil
}
func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// ...
// Write the value to the response body as a proto message.
body, err := proto.Marshal(&pb.Response{Value: view.ByteSlice()})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Write(body)
}
func (h *httpGetter) Get(in *pb.Request, out *pb.Response) error {
u := fmt.Sprintf(
"%v%v/%v",
h.baseURL,
url.QueryEscape(in.GetGroup()),
url.QueryEscape(in.GetKey()),
)
res, err := http.Get(u)
// ...
if err = proto.Unmarshal(bytes, out); err != nil {
return fmt.Errorf("decoding response body: %v", err)
}
return nil
}
完结 🎉
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 贾小白博客!