防止缓存击穿

什么是缓存雪崩、击穿、穿透?

缓存雪崩

大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。

缓存雪崩

缓存击穿

如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。

缓存击穿

缓存穿透

当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。

缓存穿透

使用 singleflight 防止缓存击穿

  1. 定义请求对象

    1
    2
    3
    4
    5
    type call struct {
    wg sync.WaitGroup // 控制线程是否等待
    val interface{} // 请求返回结果
    err error // 错误信息
    }
  2. 存储请求对象

    1
    2
    3
    4
    5
    // Group 存储请求对象
    type Group struct {
    mu sync.Mutex // 线程安全必须上锁
    m map[string]*call // server 端需要记录一下这次请求
    }
  3. 重新定义访问缓存的方法

    • 查询是否有相同的请求在查询缓存
    • 当前有请求就要排队,排队完, 拿到前面处理完成的请求结果就返回
    • 创建一个请求对象,如果其他请求来了就先让其等等,自己请求完成再给他放行
    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 以二进制方式存储,占用空间小。

  1. 定义 protpbuf 文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    syntax="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);
    }

  2. 重新定义原有的方法

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

完结 🎉