Cache2go源码分析

cache.go

cache2go 的入口文件是 cache.go 文件,我们看到,在程序的一开始就定义了一个键为 string 值为 *CacheTable 的全局 cache 对象,同时,定义了一个全局的读写锁对象,具体代码如下:

var ( cache = make(map[string]*CacheTable) mutex sync.RWMutex )

所有要被缓存的 CacheTable 最终都会存放到这个全局的 cache 对象里,mutex 这个读写锁用于在向这个全局的 cache 对象读写数据时加锁以保证线程安全。

接下来,我们看 Cache 函数,该函数接受一个 CacheTable 的名字,返回一个 CacheTable 对象,如果名字已经存在,则直接返回存在的对象,如果名字不存在,则创建一个新的对象并返回,具体代码如下:

// Cache returns the existing cache table with given name or creates a new one // if the table does not exist yet. func Cache(table string) *CacheTable { mutex.RLock() t, ok := cache[table] mutex.RUnlock() if !ok { mutex.Lock() t, ok = cache[table] // Double check whether the table exists or not. if !ok { t = &CacheTable{ name: table, items: make(map[interface{}]*CacheItem), } cache[table] = t } mutex.Unlock() } return t }

在 Cache 函数里,我们首先,使用全局的读写锁对全局的 cache 对象进行读写加锁,然后看看当前传入的 table 名字是否已经存在,我们看到,如果存在了,则直接返回了获取到的 CacheTable 对象。

否则,如果名字不存在,此时再次使用互斥锁进行加锁,再次进行获取一次,查看此时名字是否存在,如果此时还是不存在,则以当前表名,创建一个新的 CacheTable 对象,并以表名为键添加到 map 中。

这里有几个点,需要我们学习下,我们可以看到,这里使用的两次锁并不相同,第一次使用的是读写锁,第二次使用的是互斥锁,第一次使用读写锁,因为此时只是访问数据,并没有写入数据,这里使用读写锁可以提高并发性,而第二次必须使用互斥锁,是因为第二次是对全局的 cache 对象进行了写入,因此必须使用互斥锁。

还有一点,就是在写入数据之前,再次进行判断,表名是否存在,如果这里少了这次判断,很可能会出现并发的 Bug,因为,有可能再第一次判断键不存在时,走到了下面的 if 判断,但就在这个间隙时间里,这个 cache 全局对象被写入了一个同样的表名,这时候就出现了并发问题,因此,必须再写入之前,再次判断。

Cache2go源码分析总结

从 cache.go 源码中,我们可以学习到,对全局对象操作一定要加锁,如果我们仅仅是对全局对象进行读取操作,而没有写入操作,那我们可以使用读写锁,加大并发性。

同时,在写入全局对象之前,我们一定要加互斥锁,并且,写入之前,加锁逻辑里面,一定要再次校验数据的合法性,否则,可能会导致并发的问题。