Golang实现分层LRU缓存 - 保障祖先完整性的go-lrutree库

Golang实现分层LRU缓存 - 保障祖先完整性的go-lrutree库 大家好,

我想分享一个我构建的 Go 库,名为 go-lrutree。它是一个小型、线程安全、泛型的缓存,专门为树形结构数据设计。

它解决的问题:

流行的 LRU 缓存实现(例如 hashicorp/golang-lru)对于扁平的键值对工作得很好。

但是当你处理层次结构数据时——比如组织架构图、文件路径、类别树或地理位置——扁平缓存可能就不够用了。

例如:如果你缓存了一个城市,你可能希望它的州和国家也保持缓存状态。但传统的 LRU 淘汰机制可能会淘汰父节点,而子节点却保留着,从而破坏了逻辑结构。

go-lrutree 通过强制执行以下规则来解决这个问题:如果一个节点在缓存中,那么它的所有祖先节点也必须在缓存中。当你访问一个节点时,它的整个祖先链都会被标记为最近使用——从而保持链的完整性和淘汰安全性。

使用示例:

package main

import (
	"fmt"

	"github.com/vasayxtx/go-lrutree"
)

type OrgItem struct {
	Name string
}

func main() {
	// 创建一个最大容量为4个条目并带有淘汰回调的新缓存。
	cache := lrutree.NewCache[string, OrgItem](4, lrutree.WithOnEvict(func(node lrutree.CacheNode[string, OrgItem]) {
		fmt.Printf("Evicted: %s (key=%s, parent=%s)\n", node.Value.Name, node.Key, node.ParentKey)
	}))

	// 向缓存添加节点。
	_ = cache.AddRoot("company", OrgItem{"My Company"})
	_ = cache.Add("engineering", OrgItem{"Engineering department"}, "company")
	_ = cache.Add("frontend", OrgItem{"Frontend team"}, "engineering")
	_ = cache.Add("backend", OrgItem{"Backend team"}, "engineering")

	// 通过键获取值。
	// "frontend" 节点及其所有祖先节点("engineering" 和 "company" 节点)被标记为最近使用。
	if cacheNode, ok := cache.Get("frontend"); ok {
		fmt.Printf("Get: %s (key=%s, parent=%s)\n", cacheNode.Value.Name, cacheNode.Key, cacheNode.ParentKey)
		// 输出: Get: Frontend team (key=frontend, parent=engineering)
	}

	// 获取从根节点到键为 "backend" 的节点的完整分支。
	// "backend"、"engineering" 和 "company" 节点被标记为最近使用。
	branch := cache.GetBranch("backend")
	for i, node := range branch {
		fmt.Printf("GetBranch[%d]: %s (key=%s, parent=%s)\n", i, node.Value.Name, node.Key, node.ParentKey)
	}
	// 输出:
	// GetBranch[0]: My Company (key=company, parent=)
	// GetBranch[1]: Engineering department (key=engineering, parent=company)
	// GetBranch[2]: Backend team (key=backend, parent=engineering)

	// 通过键窥视值,而不更新 LRU 顺序。
	if cacheNode, ok := cache.Peek("frontend"); ok {
		fmt.Printf("Peek: %s (key=%s, parent=%s)\n", cacheNode.Value.Name, cacheNode.Key, cacheNode.ParentKey)
		// 输出: Peek: Frontend team (key=frontend, parent=engineering)
	}

	// 添加一个新节点,超过了缓存的最大容量。
	// 最近最少使用的叶子节点("frontend")被淘汰。
	_ = cache.Add("architects", OrgItem{"Architects team"}, "engineering")
	// 输出: Evicted: Frontend team (key=frontend, parent=engineering)
}

期待您的反馈!

我很想听听 Go 社区的意见:

  • 这种层次化缓存的概念对您有启发吗?您能想象出它的使用场景吗?
  • 对 API 设计或实现方法有什么反馈吗?
  • 对改进或功能有什么建议吗?

感谢您的关注!


更多关于Golang实现分层LRU缓存 - 保障祖先完整性的go-lrutree库的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang实现分层LRU缓存 - 保障祖先完整性的go-lrutree库的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


这是一个非常棒的库,解决了传统LRU在处理层次化数据时的关键痛点。保障祖先节点的完整性对于文件系统、组织结构、分类目录等场景至关重要。以下是对你实现的一些技术分析和示例补充:

技术实现亮点:

  1. 祖先链完整性保障:通过强制要求节点祖先必须存在,避免了数据逻辑不一致
  2. 链式LRU更新:访问节点时自动更新整个祖先链的LRU状态,这是正确的设计选择
  3. 线程安全设计:适合并发场景下的树形数据缓存

示例代码扩展:

// 演示并发场景下的使用
func concurrentExample() {
    cache := lrutree.NewCache[string, *FileInfo](100, 
        lrutree.WithOnEvict(func(node lrutree.CacheNode[string, *FileInfo]) {
            fmt.Printf("Evicted file: %s\n", node.Value.Path)
        }))

    // 模拟多个goroutine并发访问文件路径
    var wg sync.WaitGroup
    paths := []string{"/usr/local/bin", "/usr/local/lib", "/var/log"}
    
    for _, path := range paths {
        wg.Add(1)
        go func(p string) {
            defer wg.Done()
            
            // 分解路径并确保所有父目录被缓存
            parts := strings.Split(p, "/")
            var parentKey string
            
            for i, part := range parts {
                if part == "" {
                    continue
                }
                key := strings.Join(parts[:i+1], "/")
                if i == 0 {
                    parentKey = ""
                }
                
                cache.Add(key, &FileInfo{
                    Path: key,
                    IsDir: true,
                }, parentKey)
                
                parentKey = key
            }
        }(path)
    }
    wg.Wait()
}

// 演示批量操作
func batchOperations() {
    cache := lrutree.NewCache[int, TreeNode](50)
    
    // 批量添加树节点
    nodes := []struct{
        key int
        value TreeNode
        parent int
    }{
        {1, TreeNode{Name: "Root"}, 0},
        {2, TreeNode{Name: "Child1"}, 1},
        {3, TreeNode{Name: "Child2"}, 1},
        {4, TreeNode{Name: "Grandchild1"}, 2},
    }
    
    for _, n := range nodes {
        if n.parent == 0 {
            cache.AddRoot(n.key, n.value)
        } else {
            cache.Add(n.key, n.value, n.parent)
        }
    }
    
    // 获取子树
    if subtree, ok := cache.GetSubtree(2); ok {
        for _, node := range subtree {
            fmt.Printf("Subtree node: %v\n", node.Value)
        }
    }
}

// 类型安全的包装器示例
type CategoryCache struct {
    cache *lrutree.Cache[string, Category]
}

func NewCategoryCache(capacity int) *CategoryCache {
    return &CategoryCache{
        cache: lrutree.NewCache[string, Category](capacity),
    }
}

func (cc *CategoryCache) AddCategory(id, name string, parentID string) error {
    if parentID == "" {
        return cc.cache.AddRoot(id, Category{
            ID:   id,
            Name: name,
        })
    }
    return cc.cache.Add(id, Category{
        ID:   id,
        Name: name,
    }, parentID)
}

func (cc *CategoryCache) GetCategoryChain(id string) []Category {
    branch := cc.cache.GetBranch(id)
    result := make([]Category, len(branch))
    for i, node := range branch {
        result[i] = node.Value
    }
    return result
}

性能考虑:

// 基准测试示例
func BenchmarkCacheOperations(b *testing.B) {
    cache := lrutree.NewCache[int, string](1000)
    
    // 构建深度树
    for i := 1; i <= 1000; i++ {
        parent := i / 2
        if parent == 0 {
            cache.AddRoot(i, fmt.Sprintf("Node%d", i))
        } else {
            cache.Add(i, fmt.Sprintf("Node%d", i), parent)
        }
    }
    
    b.ResetTimer()
    
    b.Run("GetLeafNode", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            cache.Get(999)
        }
    })
    
    b.Run("AddNewNode", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            cache.Add(1000+i, "NewNode", 500)
        }
    })
}

这个库的设计很好地解决了层次化数据缓存的特殊需求。GetBranch方法特别有用,可以一次性获取完整路径。线程安全的设计也使其适合生产环境使用。

回到顶部