Go 简单设计和实现可扩展、高性能的泛型本地缓存

相信大家对于缓存这个词都不陌生,但凡追求高性能的业务场景,一般都会使用缓存,它可以提高数据的检索速度,减少数据库的压力。缓存大体分为两类:本地缓存和分布式缓存(如 Redis)。本地缓存适用于单机环境下,而分布式缓存适用于分布式环境下。在实际的业务场景中,这两种缓存方式常常被结合使用,以利用各自的优势,实现高性能的数据读取。本文将会探讨如何极简设计并实现一个可扩展、高性能的本地缓存。

设计总览

在设计一个本地缓存时,我们需要考虑以下几个关键方面:

  • 并发安全 确保缓存的读写在多个 goroutine 环境下是安全的。通常使用 sync.Mutexsync.RWMutex 来避免竞态条件和数据不一致的问题。
  • 淘汰策略 专注于当缓存空间有限时如何选择移除哪些数据。常见的淘汰策略包括 最近最少使用LRU)、最不经常使用LFU)、先进先出FIFO)等。
  • 内存管理 合理管理内存的使用。例如通过限制缓存大小或实现内存淘汰机制,避免内存泄露和过度消耗。
  • ······

除了上面列出的三项,在必要的情况下,我们可能还需要考虑其他方面,例如监控和日志、容错和恢复等。

请在此添加图片描述

本文将会讲解图中所给出的四个部分的设计:

  • Cache[K comparable, V any]:基于策略模式的灵活、可扩展和并发安全的缓存结构体设计。
  • ICache[K comparable, V any]:缓存 API 接口,用于定义缓存 API 规范。
  • SimpleCache[K comparable, V any]:基于简单 map 实现的缓存实例。
  • LRUCache[K comparable, V any]:基于 LRU 淘汰算法实现的缓存实例。

ICache 接口

首先,定义一个 ICache 接口:

 

go

复制代码

// https://github.com/chenmingyong0423/go-generics-cache/blob/master/cache.go 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 }

该接口作为多种本地缓存实现的 API 标准,确保不同本地缓存的实现都有相同的基本操作。

Cache 适配器

 

go

复制代码

// 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 }

上述代码定义的 CacheK[comparable, V any] 结构体是一个基于泛型的缓存适配器实现,它不直接实现本地缓存的逻辑。Cache 结构体有三个字段:

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

这是一个接口字段,用于抽象化底层的本地缓存操作。该接口定义了缓存的基本行为,如设置、获取和删除键值对。通过引入这个接口,Cache 结构体遵循了 依赖倒置原则策略模式,使得可以根据具体需求灵活选择不同的缓存实现策略。

*Item[V] 是值的类型,这里使用了指针,指向一个 Item 结构,Item 结构体包含了实际的值和过期时间。

2、mutex sync.RWMutex

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

3、janitor *janitor

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

Item 的设计

 

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()) }

Item 结构体作为本地缓存适配器 Cachevalue 值的类型,它有两个字段和一个方法,分别是:

  • value:用于本地缓存对应 keyvalue 值。
  • expiration :表示缓存值的过期时间。
  • Expired:判断元素是否过期的方法。

为了方便创建并初始化 Item 元素,代码中实现了一个 newItem 函数,该函数除了接受 value 值以外,还接受一个或多个 ItemOption 类型的参数。这些参数是可选的,允许我们在创建 Item 实例时设置额外的属性。例如,可以通过 WithExpiration 函数选项来指定过期时间。


Item 这种设计方式使得元素支持 多种过期机制(固定时间过期和永久不过期的机制),同时提高了代码扩展性和灵活性。如果我们后续需要为 value 添加额外的属性,只需要往 Item 结构体新增字段即可,并且 newItem 方法,引入了函数选项模式,即使后面新增字段,这些字段可以作为可选参数初始化。例如支持 滑动过期时间。所有的新增操作都不会影响已有代码。

构造 Cache 的函数

 

go

复制代码

// https://github.com/chenmingyong0423/go-generics-cache/blob/master/cache.go // NewSimpleCache - 创建一个新的简单缓存。 // interval time.Duration - 清理过期缓存项的时间间隔。在这个间隔内,缓存将自动检查并清理过期项。 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 } // NewLruCache - 创建一个新的LRU缓存。 // interval time.Duration - 清理过期缓存项的时间间隔。在这个间隔内,缓存将自动检查并清理过期项。 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 }

上述代码段包含了两个构造函数,分别用于创建两种不同类型的本地缓存。以下是对这两个函数的详细说明:

1、NewSimpleCache 函数

这个函数用于创建一个简单缓存的实例。它接受以下参数:

  • ctx context.Context:上下文,用于管理缓存的生命周期和相关操作。
  • size int:缓存的大小,可能表示缓存可以存储的最大项数。
  • interval time.Duration:用于清理过期缓存项的时间间隔。在这个时间间隔内,缓存将自动检查并清理过期的项。

函数的实现逻辑如下:

  1. 创建一个 Cache[K, V] 类型的实例。
  2. 使用 simple.NewCache[K, *Item[V]](size) 创建一个简单缓存的底层实现,并将其赋值给 Cache 实例的 cache 字段。
  3. 使用 newJanitor(ctx, interval) 创建一个清理过期项的 janitor,并将其赋值给 Cache 实例的 janitor 字段。
  4. 通过 cache.janitor.run(cache.DeleteExpired) 启动 janitor,以定期运行 DeleteExpired 方法清理过期项。
  5. 返回创建的 Cache 实例。

2、 NewLruCache 函数

这个函数用于创建一个基于最近最少使用(LRU)策略的缓存实例。它的参数与 NewSimpleCache 相同:

  • ctx context.Context:上下文,用于管理缓存的生命周期和相关操作。
  • cap int:缓存的容量,指示缓存可以存储的最大项数。
  • interval time.Duration:用于清理过期缓存项的时间间隔。

函数的实现逻辑类似于 NewSimpleCache,但它使用 lru.NewCache[K, *Item[V]](cap) 来创建一个基于 LRU 策略的底层缓存实现。

这两个函数提供了不同策略的本地缓存实现,分别适用于不同的应用场景。简单缓存可能适用于基本的缓存需求,而基于 LRU 的缓存更适合于需要淘汰老旧数据以节省空间的场景。通过这样的设计,使用者可以根据具体需求选择最适合的缓存策略。


下一个大章节的内容将详细介绍 simplelru 这两种本地缓存的实现细节。

janitor 的设计

 

go

复制代码

// https://github.com/chenmingyong0423/go-generics-cache/blob/master/cache.go type janitor struct { ctx context.Context interval time.Duration done chan struct{} once sync.Once } func (j *janitor) stop() { j.once.Do(func() { close(j.done) }) } func (j *janitor) run(cleanup func(ctx context.Context)) { go func() { ticker := time.NewTicker(j.interval) defer ticker.Stop() for { select { case <-ticker.C: cleanup(j.ctx) case <-j.ctx.Done(): j.stop() case <-j.done: cleanup(j.ctx) return } } }() }

janitor 可以认为是一个处理后台任务的结构体,其作用是定时清理 本地缓存 的过期数据。

下面是对 janitor 结构体的字段和方法的具体解释:

  • ctxcontext 上下文,作用是控制 run 方法中的协程何时停止执行,也就是控制后台任务的停止执行。
  • interval:时间间隔,指定清理操作的执行频率。
  • done:一个通道(channel),用于发出停止信号。当通道被关闭时,意味着 run 方法中的写成停止执行,结束后台任务。
  • once:一个 sync.Once 的实例,确保关闭 done 通道只执行一次。
  • stop 方法:用于停止 janitor 的运行,它利用 sync.Once 来确保关闭 done 通道的操作只执行一次,避免多次关闭通道导致的 panic。关闭 done 通道将导致 run 方法中的协程停止执行。
  • run 方法:该方法接受一个 clean 清理函数,里面包含用户自定义的清理逻辑。run 方法启动一个协程。在协程里,首先创建了一个定时器,用于控制任务的执行间隔时间;接着启动一个 for 循环,它使用 select 语句来监听多个通道:
    • ticker.C 通道接收到信号时(即每隔 j.interval 时间),调用 cleanup 函数执行清理操作。
    • j.ctx.Done() 通道接收到信号时(即上下文被取消或超时),调用 j.stop() 方法来停止 janitor
    • j.done 通道被关闭时(通过调用 stop 方法),执行最后一次清理操作然后退出协程。

方法详解

Get 方法

 

go

复制代码

// https://github.com/chenmingyong0423/go-generics-cache/blob/master/cache.go 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 }

该方法的作用是通过指定的 key 获取对应的 value,核心逻辑:

  • 加读锁:通过添加读锁,避免在读取数据时有更新或删除操作,导致数据不一致的问题。
  • 判断元素是否过期:通过 Expired 方法判断元素是否过期,成立则返回一个明确的错误 error
  • 返回结果:返回所匹配到的 value 值。

Set

 

go

复制代码

// https://github.com/chenmingyong0423/go-generics-cache/blob/master/cache.go 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) }

该方法用于将键值对 key-value 保存到本地缓存中。Set 方法除了接收 keyvalue 作为必要参数,还接受一个或多个 ItemOption 类型的参数作为可选配置。

核心逻辑:

  • 加写锁:为了保证在写入数据时的协程安全性,Set 方法首先加上写锁。这样做可以防止在写操作进行时发生读操作,避免可能导致的数据不一致问题。
  • 创建并初始化 Item:利用 newItem[V] 函数创建一个 Item 实例,其中 value 是必传参数。此外,根据不同的使用场景,可以通过传递 ItemOption 类型的参数来初始化 Item 的可选配置,如设置过期时间等。
  • 设置键值对:最后,通过 c.cache.Set 调用底层的实现的方法将键值对保存到本地缓存中。
  • 返回结果:返回 nil 或可能的错误(如果写入过程中发生错误)。

SetNX

 

go

复制代码

// https://github.com/chenmingyong0423/go-generics-cache/blob/master/cache.go 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 }

该方法和 Set 方法功能相似,但它与 Set 方法的主要区别在于它不会覆盖已存在的键值对。

核心逻辑:

  • 加写锁:为了保证在写入数据时的协程安全性,SetNX 方法首先加上写锁。这样做可以防止在写操作进行时发生读操作,避免可能导致的数据不一致问题。
  • 检查键是否存在:首先尝试获取指定的 key。如果键不存在(识别为 cacheError.ErrNoKey 错误),则继续执行;如果获取过程中发生其他错误,方法将返回错误。
  • 条件性写入:如果指定的键不存在于缓存中,SetNX 会利用 newItem[V] 函数创建一个新的 Item 实例,并将其与 key 一起保存到缓存中。在这个过程中,它也接受可选的 ItemOption 参数,允许对缓存项进行进一步的配置,例如设置过期时间。
  • 返回结果:如果键已存在,方法返回 falsenil 错误,表示没有新的键值对被添加。如果键不存在且成功设置了新的键值对,方法返回 true 和可能发生的错误 error(如果写入过程中发生错误)。

Delete

 

go

复制代码

// https://github.com/chenmingyong0423/go-generics-cache/blob/master/cache.go 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) }

该方法用于从本地缓存中删除指定的键值对。

核心逻辑:

  • 加写锁:首先,Delete 方法获取一个写锁。这是为了保证在删除操作进行时,缓存的状态不会被其他的读或写操作所干扰,从而确保操作的协程安全性。
  • 调用底层删除方法:在锁定状态下,该方法通过调用 c.cache.Delete(ctx, key) 来执行实际的删除操作。这一步骤涉及到对底层缓存数据结构的操作,以确保指定的键 key 被移除。
  • 返回结果:方法最后返回执行结果,nil 或可能发送的错误 error

Keys

 

go

复制代码

// https://github.com/chenmingyong0423/go-generics-cache/blob/master/cache.go func (c *Cache[K, V]) Keys() []K { return c.cache.Keys() }

该方法用于获取本地缓存中所有的键,并以切片的形式返回这些键。返回的键的顺序取决于底层本地缓存实现的具体细节。

DeleteExpired

 

go

复制代码

// https://github.com/chenmingyong0423/go-generics-cache/blob/master/cache.go 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++ } }

该方法用于删除本地缓存中已过期的项。核心逻辑:

  1. 加读锁:方法首先加上读锁(c.mutex.RLock()),以安全地访问共享资源。
  2. 获取所有键:然后,它调用 c.Keys() 方法来获取缓存中所有的键,并将这些键存储在 keys 切片中。
  3. 释放读锁:获取完所有键后,方法释放读锁(c.mutex.RUnlock())。
  4. 迭代检查和删除:接下来,方法遍历 keys 切片中的每个键。在遍历过程中,它实现了以下步骤:
    • 限制检查次数:设置一个计数器 i,如果检查的项数超过 10000,方法将提前结束。
    • 加写锁:对于每个键,方法加上写锁(c.mutex.Lock())以安全地执行写操作。
    • 检查并删除过期项:方法尝试获取每个键对应的项。如果获取成功且该项已过期(item.Expired() 返回 true),则调用 c.cache.Delete(ctx, key) 来删除该键值对。
    • 释放写锁:完成检查和可能的删除操作后,方法释放写锁(c.mutex.Unlock())。
  5. 计数器递增:每检查一个键,计数器 i 递增。这用于控制方法检查的最大项数,避免可能的性能问题。

这里做了一个优化:引入了一个计数器,当 i 超过 10000 时,则停止操作。这样做的好处:

  • 减少锁占用时间,防止性能下降:在有大量键值对的情况下,遍历和检查所有项会频繁获取写锁,对整个缓存系统的性能产生负面影响。尤其是在高并发的环境中。限制检查数量有助于减少锁的占用时间。

计数器 i 的最大值应根据具体场景进行调整,因为不同的应用环境和性能要求会影响到合适的最大值选择。

本地缓存的具体实现

simple cache

 

go

复制代码

// https://github.com/chenmingyong0423/go-generics-cache/blob/master/simple/simple_cache.go type Cache[K comparable, V any] struct { cache map[K]V } func NewCache[K comparable, V any](size int) *Cache[K, V] { return &Cache[K, V]{ cache: make(map[K]V, size), } } func (c *Cache[K, V]) Set(_ context.Context, key K, value V) error { c.cache[key] = value return nil } func (c *Cache[K, V]) Get(_ context.Context, key K) (V, error) { var ( value V ok bool ) if value, ok = c.cache[key]; !ok { return value, cacheError.ErrNoKey } return value, nil } func (c *Cache[K, V]) Delete(_ context.Context, key K) error { if _, ok := c.cache[key]; ok { delete(c.cache, key) return nil } return cacheError.ErrNoKey } func (c *Cache[K, V]) Keys() []K { keys := make([]K, 0) for key := range c.cache { keys = append(keys, key) } return keys }

simple cache 实现了 ICache 接口,它的设计较为简单,以 map 作为其核心数据结构,使得键值对的存储和检索操作简单高效。这种缓存实现适用于不需要复杂缓存策略的基本用例。

需要注意的是,在 GetDelete 方法中,如果键不存在,则会返回一个明确的错误 cacheError.ErrNoKey,这有助于调用者区分 "缓存未命中" 与其他类型的错误。

lru cache

 

go

复制代码

// https://github.com/chenmingyong0423/go-generics-cache/blob/master/lru/lru_cache.go type entry[K comparable, V any] struct { key K value V } func NewCache[K comparable, V any](cap int) *Cache[K, V] { return &Cache[K, V]{ maxEntries: cap, cache: make(map[K]*list.Element, cap), linkedDoublyList: list.New(), } } type Cache[K comparable, V any] struct { maxEntries int cache map[K]*list.Element linkedDoublyList *list.List } func (c *Cache[K, V]) Set(_ context.Context, key K, value V) error { if e, ok := c.cache[key]; ok { // 元素存在 c.linkedDoublyList.MoveToFront(e) e.Value.(*entry[K, V]).value = value return nil } // 元素不存在 e := &entry[K, V]{ key: key, value: value, } c.cache[key] = c.linkedDoublyList.PushFront(e) if c.linkedDoublyList.Len() > c.maxEntries { // 删除最后一个元素 e := c.linkedDoublyList.Back() c.linkedDoublyList.Remove(e) delete(c.cache, e.Value.(*entry[K, V]).key) } return nil } func (c *Cache[K, V]) Get(_ context.Context, key K) (v V, err error) { if e, ok := c.cache[key]; ok { c.linkedDoublyList.MoveToFront(e) e := e.Value.(*entry[K, V]) return e.value, nil } return v, cacheError.ErrNoKey } func (c *Cache[K, V]) Delete(_ context.Context, key K) error { if e, ok := c.cache[key]; ok { c.linkedDoublyList.Remove(e) delete(c.cache, key) return nil } return cacheError.ErrNoKey } func (c *Cache[K, V]) Keys() []K { keys := make([]K, 0) // 根据添加顺序返回 for e := c.linkedDoublyList.Back(); e != nil; e = e.Prev() { keys = append(keys, e.Value.(*entry[K, V]).key) } return keys }

lru cache 实现了 ICache 接口。这里借助了哈希表(map)和双向链表(这里使用 container 包里的一个具体实现 List)来实现 最近最少使用 lru 本地缓存。

类型定义

  • entry[K comparable, V any]:这是一个私有的结构体,用于存储缓存中的键(key)和值(value)。
  • Cache[K comparable, V any]:这是公开的缓存结构体,包含以下字段:
    • maxEntries:缓存能够存储的最大条目数。
    • cache:一个映射,将键映射到双向链表中的元素(list.Element)。
    • linkedDoublyList:一个 list.List 类型的双向链表,用于维护键的使用顺序。

构造函数

  • NewCache[K comparable, V any](cap int):创建并返回一个新的 Cache[K, V] 实例。接受缓存容量 cap 作为参数,并初始化内部结构。

方法

  • Set(_ context.Context, key K, value V):向缓存中添加一个键值对。基于 最近最少使用 的原则,如果键已经存在,则更新其值并将其移至链表的前端。如果键不存在,则创建一个新的 entry 项并将其加入链表的前端。如果加入新项后缓存超过最大容量,则从链表尾部移除最少使用的项。
  • Get(_ context.Context, key K):根据键从缓存中检索值。如果找到了键,则将对应的链表元素移至前端并返回其值。如果键不存在,则返回 cacheError.ErrNoKey 错误。
  • Delete(_ context.Context, key K):从缓存中删除指定的键及其对应的值。如果键存在,则从链表和 map 中移除相应的元素。
  • Keys():返回一个包含缓存中所有键的切片,按照从最近到最少使用的顺序排列。

小结

本文详细介绍了如何设计和实现一个极简的可扩展、高性能的泛型本地缓存。

核心在于引入了 Cache 适配器,它的关键字段 cache 是一个类型为 ICache 的接口。这个设计使得我们能够灵活地提供多种不同底层实现的本地缓存实例,例如 simple cachelru cache。这样,Cache 适配器不仅支持多样化的缓存策略,还保留了通用的缓存操作接口,如 GetSetSetNXDelete

在具体实现方面, simple cache 较为简单,基于 map 的读写操作实现,而 lru cache 则更为复杂,它结合哈希表(map)和双向链表(使用 container 包里的 List,也可以自己实现一个双向链表)来实现 最近最少使用 lru 本地缓存。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/724219.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【小智好书分享• 第二期】《低代码平台开发实践:基于React》

最近&#xff0c;我发现了一个超级强大的人工智能学习网站。它以通俗易懂的方式呈现复杂的概念&#xff0c;而且内容风趣幽默。我觉得它对大家可能会有所帮助&#xff0c;所以我在此分享。点击这里跳转到网站。 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&am…

磁性机器人在医学领域取得进展

磁性医疗机器人利用磁场梯度来控制设备的运动&#xff0c;并最终以高精度进入体内的目标组织。这些磁性机器人可以采用导管和微型或纳米机器人的形式&#xff0c;并由磁导航系统操纵。磁性机器人最近取得了一些进展&#xff0c;为临床诊断和治疗用途开辟了新的可能性。在本期的…

【二叉树的最近公共祖先】【后序遍历】Leetcode 236. 二叉树的最近公共祖先

【二叉树的最近公共祖先】【后序遍历】Leetcode 236. 二叉树的最近公共祖先 解法1 涉及到结果向上返回就要用后序遍历解法2 自己写的方法 后序遍历 ---------------&#x1f388;&#x1f388;236. 二叉树的最近公共祖先 题目链接&#x1f388;&#x1f388;-----------------…

CMake-深入理解find_package()的用法

前言&#xff1a; CMake给我们提供了find_package()命令用来查找依赖包&#xff0c;理想情况下&#xff0c;一句find_package()命令就能把一整个依赖包的头文件包含路径、库路径、库名字、版本号等情况都获取到&#xff0c;后续只管用就好了。但实际使用过程可能会出现这样那样…

SpringBoot集成flink

Flink是一个批处理和流处理结合的统一计算框架&#xff0c;其核心是一个提供了数据分发以及并行化计算的流数据处理引擎。 最大亮点是流处理&#xff0c;最适合的应用场景是低时延的数据处理。 场景&#xff1a;高并发pipeline处理数据&#xff0c;时延毫秒级&#xff0c;且兼具…

鸿蒙NEXT开发实战:【视频文件裁剪】

使用OpenHarmony系统提供的ffmpeg三方库的能力在系统中实现了音视频文件裁剪的功能&#xff0c;并通过NAPI提供给上层应用调用。 基础信息 视频文件裁剪 简介 在OpenHarmony系统整个框架中有很多子系统&#xff0c;其中多媒体子系统是OpenHarmony比较重要的一个子系统&#…

Spring基础——方法注入(Method Injection)

目录 查找方法注入&#xff08;Lookup Method&#xff09;查找方法注入基于XML的方法注入基于注解的方法注入 Arbitrary Method Replacement&#xff08;任意方法替换&#xff09; 文章所用项目源码参考&#xff1a;java_spring_learn_repo 查找方法注入&#xff08;Lookup Met…

解决微信好友添加频繁问题

今天我们来聊一聊微信好友添加频繁的问题。在日常使用中&#xff0c;有时候我们会遇到一些添加好友受限的情况&#xff0c;那么究竟是什么原因导致了这一问题呢&#xff1f;接下来&#xff0c;让我们逐一来看一看。 1. 添加好友的频率太高 首先&#xff0c;如果我们在短时间内…

.NetCore6.0实现ActionFilter过滤器记录接口请求日志

文章目录 目的实现案例&#xff1a;一.首先我们新建一个WebApi项目二.配置 appsettings.json 文件&#xff0c;配置日志存放路径三.创建 Model 文件夹&#xff0c;创建AppConfig类和ErrorLog类1.在AppConfig类中编写一个GetConfigInfo方法获取配置文件中的值2.在ErrorLog类中&a…

供应josef约瑟DL-24C电流继电器 额定电流3A 整定范围0.5-2A 电气控制必备元件

电流继电器是一种特殊的电子控制器件&#xff0c;具有控制系统和被控制系统&#xff0c;它使用较小的电流去控制较大的电流&#xff0c;起到自动开关的作用。以下是电流继电器的特征&#xff1a; 承载大电流&#xff1a;电流继电器可以承载大电流&#xff0c;通常能够承受数十…

SpringBoot集成Docker

Docker是一个开源的应用容器引擎&#xff0c;它允许开发者将应用及其依赖打包到一个可移植的容器中。 一、依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://ww…

大语言模型(LLM):每个专业人士的完美助手

「大语言模型&#xff08;LLM&#xff09;革命」&#xff1a;ChatGPT如何引领工作效率新篇章 在不断发展的技术领域&#xff0c;像 ChatGPT 这样的大型语言模型 (LLM) 已成为各行业专业人士不可或缺的工具。 这篇博文探讨了大语言模型&#xff08;LLM&#xff09;在专业环境中的…

GDPU Java 天码行空2

实验2 类与封装性 文章目录 实验2 类与封装性&#xff08;一&#xff09;实验目的&#xff08;二&#xff09;实验内容和步骤&#xff08;1&#xff09;建立学生类和测试类。学生类中有成员变量&#xff1a;姓名&#xff0c;年龄&#xff1b;成员方法&#xff1a;学习&#xff…

程序逻辑控制

1.java的三大结构 可以说java的这三大结构包括其中的语句跟c语言上的基本上都是一样的。现在就当重新复习一遍吧&#xff01; 1.顺序结构 2.分支结构 if语句 跟c语言的语法一模一样。就直接看文案了。 switch语句 java中的switch语句跟c语言中的switch几乎相同&#xff0c;…

AtCoder Beginner Contest 343 A~F

A.Wrong Answer&#xff08;模拟&#xff09; 题意&#xff1a; 给你两个整数 A A A和 B B B&#xff0c;它们介于 0 0 0和 9 9 9之间。 输出任何一个介于 0 0 0和 9 9 9之间且不等于 A B AB AB的整数。 分析&#xff1a; 按题意判断并输出一个整数即可。 代码&#xff…

qnx display

05-SA8155 QNX Display框架及代码分析(1)_openwfd-CSDN博客 backlight p: 0 t: 0 00000 SHD -----ONLINE----- 2024/03/06 13:49:22.046 backlight p:1060958 t: 1 00000 ERR backlight_be[backlight_be.c:284]: pthread_create enter 2024/03/06 13…

python基础练习题目

1. 根据身高体重&#xff0c;判断人的胖瘦 描述&#xff1a; 通过身高和体重&#xff0c;判断一个人的胖瘦。国际上一般采用BMI体重指数&#xff0c;计算公式为BMI 体重 / 身高2(保留小数点后1位)&#xff0c;参考标准如下&#xff1a;‪‬‪‬‪‬‪‬‪‬‮‬‪‬‫‬‪‬‪…

SpringBoot集成RocketMQ

RocketMQ是一个纯Java、分布式、队列模型的开源消息中间件&#xff0c;前身是MetaQ&#xff0c;是阿里参考Kafka特点研发的一个队列模型的消息中间件&#xff0c;后开源给apache基金会成为了apache的顶级开源项目&#xff0c;具有高性能、高可靠、高实时、分布式特点。 环境搭…

对VisionPro的认识,CogPMAlingTool模板匹配工具练习

什么是VisionPro&#xff1f; 在认识VisionPro之前我们需要先熟悉一下图片的各种格式 这里我们可以参考来自githubcurry博主的文章 图片各种格式的区别以及计算机如何存储图片 VisionPro 是由世界领先的机器视觉公司 Cognex 开发的一款专业机器视觉软件。它提供了强大的图像…

【一】【SQL Server】如何运用SQL Server中查询设计器通关数据库期末查询大题

职工考勤20170320 职工考勤20170320数据库展示 职工考勤表展示 职务代码表展示 一、基本操作 代码方式&#xff1a; --第一大题、基本操作 ALTER TABLE [dbo].[职工考勤表] DROP COLUMN [照片];EXEC sp_rename dbo.职工考勤表.职工编号, 工号, COLUMN;ALTER TABLE 职工考勤表 A…