Go 实现一个支持多种过期、淘汰机制的本地缓存的核心原理

简介: 本文旨在探讨实现一个支持多种 过期、淘汰 机制的 go 本地缓存的核心原理,我将重点讲解如何支持多样化的过期和淘汰策略。

前言

相信大家对于缓存这个词都不陌生,但凡追求高性能的业务场景,一般都会使用缓存,它可以提高数据的检索速度,减少数据库的压力。

本文旨在探讨实现一个支持多种 过期、淘汰 机制的本地缓存的核心原理,我将重点讲解如何支持多样化的过期和淘汰策略。如果需要查看完整的代码设计,请前往以下链接:

https://github.com/chenmingyong0423/go-generics-cache

准备好了吗?准备一杯你最喜欢的咖啡或茶,随着本文一探究竟吧。

过期机制和淘汰机制的区别

如果分不清过期机制和淘汰机制的读者,可以先看看下面的描述:

1、过期机制:

  • 目的:确保数据的时效性和有效性。
  • 实现方式:通过设置固定过期时间或基于最后访问时间的过期机制,确保数据保持最新。

2、淘汰机制

  • 目的:控制缓存的大小,避免缓存占用过多的内存。
  • 实现方式:使用如 LRU(最近最少使用)或 LFU(最不经常使用)等策略,在达到最大容量时移除部分缓存项。

ICache 接口定义

type ICache[K comparable, V any] interface {
   

    Set(ctx context.Context, key K, value V) error

    Get(ctx context.Context, key K) (V, error)

    Delete(ctx context.Context, key K) error

    Keys() []K
}

在实现本地缓存之前,首先定义一个 ICache[K comparable, V any] 泛型接口,该接口作为多种本地缓存实现的 API 标准,确保不同本地缓存的实现都有相同的基本操作。

本地缓存适配器的实现

Cache 结构体

// https://github.com/chenmingyong0423/go-generics-cache/blob/master/cache.go
type Cache[K comparable, V any] struct {
   
    cache ICache[K, *Item[V]]
    mutex sync.RWMutex

    janitor *janitor
}

type Item[V any] struct {
   
    value      V
    expiration time.Time
}

func (c *Cache[K, V]) Get(ctx context.Context, key K) (v V, err error) {
   
    c.mutex.RLock()
    defer c.mutex.RUnlock()
    item, err := c.cache.Get(ctx, key)
    if err != nil {
   
        return
    }
    if item.Expired() {
   
        return v, cacheError.ErrNoKey
    }
    return item.value, nil
}

func (c *Cache[K, V]) Set(ctx context.Context, key K, value V, opts ...ItemOption) (err error) {
   
    c.mutex.Lock()
    defer c.mutex.Unlock()
    item := newItem[V](value, opts...)
    return c.cache.Set(ctx, key, item)
}

// 省略其余方法

上述代码定义了一个泛型的 Cache[K comparable, V any] 缓存结构体实现。该结构体并非实际的本地缓存的实现,而是一个适配器。该结构体有三个字段:

1、cache ICache[K, *Item[V]]

  • 底层实现缓存操作的接口。该接口定义了缓存的基本行为,如设置、获取和删除键值对。
  • *Item[V] 是值的类型,这里使用了指针,指向一个 Item 结构,Item 结构体包含了实际的值和过期时间。

2、mutex sync.RWMutex

  • 读写互斥锁,用于避免并发读写时数据不一致性的问题。

3、janitor *janitor

  • 一个用于处理后台任务的结构体,例如定时清理过期数据,单独提出一个结构体是为了解耦。

实现支持多种淘汰机制的本地缓存原理

如果要支持多种淘汰机制,最佳的做法是不应集中在一个本地缓存的具体实现里去应用,这会导致代码复杂和难以维护,同时违反了软件设计的开闭原则。

我们应该设计成 → 一个 淘汰机制对应 一个 策略(本地缓存的具体实现)。回想上面的代码定义,Cache 是一个适配器结构体,它有一个 ICache 类型的字段 cache,通过引入该接口字段,遵循了 依赖倒置原则策略模式,从而可以根据不同需求选择合适的缓存实现。

不同的缓存实现有着不同的缓存淘汰机制,例如 最近最少使用LRU)缓存实现、最不经常使用LFU)的缓存实现,基于这个接口字段,我们就可以设置对应的淘汰机制。

下面是创建 simpleLRU 本地缓存的函数:

// https://github.com/chenmingyong0423/go-generics-cache/blob/master/cache.go
func NewSimpleCache[K comparable, V any](ctx context.Context, size int, interval time.Duration) *Cache[K, V] {
   
    cache := &Cache[K, V]{
   
        cache:   simple.NewCache[K, *Item[V]](size),
        janitor: newJanitor(ctx, interval),
    }
    cache.janitor.run(cache.DeleteExpired)
    return cache
}

func NewLruCache[K comparable, V any](ctx context.Context, cap int, interval time.Duration) *Cache[K, V] {
   
    cache := &Cache[K, V]{
   
        cache:   lru.NewCache[K, *Item[V]](cap),
        janitor: newJanitor(ctx, interval),
    }
    cache.janitor.run(cache.DeleteExpired)
    return cache
}

在这两个构造函数中,Cache 结构体的 cache 字段初始化为不同的本地缓存实现。这种设计允许根据具体需求选择相应的淘汰机制,例如使用 LRU 本地缓存来淘汰元素。这样,我们就可以灵活应对不同的缓存场景,确保缓存策略的适用性和高效性。

如果想了解 lru 本地缓存如何实现,可前往 → https://github.com/chenmingyong0423/go-generics-cache/blob/master/lru/lru_cache.go 进行查看。

实现支持多种过期机制的本地缓存原理

// https://github.com/chenmingyong0423/go-generics-cache/blob/master/cache.go
type ItemOption func(*itemOptions)

type itemOptions struct {
   
    expiration time.Time
}

func WithExpiration(exp time.Duration) ItemOption {
   
    return func(o *itemOptions) {
   
        o.expiration = time.Now().Add(exp)
    }
}

type Item[V any] struct {
   
    value      V
    expiration time.Time
}

func newItem[V any](value V, opts ...ItemOption) *Item[V] {
   
    var item = &itemOptions{
   }
    for _, opt := range opts {
   
        opt(item)
    }
    return &Item[V]{
   
        value:      value,
        expiration: item.expiration,
    }
}

func (i *Item[V]) Expired() bool {
   
    return !i.expiration.IsZero() && i.expiration.Before(time.Now())
}

func (c *Cache[K, V]) Get(ctx context.Context, key K) (v V, err error) {
   
    c.mutex.RLock()
    defer c.mutex.RUnlock()
    item, err := c.cache.Get(ctx, key)
    if err != nil {
   
        return
    }
    if item.Expired() {
   
        return v, cacheError.ErrNoKey
    }
    return item.value, nil
}

func (c *Cache[K, V]) Delete(ctx context.Context, key K) (err error) {
   
    c.mutex.Lock()
    defer c.mutex.Unlock()
    return c.cache.Delete(ctx, key)
}

func (c *Cache[K, V]) Keys() []K {
   
    return c.cache.Keys()
}

func (c *Cache[K, V]) DeleteExpired(ctx context.Context) {
   
    c.mutex.RLock()
    keys := c.Keys()
    c.mutex.RUnlock()
    i := 0
    for _, key := range keys {
   
        if i > 10000 {
   
            return
        }
        c.mutex.Lock()
        if item, err := c.cache.Get(ctx, key); err == nil && item.Expired() {
   
            _ = c.cache.Delete(ctx, key)
        }
        c.mutex.Unlock()
        i++
    }
}

func (c *Cache[K, V]) SetNX(ctx context.Context, key K, value V, opts ...ItemOption) (b bool, err error) {
   
    c.mutex.Lock()
    defer c.mutex.Unlock()
    _, err = c.cache.Get(ctx, key)
    if err != nil {
   
        if errors.Is(err, cacheError.ErrNoKey) {
   
            item := newItem[V](value, opts...)
            return true, c.cache.Set(ctx, key, item)
        }
        return false, err
    }
    return false, nil
}

func (c *Cache[K, V]) Set(ctx context.Context, key K, value V, opts ...ItemOption) (err error) {
   
    c.mutex.Lock()
    defer c.mutex.Unlock()
    item := newItem[V](value, opts...)
    return c.cache.Set(ctx, key, item)
}

我们重点关注 Set 方法,其第三个参数为 ItemOptions 类型,它是一个可选参数,意味着我们可以选择性地去设置 Item 的某些属性值。目前 ItemOption 只有一个 expiration 过期时间的可选参数,因此我们可以根据不同情况,决定是否使用 WithExpiration 函数设置过期时间。

这种设计,使得元素支持多种过期机制(固定时间过期和永久不过期的机制)。当然我们还可以添加其他的过期机制,例如 滑动过期时间(缓存项的过期时间会根据最后一次访问时间进行更新),那么就需要对 Item 结构体和 itemOptions 以及 Cache 结构体的 GetSet 方法进行修改。例如 Item 的一个修改案例:

type Item[V any] struct {
   
    value             V
    fixedExpiration   time.Time // 固定过期时间
    slidingExpiration time.Duration
    lastAccessed      time.Time // 记录最后访问时间
}

相应的,itemOptions 和关联代码也要修改。

小结

本文探讨了实现一个支持多种 过期、淘汰 机制的本地缓存的核心原理。

为了实现多种淘汰机制,我们可以采用不同的策略(也称为本地缓存的具体实现)来实现各种淘汰机制,然后,通过一个本地缓存适配器结合策略模式,根据不同需求选择合适的缓存实现(淘汰机制)。

至于实现多种过期机制的实现,我们可以将设计重点放在缓存项 ItemSet 方法上。例如,在 Item 项中,如果需要支持多种过期机制,我们可以根据需求设计其属性,并使用选项模式,有选择地设置属性的值。最终,将可选参数传递给 Set 方法来实现不同的过期策略。

本文提到的设计是基于个人理解去实现的,可能会有不妥之处。如果有错误的地方,欢迎指出!

目录
相关文章
|
2月前
|
人工智能 安全 Java
Go与Java泛型原理简介
本文介绍了Go与Java泛型的实现原理。Go通过单态化为不同类型生成函数副本,提升运行效率;而Java则采用类型擦除,将泛型转为Object类型处理,保持兼容性但牺牲部分类型安全。两种机制各有优劣,适用于不同场景。
88 24
|
5月前
|
缓存 并行计算 PyTorch
PyTorch CUDA内存管理优化:深度理解GPU资源分配与缓存机制
本文深入探讨了PyTorch中GPU内存管理的核心机制,特别是CUDA缓存分配器的作用与优化策略。文章分析了常见的“CUDA out of memory”问题及其成因,并通过实际案例(如Llama 1B模型训练)展示了内存分配模式。PyTorch的缓存分配器通过内存池化、延迟释放和碎片化优化等技术,显著提升了内存使用效率,减少了系统调用开销。此外,文章还介绍了高级优化方法,包括混合精度训练、梯度检查点技术及自定义内存分配器配置。这些策略有助于开发者在有限硬件资源下实现更高性能的深度学习模型训练与推理。
938 0
|
2月前
|
存储 人工智能 安全
深入理解 go sync.Map - 基本原理
本文介绍了 Go 语言中 `map` 在并发使用时的常见问题及其解决方案,重点对比了 `sync.Mutex`、`sync.RWMutex` 和 `sync.Map` 的性能差异及适用场景。文章指出,普通 `map` 不支持并发读写,容易引发错误;而 `sync.Map` 通过原子操作和优化设计,在某些场景下能显著提升性能。同时详细讲解了 `sync.Map` 的基本用法及其适合的应用环境,如读多写少或不同 goroutine 操作不同键的场景。
127 1
|
1月前
|
缓存 监控 安全
告别缓存击穿!Go 语言中的防并发神器:singleflight 包深度解析
在高并发场景中,多个请求同时访问同一资源易导致缓存击穿、数据库压力过大。Go 语言提供的 `singleflight` 包可将相同 key 的请求合并,仅执行一次实际操作,其余请求共享结果,有效降低系统负载。本文详解其原理、实现及典型应用场景,并附示例代码,助你掌握高并发优化技巧。
153 0
|
3月前
|
算法 Java Go
Go内存原理-GC原理
本文介绍了Go语言中垃圾回收(GC)机制的发展与实现原理,涵盖从标记-清除算法到三色标记法,再到三色标记加混合写屏障的演进过程,重点解析各版本GC的核心思想、优缺点及性能优化方向。
|
4月前
|
安全 Go 开发者
Go语言之切片的原理与用法 - 《Go语言实战指南》
切片(slice)是Go语言中用于处理变长数据集合的核心结构,基于数组的轻量级抽象,具有灵活高效的特点。切片本质是一个三元组:指向底层数组的指针、长度(len)和容量(cap)。本文详细介绍了切片的声明与初始化方式、基本操作(如访问、修改、遍历)、长度与容量的区别、自动扩容机制、共享与副本处理、引用类型特性以及常见陷阱。通过理解切片的底层原理,开发者可以更高效地使用这一数据结构,优化代码性能。
112 13
|
4月前
|
Go 调度
GO语言函数的内部运行机制分析
以上就是Go语言中函数的内部运行机制的概述,展示了函数在Go语言编程中如何发挥作用,以及Go如何使用简洁高效的设计,使得代码更简单,更有逻辑性,更易于理解和维护。尽管这些内容深入了一些底层的概念,但我希望通过这种方式,将这些理论知识更生动、更形象地带给你,让你在理解的同时找到编程的乐趣。
68 5
|
4月前
|
安全 Go
defer关键字:延迟调用机制-《Go语言实战指南》
Go 语言中的 `defer` 是用于延迟执行函数调用的关键字,广泛应用于资源释放、异常捕获和日志记录等场景。它在函数返回前执行,支持栈式后进先出(LIFO)顺序,参数求值时机为声明时而非执行时。常见用法包括文件关闭、锁解锁及结合 `recover` 处理 panic。尽管高效,频繁使用可能带来性能开销,需谨慎处理。总结而言,`defer` 是构建健壮代码的核心工具之一。
|
4月前
|
人工智能 Go
[go]Slice 切片原理
本文详细介绍了Go语言中的切片(slice)数据结构,包括其定义、创建方式、扩容机制及常见操作。切片是一种动态数组,依托底层数组实现,具有灵活的扩容和传递特性。文章解析了切片的内部结构(包含指向底层数组的指针、长度和容量),并探讨了通过`make`创建切片、基于数组生成切片以及切片扩容的规则。此外,还分析了`append`函数的工作原理及其可能引发的扩容问题,以及切片拷贝时需要注意的细节。最后,通过典型面试题深入讲解了切片在函数间传递时的行为特点,帮助读者更好地理解和使用Go语言中的切片。
102 0
|
5月前
|
缓存 NoSQL Go
【LeetCode 热题100】146:LRU 缓存(详细解析)(Go语言版)
本文详细解析了力扣 146 题——LRU 缓存机制的实现方法。通过结合哈希表与双向链表,确保 `get` 和 `put` 操作均在 O(1) 时间内完成。哈希表用于快速查找,双向链表记录访问顺序,支持最近使用数据的高效更新与淘汰。代码以 Go 语言实现,结构清晰,涵盖核心操作如节点移动、插入与删除。此题为面试高频考点,适用于数据缓存、页面置换等场景,掌握后可加深对缓存策略的理解。
249 4