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

news2024/11/17 13:38:38

相信大家对于缓存这个词都不陌生,但凡追求高性能的业务场景,一般都会使用缓存,它可以提高数据的检索速度,减少数据库的压力。缓存大体分为两类:本地缓存和分布式缓存(如 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.coloradmin.cn/o/1492656.html

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

相关文章

论文目录3:大模型时代(2023+)

1 instruction tuning & in context learning 论文名称来源主要内容Finetuned Language Models Are Zero-Shot Learners2021 机器学习笔记&#xff1a;李宏毅ChatGPT Finetune VS Prompt_UQI-LIUWJ的博客-CSDN博客 早期做instruction tuning的work MetaICL: Learning to …

CSS文本样式值,web前端开发资料

正文 什么是行内元素&#xff1f; display属性为inline的元素为行内元素&#xff0c;英文&#xff1a;inline element&#xff0c;其中文叫法有多种&#xff0c;如&#xff1a;内联元素、内嵌元素、行内元素、直进式元素等。 特点&#xff1a; 和其他元素都在一行上&#x…

补点基础——几何尺寸和公差

几何尺寸与公差&#xff08;Geometric dimensioning and tolerancing&#xff09;代号&#xff1a;GD&T&#xff0c;主要是以下两个组织制定&#xff1a; 1.美国机械工程师学会&#xff08;ASME&#xff09; ASME Y14.5 是 GD & T 的完整定义&#xff0c;其中包含所有…

mac报错:zsh:command not found: brew

1、基本概述&#xff1f; 在使用brew安装程序的时候MAC提示&#xff1a; zsh:command not found: brew 本质就是brew没有安装&#xff0c;这个命令与linux系统中的yum命令类似。 使用的环境说明&#xff1a; 虚拟机版本&#xff1a;VMware Workstation 17 Pro mac os Ventu…

基于ERNIR3.0文本分类的开发实践

参考&#xff1a;基于ERNIR3.0文本分类&#xff1a;(KUAKE-QIC)意图识别多分类(单标签) - 飞桨AI Studio星河社区 (baidu.com) https://zhuanlan.zhihu.com/p/574666812?utm_id0 遇到的问题&#xff1a;如下 采用paddleNLP下文本分类实例进行分类训练后发现 生成的模型分类不…

组基轨迹建模 GBTM的介绍与实现(Stata 或 R)

基本介绍 组基轨迹建模&#xff08;Group-Based Trajectory Modeling&#xff0c;GBTM&#xff09;&#xff08;旧名称&#xff1a;Semiparametric mixture model&#xff09; 历史&#xff1a;由DANIELS.NAGIN提出&#xff0c;发表文献《Analyzing Developmental Trajectori…

【软件架构的常用分类及建模方法】

曾梦想执剑走天涯&#xff0c;我是程序猿【AK】 目录 简述概要知识图谱 简述概要 了解软件架构的常用分类及建模方法 知识图谱 1.1.2 软件架构的常用分类及建模方法 软件架构的常用分类 多年来&#xff0c;“架构”概念经过不断演化&#xff0c;目前已形成了满足不同用途的…

Meta AI最近推出的新模型,Searchformer

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

echarts柱状图可鼠标左击出现自定义弹框,右击隐藏弹框并阻止默认右击事件

每项x轴数据对应有两条柱图和一条阴影效果是学习其它博客得到的效果&#xff0c;这个是学习的原文链接&#xff1a;echarts两个合并柱体&#xff08;普通柱状图象形柱图&#xff09;共享一个柱体阴影 因为这次情况比较特殊&#xff0c;不仅需要自定义弹框内容&#xff0c;而且…

Python——与Matlab对应的Python版本

参考资料&#xff1a; Python——与Matlab对应的Python版本

Android高级工程师面试实战,三幅图给你弄懂EventBus核心原理

阿里技术一面-35min 自我介绍 Android 有没有遇到OOM问题(有遇到内存泄漏问题)Handler机制ThreadLocalActivity启动到加载View过程View绘制过程LinearLayout (wrap_content) & TextView (match_parent) 最终结果???OKHttp(1. 为什么选择它&#xff1f; 2. 性能了解不…

(C++练习)选择题+编程题

个人主页&#xff1a;Lei宝啊 愿所有美好如期而遇 选择题 以下程序输出结果是什么&#xff08;&#xff09; class A{public:virtual void func(int val 1){ std::cout<<"A->"<< val <<std::endl;}virtual void test(){ func();}};class B…

CSS标准文档流与脱离文档流,爆推

面试题&#xff1a; Html 1&#xff0c;html语义化 2&#xff0c;meta viewport相关 3&#xff0c;canvas 相关 CSS 1&#xff0c;盒模型 1.1&#xff0c;ie盒模型算上border、padding及自身&#xff08;不算margin&#xff09;&#xff0c;标准的只算上自身窗体的大小 c…

【SQL】指定日期的产品价格(IFNULL函数)

题目描述 leetcode题目&#xff1a;指定日期的产品价格 思路 找出所有的产品的指定的日期的价格&#xff1b;若找不到某个产品的更改日期&#xff0c;则将该产品价格设置为10。 关键点&#xff1a; if没有16号的&#xff0c;怎么找到前一个日期的&#xff1f;> 日期小…

flutter弹窗输入,Android学习的三个终极问题及学习路线规划

题库非常全面包括&#xff1a; Android基础知识: 基本涵盖Android所有知识体系&#xff0c;四大组件&#xff0c;Fragment,WebView,事件分发&#xff0c;View绘制…Java基础知识&高阶知识点: 基础部分不谈了&#xff0c;高阶部分&#xff1a;泛型&#xff0c;反射&#xff…

白皮书发布|超融合运行 K8s 的场景、功能与优势

目前&#xff0c;不少企业都使用虚拟化/超融合运行 Kubernetes 和容器化应用。一些用户可能会有疑惑&#xff1a;既然 Kubernetes 可以部署在裸金属上&#xff0c;使用虚拟化不是“多此一举”吗&#xff1f; 在电子书《IT 基础架构团队的 Kubernetes 管理&#xff1a;从入门到…

java 面试题总结

1锁粗化和锁消除&#xff0c;锁膨胀和锁升级的区别。 https://www.cnblogs.com/xuxinstyle/p/13387778.html .无锁 < 偏向锁 < 轻量级锁 < 重量级锁 &#xff0c;说的时候不要忘记说无锁状态 2.Map 的实现&#xff0c;线程安全的实现 1、ConcurrentHashMap在JDK 1.7…

【HarmonyOS】Dev Eco Studio4.0开发工具下载SDK10

目录 点击创建项目 选择空项目&#xff08;OpenHarmony&#xff09;&#xff0c;点击Next 此时SDK为10 点击 configure OpenHarmony SDK 创建一个新目录文件存放SDK&#xff0c;不要跟之前的SDK文件目录重合&#xff0c;点击Next 点击Next 勾选Accept&#xff0c;点…

【2024.03.05】定时执行专家 V7.1 发布 - TimingExecutor V7.1 Release

目录 ▉ 软件介绍 ▉ 新版本 V7.1 下载地址 ▉ V7.1 新功能 ▼2024-03-03 V7.1 - 更新日志 ▉ V7.0 新UI设计 ▉ 软件介绍 《定时执行专家》是一款制作精良、功能强大、毫秒精度、专业级的定时任务执行软件。软件具有 25 种【任务类型】、12 种【触发器】触发方式&#x…

docker安装ES和kibana

文章目录 一、安装Elasticsearch1. 安装Elasticsearch2. 安装IK分词器3. elasticsearch-head 监控的插件4. 配置跨域 二、安装kibana 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、安装Elasticsearch 1. 安装Elasticsearch 安装Elasticsearch参…