最近面试遇到利用golang标准库创建cache的问题,之前一直用的第三方的库,例如由PatrickMN提供的开源库go-cache(github.com/patrickmn/go-cache),但是面试问到了还是得研究下的。
问题思路
cache无非就几样关键的:
- 数据标识(key)
- 数据(value)
- 过期时间(expire_time)
首先,定义我们的缓存数据,其内包含数据以及数据过期时间
type cacheItem struct {
value interface{}
expiration time.Time
}
虽然我们有了这些数据,但是还没有相关的标识去区分他们:
type SimpleCache struct {
mu sync.Mutex //互斥锁,保障数据安全性
values map[string]*cacheItem
}
然后再创建一个构建器:
func NewSimpleCache() *SimpleCache {
return &SimpleCache{
values: make(map[string]*cacheItem),
}
}
还有相关的读写方法:
//写方法,将数据存入缓存并设置过期时间
func (c *SimpleCache) Set(key string, value interface{}, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
c.values[key] = &cacheItem{
value: value,
expiration: time.Now().Add(ttl),
}
}
//度方法
func (c *SimpleCache) Get(key string) (interface{}, bool) {
c.mu.Lock()
defer c.mu.Unlock()
item, exists := c.values[key]
if !exists || item.expiration.Before(time.Now()) {
return nil, false
}
return item.value, true
}
代码到这里其实一个简单的cache就已经完成了,虽然完成了,但是还是有很大的弊端,比如说:
1.上述的互斥锁会阻塞所有试图访问缓存的goroutine,对于本身为了效率而生的cache来说是很不合格的,所以这里选择使用读写锁分离读写操作会更好些
2.添加缓存容量限制,并使用LRU(Least Recently Used)策略实现可以自动移除最长时间未被访问的缓存项,这对于容量有限的缓存尤其重要。
3.增加缓存命中率、未命中率、eviction次数等统计信息,便于监控和调优
相关的优化如下:
*新增缓存命中率、未命中率、eviction次数
type CacheStats struct {
Hits uint64
Misses uint64
Evicts uint64
}
*使用读写锁
*新增cache容量
*lruOrder切片,用来维护缓存项的LRU顺序,最近使用的项放在末尾
type SimpleCache struct {
mu sync.RWMutex
values map[string]*cacheItem
lruOrder []string
maxItems int
stats CacheStats
}
*更新后的构造器
func NewSimpleCache(maxItems int) *SimpleCache {
return &SimpleCache{
values: make(map[string]*cacheItem),
lruOrder: make([]string, 0, maxItems),
maxItems: maxItems,
}
}
*更新后的读取和写入
//加写锁保证线程安全。
//若键已存在,则先从LRU列表中移除。
//更新或插入新的cacheItem到values中,并将其添加到LRU列表的末尾。
//如果超出最大项数,触发evictLRU移除最老的缓存项。
func (c *SimpleCache) Set(key string, value interface{}, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
_, exists := c.values[key]
if exists {
c.removeLRUItem(key)
}
c.values[key] = &cacheItem{
value: value,
expiration: time.Now().Add(ttl),
}
c.addLRUItem(key)
if len(c.lruOrder) > c.maxItems {
c.evictLRU()
}
}
//加读锁保证并发安全。
//检查键是否存在且未过期,更新统计信息,并返回值。
//优化了读取逻辑,减少不必要的过期检查。
func (c *SimpleCache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
item, exists := c.values[key]
if !exists || item.expiration.Before(time.Now()) {
c.stats.Misses++
return nil, false
}
c.stats.Hits++
return item.value, true
}
func (c *SimpleCache) removeLRUItem(key string) {
for i, k := range c.lruOrder {
if k == key {
c.lruOrder = append(c.lruOrder[:i], c.lruOrder[i+1:]...)
break
}
}
}
*以下是LUR维护及满容量数据清理操作:
//removeLRUItem 和 addLRUItem: 分别用于从LRU列表中移除和添加缓存项的键。这两个辅助函数帮助维护缓存项的访问顺序
func (c *SimpleCache) removeLRUItem(key string) {
for i, k := range c.lruOrder {
if k == key {
c.lruOrder = append(c.lruOrder[:i], c.lruOrder[i+1:]...)
break
}
}
}
func (c *SimpleCache) addLRUItem(key string) {
c.lruOrder = append(c.lruOrder, key)
}
//evictLRU: 当缓存项数量超过限制时,移除最老的缓存项。这个方法会从values和lruOrder中移除最老的键,并更新统计信息
func (c *SimpleCache) evictLRU() {
keyToRemove := c.lruOrder[0]
delete(c.values, keyToRemove)
c.removeLRUItem(keyToRemove)
c.stats.Evicts++
}
*最后是状态统计
func (c *SimpleCache) Stats() CacheStats {
c.mu.RLock()
defer c.mu.RUnlock()
return c.stats
}
写了一个简单的测试用例:
func main() {
cache := NewSimpleCache(2)
cache.Set("key1", "Hello, World!", 2*time.Second)
cache.Set("key2", "Hello, Golang!", 2*time.Second)
fmt.Println(cache.Get("key1"))
fmt.Println(cache.Get("key2"))
time.Sleep(5 * time.Second)
fmt.Println(cache.Get("key1")) // 应该已被移除
fmt.Println(cache.Get("key2"))
fmt.Println("Cache Stats:", cache.Stats())
}
运行结果如下: