什么是预读失效,什么是缓存污染?
预读失效和缓存污染就是我们添加的缓存,并没有大量被访问,并不是热点数据,也就是说把冷数据放到缓存中来做预读,那么就会失效去db中查,没有提升性能;
LRU和LRU-K的区别是什么?
以及为什么用LRU-k 算法,怎么解决缓存污染和预读失效的?
因为普通的LRU算法并不能准确将热点数据选出到缓存,所以采用了LRU-k策略,并且用了冷热双链的策略,首先添加冷缓存,并且设置访问k次后才能提到热缓存中,也就是避免了短期偶尔访问的数据加入,这样就能将长期来看频繁访问的数据加入到热缓存,这样预读就不会失效;
LRU怎么实现的?LRU-K怎么实现的?,为什么用双向链表?
LRU-K代码:
package LRU_K
import (
"container/list"
"fmt"
)
type CacheItem struct {
Key string
Value interface{}
Freq int
ListElement *list.Element
}
type LRUCache struct {
capacity int
K int
cache map[string]*CacheItem
coldList *list.List
hotList *list.List
coldCap int
hotCap int
}
func (LRUCache) NewLRUCache(capacity int, K int, hotCap int) *LRUCache {
return &LRUCache{
capacity: capacity,
K: K,
cache: make(map[string]*CacheItem),
coldList: list.New(),
hotList: list.New(),
coldCap: capacity - hotCap, // 冷缓存的容量
hotCap: hotCap, // 热缓存的容量
}
}
func (c *LRUCache) Get(key string) (interface{}, bool) {
if item, ok := c.cache[key]; ok {
item.Freq++
if item.Freq == c.K {
// 移动到热缓存
c.coldList.Remove(item.ListElement)
item.ListElement = c.hotList.PushFront(item)
if c.hotList.Len() > c.hotCap {
c.evictHot()
}
} else {
// 在冷缓存中移动到头部
c.coldList.MoveToFront(item.ListElement)
}
return item.Value, true
}
return nil, false
}
func (c *LRUCache) Put(key string, value interface{}) {
if item, ok := c.cache[key]; ok {
item.Value = value
item.Freq++
if item.Freq == c.K {
// 移动到热缓存
c.coldList.Remove(item.ListElement)
item.ListElement = c.hotList.PushFront(item)
if c.hotList.Len() > c.hotCap {
c.evictHot()
}
} else {
// 在冷缓存中移动到头部
c.coldList.MoveToFront(item.ListElement)
if c.coldList.Len() > c.coldCap {
c.evictCold()
}
}
} else {
item := &CacheItem{Key: key, Value: value, Freq: 1}
c.cache[key] = item
if c.coldList.Len() >= c.coldCap {
c.evictCold()
}
item.ListElement = c.coldList.PushBack(item)
if len(c.cache) > c.capacity {
c.evictHot()
}
}
}
func (c *LRUCache) evictCold() {
if c.coldList.Len() > 0 {
item := c.coldList.Back().Value.(*CacheItem)
c.coldList.Remove(c.coldList.Back())
delete(c.cache, item.Key)
}
}
func (c *LRUCache) evictHot() {
if c.hotList.Len() > 0 {
item := c.hotList.Back().Value.(*CacheItem)
c.hotList.Remove(c.hotList.Back())
delete(c.cache, item.Key)
// 将被淘汰的热缓存项移动到冷缓存头部
item.ListElement = c.coldList.PushFront(item)
}
}
func (c *LRUCache) PrintCache() {
fmt.Printf("Cache Status (Total Capacity: %d, Hot Capacity: %d, Cold Capacity: %d)\n", c.capacity, c.hotCap, c.coldCap)
fmt.Println("Cold List:")
for e := c.coldList.Front(); e != nil; e = e.Next() {
item := e.Value.(*CacheItem)
fmt.Printf(" Key: %s, Value: %v, Freq: %d\n", item.Key, item.Value, item.Freq)
}
fmt.Println("Hot List:")
for e := c.hotList.Front(); e != nil; e = e.Next() {
item := e.Value.(*CacheItem)
fmt.Printf(" Key: %s, Value: %v, Freq: %d\n", item.Key, item.Value, item.Freq)
}
}
/*
package main
import (
"container/list"
"fmt"
)
type CacheItem struct {
Key string
Value interface{}
}
type LRUCache struct {
capacity int
cache map[string]*list.Element
list *list.List
}
func NewLRUCache(capacity int) *LRUCache {
return &LRUCache{
capacity: capacity,
cache: make(map[string]*list.Element),
list: list.New(),
}
}
func (c *LRUCache) Get(key string) (interface{}, bool) {
if elem, ok := c.cache[key]; ok {
c.list.MoveToFront(elem)
return elem.Value.(*CacheItem).Value, true
}
return nil, false
}
func (c *LRUCache) Put(key string, value interface{}) {
if elem, ok := c.cache[key]; ok {
// Update the value and move to front
c.list.MoveToFront(elem)
elem.Value.(*CacheItem).Value = value
return
}
if len(c.cache) == c.capacity {
// Remove the oldest item
oldest := c.list.Back()
c.list.Remove(oldest)
delete(c.cache, oldest.Value.(*CacheItem).Key)
}
// Add new item
c.cache[key] = c.list.PushFront(&CacheItem{Key: key, Value: value})
}
func (c *LRUCache) PrintCache() {
for e := c.list.Front(); e != nil; e = e.Next() {
item := e.Value.(*CacheItem)
fmt.Printf("Key: %s, Value: %v\n", item.Key, item.Value)
}
}
func main() {
cache := NewLRUCache(3)
cache.Put("key1", "value1")
cache.Put("key2", "value2")
cache.Put("key3", "value3")
cache.Get("key1")
cache.Put("key4", "value4") // Evicts key2
cache.PrintCache()
}
LRU-K 算法的优点
更精确地反映访问模式:LRU-K 算法通过跟踪最近 K 次的访问记录来判断一个缓存项的热度,这比传统的 LRU 算法(只考虑最后一次访问)更准确地反映了缓存项的访问模式。
减少页面置换:在 LRU-K 中,一个缓存项需要在 K 次访问后才会被考虑移动到热缓存,这减少了因频繁访问而导致的页面置换,从而提高了缓存的效率。
更好的性能:对于那些访问模式不是完全局部性的应用程序,LRU-K 可以提供比 LRU 更好的性能。它允许缓存系统识别并保留那些虽然不是最热门,但是仍然频繁访问的数据。
适应性:LRU-K 通过参数 K 的设置,可以适应不同的访问模式。例如,对于有明显工作集的应用程序,可以设置较小的 K 值;而对于访问模式更加随机的应用程序,可以设置较大的 K 值。
减少缓存污染:在分布式缓存系统中,LRU-K 可以减少缓存污染,因为它可以更有效地识别并保留那些真正需要被缓存的数据。
总的来说,LRU-K 算法提供了一种更灵活和更精确的方式来管理缓存,特别是在面对复杂和不可预测的访问模式时。
*/
LRU-k也是通过双向链表和哈希来实现的,就是每次冷缓存访问时访问次数加一然后判断是否达到进入热缓存的条件,如果是,那么就移动到热缓存头部,淘汰掉热缓存尾部到冷缓存头部。
拓展:
LFU(Least Frequently Used)缓存算法
LFU 是一种缓存替换算法,它基于对象的使用频率进行淘汰。该算法的核心思想是:最少使用频率的元素最有可能在未来不被访问,因此应该优先被移出缓存。具体流程如下:
-
计数器:每个缓存对象都有一个访问计数器,用来记录该对象被访问的次数。
-
淘汰策略:当缓存空间不足时,LFU 会移除使用频率最低的对象。如果有多个对象使用频率相同,则可以通过比较它们的最近访问时间来进一步决定淘汰哪一个。
优点:
- 能有效地保留使用频率高的元素,适用于热点数据不经常变化的场景。
缺点:
- 如果某个元素在过去被频繁访问但随后长时间不被访问,它的计数器仍然保持较高的值,可能不及时被淘汰。可以通过衰减技术来缓解这个问题。