面试题答案
一键面试可能导致命中率下降的原因
- 缓存数据结构不合理:如果数据结构设计不当,例如使用简单的数组存储缓存,查找效率为 O(n),会导致查找缓存时耗费过多时间,降低命中率。
- 缓存淘汰策略不佳:若采用不合适的淘汰策略,比如先进先出(FIFO),可能会将仍频繁使用的数据淘汰掉,而最近最少使用(LRU)算法在某些场景下也可能因误判而淘汰即将被再次使用的数据。
- 分布式缓存一致性问题:在分布式环境中,不同节点的缓存数据同步不及时,导致部分节点获取到过期数据,从而降低命中率。
缓存数据结构优化
- 使用哈希表:Go 语言中的 map 是一种哈希表实现,具有 O(1) 的查找时间复杂度。
type Cache struct {
data map[string]interface{}
}
func NewCache() *Cache {
return &Cache{
data: make(map[string]interface{}),
}
}
func (c *Cache) Get(key string) (interface{}, bool) {
value, exists := c.data[key]
return value, exists
}
func (c *Cache) Set(key string, value interface{}) {
c.data[key] = value
}
- 结合链表实现 LRU 缓存:可以使用双向链表和哈希表结合的方式实现 LRU 缓存,在哈希表中存储键值对,同时链表记录数据的访问顺序。
type LRUCache struct {
capacity int
cache map[string]*DLinkedNode
head *DLinkedNode
tail *DLinkedNode
}
type DLinkedNode struct {
key string
value interface{}
prev *DLinkedNode
next *DLinkedNode
}
func NewLRUCache(capacity int) *LRUCache {
head := &DLinkedNode{}
tail := &DLinkedNode{}
head.next = tail
tail.prev = head
return &LRUCache{
capacity: capacity,
cache: make(map[string]*DLinkedNode),
head: head,
tail: tail,
}
}
func (c *LRUCache) MoveToHead(node *DLinkedNode) {
c.removeNode(node)
c.addToHead(node)
}
func (c *LRUCache) removeNode(node *DLinkedNode) {
node.prev.next = node.next
node.next.prev = node.prev
}
func (c *LRUCache) addToHead(node *DLinkedNode) {
node.next = c.head.next
node.prev = c.head
c.head.next.prev = node
c.head.next = node
}
func (c *LRUCache) removeTail() *DLinkedNode {
node := c.tail.prev
c.removeNode(node)
return node
}
func (c *LRUCache) Get(key string) (interface{}, bool) {
if node, exists := c.cache[key]; exists {
c.MoveToHead(node)
return node.value, true
}
return nil, false
}
func (c *LRUCache) Put(key string, value interface{}) {
if node, exists := c.cache[key]; exists {
node.value = value
c.MoveToHead(node)
} else {
newNode := &DLinkedNode{key: key, value: value}
c.cache[key] = newNode
c.addToHead(newNode)
if len(c.cache) > c.capacity {
removed := c.removeTail()
delete(c.cache, removed.key)
}
}
}
缓存淘汰策略调整
- 调整为最不经常使用(LFU)策略:记录每个数据项的访问频率,淘汰访问频率最低的数据。可以使用两个哈希表,一个记录键值对,另一个记录键与访问频率的映射,同时使用一个频率堆来管理不同频率的数据。
type LFUNode struct {
key string
value interface{}
freq int
next *LFUNode
prev *LFUNode
}
type LFUCounter struct {
head *LFUNode
tail *LFUNode
}
func (l *LFUCounter) Add(node *LFUNode) {
if l.head == nil {
l.head = node
l.tail = node
} else {
node.prev = l.tail
l.tail.next = node
l.tail = node
}
}
func (l *LFUCounter) Remove(node *LFUNode) {
if node.prev == nil {
l.head = node.next
} else {
node.prev.next = node.next
}
if node.next == nil {
l.tail = node.prev
} else {
node.next.prev = node.prev
}
}
type LFUCacher struct {
capacity int
size int
cache map[string]*LFUNode
freqTable map[int]*LFUCounter
minFreq int
}
func NewLFUCacher(capacity int) *LFUCacher {
return &LFUCacher{
capacity: capacity,
cache: make(map[string]*LFUNode),
freqTable: make(map[int]*LFUCounter),
}
}
func (l *LFUCacher) Get(key string) (interface{}, bool) {
if node, exists := l.cache[key]; exists {
l.increaseFreq(node)
return node.value, true
}
return nil, false
}
func (l *LFUCacher) Put(key string, value interface{}) {
if l.capacity == 0 {
return
}
if node, exists := l.cache[key]; exists {
node.value = value
l.increaseFreq(node)
} else {
newNode := &LFUNode{key: key, value: value, freq: 1}
l.cache[key] = newNode
if _, exists := l.freqTable[1];!exists {
l.freqTable[1] = &LFUCounter{}
}
l.freqTable[1].Add(newNode)
l.size++
l.minFreq = 1
if l.size > l.capacity {
l.removeMinFreq()
}
}
}
func (l *LFUCacher) increaseFreq(node *LFUNode) {
oldFreq := node.freq
l.freqTable[oldFreq].Remove(node)
if oldFreq == l.minFreq && l.freqTable[oldFreq].head == nil {
l.minFreq++
}
node.freq++
if _, exists := l.freqTable[node.freq];!exists {
l.freqTable[node.freq] = &LFUCounter{}
}
l.freqTable[node.freq].Add(node)
}
func (l *LFUCacher) removeMinFreq() {
counter := l.freqTable[l.minFreq]
removed := counter.head
counter.Remove(removed)
delete(l.cache, removed.key)
l.size--
}
分布式缓存一致性
- 使用分布式哈希表(DHT):如一致性哈希算法,将缓存节点映射到一个环形哈希空间上,数据根据其哈希值分布在环上的节点中。当节点增加或减少时,只有部分数据需要迁移。
package main
import (
"crypto/sha1"
"fmt"
"sort"
"strconv"
)
type Node struct {
id int
label string
}
type HashRing struct {
nodes map[uint32]Node
sorted []uint32
replicas int
}
func NewHashRing(replicas int) *HashRing {
return &HashRing{
nodes: make(map[uint32]Node),
replicas: replicas,
}
}
func (h *HashRing) AddNode(node Node) {
for i := 0; i < h.replicas; i++ {
key := fmt.Sprintf("%s-%d", node.label, i)
hash := h.hash(key)
h.nodes[hash] = node
h.sorted = append(h.sorted, hash)
}
sort.Slice(h.sorted, func(i, j int) bool {
return h.sorted[i] < h.sorted[j]
})
}
func (h *HashRing) GetNode(key string) Node {
hash := h.hash(key)
index := sort.Search(len(h.sorted), func(i int) bool {
return h.sorted[i] >= hash
})
if index == len(h.sorted) {
index = 0
}
return h.nodes[h.sorted[index]]
}
func (h *HashRing) hash(s string) uint32 {
hasher := sha1.New()
hasher.Write([]byte(s))
hashBytes := hasher.Sum(nil)
return uint32(hashBytes[0]) | uint32(hashBytes[1])<<8 | uint32(hashBytes[2])<<16 | uint32(hashBytes[3])<<24
}
- 使用分布式缓存系统如 Redis:Redis 提供了发布/订阅机制来处理缓存一致性问题。当一个节点更新缓存时,通过发布消息通知其他节点更新或删除相应缓存数据。
package main
import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
)
var ctx = context.Background()
func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})
// 订阅频道
pubsub := rdb.Subscribe(ctx, "cache-updates")
defer pubsub.Close()
go func() {
for {
msg, err := pubsub.ReceiveMessage(ctx)
if err != nil {
fmt.Println("ReceiveMessage error:", err)
return
}
// 接收到消息后处理缓存更新或删除
fmt.Println("Received message:", msg.Payload)
}
}()
// 发布缓存更新消息
err := rdb.Publish(ctx, "cache-updates", "key1-updated").Err()
if err != nil {
fmt.Println("Publish error:", err)
return
}
}
通过以上从缓存数据结构优化、缓存淘汰策略调整、以及分布式缓存一致性等方面的优化,可以有效提升缓存命中率,从而提高系统性能。