电商评价系统:(二)singleflight 解决缓存击穿

58 阅读2分钟

我们这次是用redis给es做缓存,但是缓存还是会面临经典的几个问题——穿透、雪崩、击穿、缓存更新策略等等。

这里采用了go的singleflight库来解决缓存击穿的问题。

1. 浅层

1.1 什么是缓存击穿

热数据失效。

个人认为,记忆这种东西,提示词(就好像那个索引),越短越好。其实面试的时候,只要想起来了缓存击穿是什么,就可以扩展出来说很多。

1.2 singleflight 能干什么呢

SingleFlight 可以将对同一条数据的并发请求进行合并,只允许一个请求访问数据库中的数据,这个请求得到的数据结果与其他请求共享。

——— 来自《亿级流量系统架构设计与实战》

具体可以看这个demo:

package main

import (
	"log"
	"sync"

	"golang.org/x/sync/singleflight"
)

var g singleflight.Group

// 模拟数据库查询请求
func getDataFromDBV1(key string) (string, error) {
	data, err, _ := g.Do(key, func() (interface{}, error) {
		log.Printf("get data from db, key: %s", key)
		return "hello singleflight", nil
	})
	if err != nil {
		return "", err
	}
	return data.(string), nil
}

func getDataFromDBV2(key string) (string, error) {
	log.Printf("get data from db, key: %s", key)
	return "hello singleflight", nil
}

// 并发数=10
func main() {
	var wg sync.WaitGroup
	wg.Add(10)
	for i := 0; i < 10; i++ {
		go func() {
			defer wg.Done()
			data, err := getDataFromDBV1("fake-key")
			if err != nil {
				return
			}
			log.Printf("get data success, data: %s", data)
		}()
	}
	wg.Wait()
}

得到的结果是,只查询了数据库一次,但每个请求都获得了结果,如图。

image.png

2. 读源码

singleflight 底层是如何实现这一功能的呢?

type call struct {
	wg sync.WaitGroup

	val interface{}
	err error

	dups  int
	chans []chan<- Result
}

type Group struct {
	mu sync.Mutex       // protects m
	m  map[string]*call // lazily initialized
}
func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
	g.mu.Lock()
	if g.m == nil {
		g.m = make(map[string]*call)
	}
	if c, ok := g.m[key]; ok {
		c.dups++
		g.mu.Unlock()
		c.wg.Wait()

		return c.val, c.err, true
	}
	c := new(call)
	c.wg.Add(1)
	g.m[key] = c
	g.mu.Unlock()

	g.doCall(c, key, fn)
	return c.val, c.err, c.dups > 0
}
func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
    defer func() {
        g.mu.Lock()
        defer g.mu.Unlock()
        c.wg.Done()
        if g.m[key] == c {
                delete(g.m, key)
        }
    }()
    c.val, c.err = fn()
}

SingleFlight 主要是利用了sync.WaitGroup, 一个请求调用WaitGroup.Add(1),然后真正执行查询函数,其他请求看到 map[key] 已经存在,则执行WaitGroup.wait() 等待结果。真正执行查询的请求获取到结果后,更新c.val, 并调用WaitGroup.Done(),让计数器减1,此时正在等待结果的其他请求就会被放行,自然就能看到更新后的结果了。

这张图来自《亿级流量系统架构理论与实战》 image.png

3. 应用到评论系统