第二个迭代周期的目标是保证缓存的并发安全,并封装出核心结构Group,Group可以理解为MySQL中的table。
另设置降级方法,如果缓存中没有值,应该调用什么方法查询,由使用者传入。比如降级查询MySQL数据库中的一张表,我们恰好可以将缓存的Group与此table对应。
第二个迭代周期完成后实现效果如下:
在这个过程中,加锁是十分必要的,如上图所示的MainCache结构,其add方法不加锁会有什么后果呢? 我们可以看下面这张图:
func (c *MainCache) Add(key string, value ByteView) {
c.mu.Lock()
//保证正常异常情况最终都能解锁
defer c.mu.Unlock()
if c.lru == nil {
c.lru = lru.New(c.maxBytes, nil)
}
c.lru.Add(key, value)
}
当一个共享变量被多个线程操作时,如果多个步骤间有影响,那么就可能由于时间篇的轮转带来线程安全问题。
那么为什么MainCache块上面加锁,Group块上也加锁了呢,加两层锁有必要吗?其实是有的。
// 新建Group,必须要有Getter
func NewGroup(name string, maxBytes int64, getter Getter) *Group {
if getter == nil {
//阻断,手动设置恐慌
panic("getter nil")
}
mu.Lock()
defer mu.Unlock()
g := &Group{
name: name,
getter: getter,
//内部lru没有new,用的时候没有会自动初始化,传入大小即可
mainCache: MainCache{maxBytes: maxBytes},
}
//锁保证了这两段代码的原子性
groups[name] = g
return g
}
// 读操作,读读不互斥,读写互斥,使用读锁即可
func GetGroup(name string) *Group {
mu.RLock()
g := groups[name]
mu.RUnlock()
return g
}
// 选中Group后,从中get缓存内容value的方法
func (g *Group) Get(key string) (ByteView, error) {
if key == "" {
return ByteView{}, fmt.Errorf("nil key")
}
//在MainCache中进行查询,查询对应的key
if v, ok := g.mainCache.Get(key); ok {
log.Println("[WindCache] hit")
return v, nil
}
//没命中,调用降级方法加载,并加载至缓存中
return g.load(key)
}
//...略过部分代码
// 加载到缓存中
func (g *Group) populateCache(key string, value ByteView) {
g.mainCache.Add(key, value)
}
从上面的代码不难看出,该方法内的写锁是为了保证创建新组和将新组加入映射这两个操作的原子性,使用组名查询组的时候,我们允许多个查询Group的读请求并发进行,但是比如,我们的多个请求很可能是要查询同一个key,这时如果数据恰好没有加载到缓存中,只是保存在数据库中,那么会调用降级方法去数据库中(举例)加载,并调用MainCache的add方法进行添加,所以下层的MainCache还是会面临并发问题,因此需要在MainCache层也加上锁。