7天用Go从零实现分布式缓存GeeCache(改进)(未完待续)

lru包

好的,我来为您完整地解说这段代码,指出其中的问题并给出改进方案。

代码分析:

您提供的 Add 方法用于将一个键值对添加到缓存中,或者更新已有的键值对。代码如下:

go 复制代码
// Add 将一个值添加到缓存中。
func (c *Cache) Add(key string, value Value) {
    if ele, ok := c.cache[key]; ok {
        c.ll.MoveToFront(ele)
        kv := ele.Value.(*entry)
        c.nbytes += int64(value.Len()) - int64(kv.value.Len())
        kv.value = value
    } else {
        ele := c.ll.PushFront(&entry{key, value})
        c.cache[key] = ele
        c.nbytes += int64(len(key)) + int64(value.Len())
    }
    for c.maxBytes != 0 && c.maxBytes < c.nbytes {
        c.RemoveOldest()
    }
}

存在的问题:

  1. 无限循环的风险:

    • 当添加或更新的缓存项非常大,其大小超过了 maxBytes,并且此时缓存为空(c.ll.Len() == 0)时,RemoveOldest() 方法无法移除任何项,因为缓存中没有项可移除。
    • 此时,c.nbytes 始终大于 c.maxBytes,循环条件 c.maxBytes != 0 && c.maxBytes < c.nbytes 一直为真,导致陷入无限循环。
  2. 更新操作可能导致缓存超限:

    • 当更新已有的缓存项时,如果新值比旧值大,可能会导致 c.nbytes 超过 maxBytes,需要移除旧的缓存项来腾出空间。

改进方案:

为了解决上述问题,需要在循环条件中增加对缓存长度的判断,避免在缓存为空时进入无限循环。修改后的循环条件如下:

go 复制代码
for c.maxBytes != 0 && c.nbytes > c.maxBytes && c.ll.Len() > 0 {
    c.RemoveOldest()
}

改进后的代码:

go 复制代码
// Add 将一个值添加到缓存中。
func (c *Cache) Add(key string, value Value) {
    if ele, ok := c.cache[key]; ok {
        // 更新已有的缓存项
        c.ll.MoveToFront(ele)
        kv := ele.Value.(*entry)
        delta := int64(value.Len()) - int64(kv.value.Len())
        c.nbytes += delta
        kv.value = value
    } else {
        // 添加新的缓存项
        ele := c.ll.PushFront(&entry{key, value})
        c.cache[key] = ele
        c.nbytes += int64(len(key)) + int64(value.Len())
    }
    // 检查并移除超出大小限制的缓存项
    for c.maxBytes != 0 && c.nbytes > c.maxBytes && c.ll.Len() > 0 {
        c.RemoveOldest()
    }
}

详细解说:

  1. 循环条件的修改:

    • 增加 c.ll.Len() > 0 判断: 防止在缓存为空时进入无限循环。当缓存中没有任何项可供移除时,循环会终止,避免程序卡死。

    • 调整比较符号为 >c.maxBytes < c.nbytes 修改为 c.nbytes > c.maxBytes,使逻辑更直观,表示当前缓存大小超过了最大限制。

  2. 更新操作的处理:

    • 正确计算缓存大小的变化: 在更新已有缓存项时,使用临时变量 delta 计算新旧值大小的差异,并更新 c.nbytes。这有助于避免计算错误,提高代码可读性。

    • 可能需要移除项: 如果更新后的缓存大小超过了 maxBytes,上述循环会自动移除最久未使用的缓存项,直到缓存大小符合限制。

  3. 添加新项的处理:

    • 在添加新项时,计算并增加 c.nbytes,包括键和值的大小。

额外建议:

  • 处理单个缓存项超过最大限制的情况:

    • 如果添加的单个缓存项大小就已经超过了 maxBytes,可以选择:

      • 拒绝添加该项: 直接返回,不将其添加到缓存中。

      • 特殊处理: 临时允许缓存大小超过限制,或者调整 maxBytes

  • 线程安全性:

    • 如果缓存会被多个 goroutine 同时访问,建议使用 sync.Mutexsync.RWMutex 来保护对缓存的读写操作,确保线程安全。
  • 完善 RemoveOldest() 方法:

    • 确保 RemoveOldest() 方法在缓存为空时能够安全地处理,不会导致程序崩溃。
  • 日志和监控:

    • 在移除缓存项或遇到异常情况时,记录日志,方便调试和监控缓存的运行状态。
相关推荐
初次攀爬者3 天前
ZooKeeper 实现分布式锁的两种方式
分布式·后端·zookeeper
花酒锄作田4 天前
Gin 框架中的规范响应格式设计与实现
golang·gin
知我Deja_Vu4 天前
redisCommonHelper.generateCode(“GROUP“),Redis 生成码方法
数据库·redis·缓存
断手当码农4 天前
Redis 实现分布式锁的三种方式
数据库·redis·分布式
没有bug.的程序员5 天前
电商秒杀系统深度进阶:高并发流量建模、库存零超卖内核与 Redis+MQ 闭环
数据库·redis·缓存·高并发·电商秒杀·流量建模·库存零超卖
troublea5 天前
ThinkPHP3.x高效学习指南
mysql·nginx·缓存
qwfys2005 天前
How to install golang 1.26.0 to Ubuntu 24.04
ubuntu·golang·install
初次攀爬者5 天前
Redis分布式锁实现的三种方式-基于setnx,lua脚本和Redisson
redis·分布式·后端
troublea5 天前
ThinkPHP6快速入门指南
数据库·mysql·缓存
业精于勤_荒于稀5 天前
物流订单系统99.99%可用性全链路容灾体系落地操作手册
分布式