大白话解析LevelDB:LRUCache

文章目录

    • LRUCache 的实现思路
      • lru_ 链表
      • table_ 哈希表
      • in_use_ 链表
    • LRUCache 的代码实现
      • LRUCache 的定义
      • LRUHandle
        • LRUHandle::key, LRUHandle::hash, LRUHandle::value
        • LRUHandle::next_hash
        • LRUHandle::next, LRUHandle::prev
        • LRUHandle::charge
        • LRUHandle::in_cache
        • LRUHandle::refs
        • LRUHandle::deleter
        • LRUHandle::key_data, LRUHandle::key_length
      • HandleTable
        • HandleTable::Insert(LRUHandle* h)
        • HandleTable::Lookup(const Slice& key, uint32_t hash)
        • HandleTable::Remove(const Slice& key, uint32_t hash)
        • HandleTable::FindPointer(const Slice& key, uint32_t hash)
        • HandleTable::Resize()
      • LRUCache::Insert(const Slice& key, uint32_t hash, void* value, size_t charge, void (\*deleter)(const Slice& key, void* value))
      • LRUCache::Lookup(const Slice& key, uint32_t hash)
      • LRUCache::Release(Cache::Handle* handle)
      • LRUCache::Erase(const Slice& key, uint32_t hash)
      • LRUCache::Prune()

LRUCache是一个基于LRU(Least Recently Used)算法实现的Cache

Cache满了之后,再插入新的缓存项时,会将Cache中访问时间最早的缓存项移除,为新的缓存项腾出空间。

LRUCache 的实现思路

LRUCache由 3 个数据结构组成,2 个链表和 1 个哈希表:

class LRUCache {private:// ...// LRU 链表的 Dummy Head 节点。// LRU 链表中存放 refs == 1 && in_cache == true 的缓存项。// LRU 链表中,最新的缓存项是尾节点,最老的是头节点。LRUHandle lru_ GUARDED_BY(mutex_);// in_use 链表的 Dummy Head 节点。// in_use 链表中的缓存项是正在被客户端使用的,它们的引用次数 >= 2,in_cache==true。LRUHandle in_use_ GUARDED_BY(mutex_);// Cache 中所有缓存项的 Hash 表,用于快速查找缓存项。HandleTable table_ GUARDED_BY(mutex_);
};

我们先来看lru_链表和table_哈希表,这两个数据结构是LRUCache的核心,in_use_链表稍后再说。

lru_ 链表

lru_链表是一个双向链表,用于存放Cache中的缓存项。

链表节点用LRUHandle来表示(感觉如果叫做LRUNode的话会更好理解),lru链表的示意图如下:

   Older
<-----------------------------------------------------------------------------------------------++------------+             +------------+              +------------+              +------------+              +------------+
|            | +---------> |            |  +---------> |            |  +---------> |            |  +---------> |            |
|  LRUHandle |             |  LRUHandle |              |  LRUHandle |              |  LRUHandle |              |  LRUHandle |
|            | <---------+ |            |  <---------+ |            |  <---------+ |            |  <---------+ |            |
+------------+             +------------+              +------------+              +------------+              +------------+lru_
+------------------------------------------------------------------------------------------------>Younger

lru_链表的头节点是一个 Dummy Head 节点,不存放任何数据,只是用来简化链表的操作。

lru_链表的头部的节点,是访问时间最新的节点,而越靠近尾部的节点,访问时间越早。

当需要往Cache中插入新节点的时候,会使用头插法将该节点插入到lru_链表的头部。

如果Cache满了,需要移除一些老节点为新节点腾出空间,就会从lru_链表的尾部开始移除节点,直到空间足够插入新节点位置。

这样一来,往Cache中插入新节点就解决了,只需要 O ( 1 ) O(1) O(1)的时间往lru_链表的头部插入即可。

那么怎么从Cache中快速查找一个缓存项呢?这就需要用到table_哈希表了。

table_ 哈希表

table_哈希表是一个HandleTable,用于快速查找Cache中的缓存项。

LevelDB 设计的LRUHandle很巧妙,LRUHandle中存储了该缓存项的keyvaluehash等信息。

其中key是这个缓存项的唯一标识,hashkey的哈希值,value是缓存项的值。

使用table_.Lookup(key, hash)可以在 O ( 1 ) O(1) O(1)的时间复杂度内查找到key对应的缓存项在lru_链表中的位置。

也就是说,往Cache中插入一个Key-Value时,会构建出一个LRUHandle插入到lru_链表的头部,同时会在table_哈希表中插入{key, LRUHandle}

这样在查找key对应的缓存项时,只需要在table_哈希表中查找即可,不需要遍历整个lru_链表。

如果要从Cache中删除某个key对应的缓存项,也只需要在table_哈希表中查找到key对应的LRUHandle所在位置,然后从lru_链表中移除即可。

如此一来,通过table_哈希表和lru_链表的相互配合,就已经可以实现一个高效的LRUCache了,其增删改查的时间复杂度都是 O ( 1 ) O(1) O(1)

那么in_use_链表是干什么的呢?

in_use_ 链表

lru_链表一样,in_use_链表也是一个双向链表,用于存放Cache中的缓存项。

什么样的缓存项会被放到in_use_链表中呢?

我们来看这样一个场景:

LRUCache* cache里有{LRUHandle_1, LRUHandle_2, LRUHandle_3}三个缓存项,他们的Key分别为"key1", "key2", "key3",且引用计数都为1

此时客户端从cache里获取了key1key2的缓存项:

LRUHandle* lruhandle_1 = cache->Lookup("key1");
LRUHandle* lruhandle_2 = cache->Lookup("key2");// lruhandle_1 和 lruhandle_2 正在被客户端使用..

会让lruhandle_1lruhandle_2的引用计数加一,此时lruhandle_1lruhandle_2的引用计数都变为了2,那么他们就会从lru_链表中移出来,放到in_use_链表中。

lruhandle_1lruhandle_2被客户端使用完毕后,通过LRUCache::Release(lruhandle)方法将他们的引用计数减一。

cache->Release(lruhandle_1);
cache->Release(lruhandle_2);

此时lruhandle_1lruhandle_2的引用计数都变回为了1,会从in_use_链表中移出来,又放回到lru_链表中。

in_use_链表的作用是让我们能清晰的知道哪些缓存项是正在被客户端使用的,哪些是在Cache中但是没有正在被使用,这样可以实现更精细的缓存策略。

比如在LRUCache::Prune()方法中,可以将所有没有正在被使用的缓存项从Cache中移除。

LRUCache 的代码实现

LRUCache 的定义

我们先来看下LRUCache的定义,都有哪些公共接口:

class LRUCache {public:LRUCache();~LRUCache();// 设置 Cache 的容量。// 当插入一条缓存项使得 Cache 的总大小超过容量时,会将最老(访问时间最早)的缓存项移除。void SetCapacity(size_t capacity) { capacity_ = capacity; }// 插入一个缓存项到 Cache 中,同时注册该缓存项的销毁回调函数。// key: 缓存项的 key// hash: key 的 hash 值,需要客户端自己计算// value: 缓存数据的指针// charge: 缓存项的大小,需要客户端自己计算,因为缓存项里只存储了缓存数据的指针// deleter: 缓存项的销毁回调函数Cache::Handle* Insert(const Slice& key, uint32_t hash, void* value, size_t charge,void (*deleter)(const Slice& key, void* value));// 根据 key 和 hash 查找缓存项。Cache::Handle* Lookup(const Slice& key, uint32_t hash);// 将缓存项的引用次数减一。void Release(Cache::Handle* handle);// 将缓存项从 Cache 中移除。void Erase(const Slice& key, uint32_t hash);// 移除 Cache 中所有没有正在被使用的缓存项,也就是引用计数为 1 的那些。void Prune();// 返回 Cache 里所有缓存项的总大小,也就是 Cache 的占用的内存空间。size_t TotalCharge() const {MutexLock l(&mutex_);return usage_;} private:// LRUCache 的 3 个核心数据结构:// LRU 链表的 Dummy Head 节点。// LRU 链表中存放 refs == 1 && in_cache == true 的缓存项。// LRU 链表中,最新的缓存项是尾节点,最老的是头节点。LRUHandle lru_ GUARDED_BY(mutex_);// in_use 链表的 Dummy Head 节点。// in_use 链表中的缓存项是正在被客户端使用的,它们的引用次数 >= 2,in_cache==true。LRUHandle in_use_ GUARDED_BY(mutex_);// Cache 中所有缓存项的 Hash 表,用于快速查找缓存项。HandleTable table_ GUARDED_BY(mutex_);
};

LRUHandle

LRUHandleLRUCache的核心数据结构,用于表示Cache中的缓存项,把它叫做LRUNode可能更好理解一些,lru_链表和in_use_链表都是由若干个LRUHandle节点组成的。

忘记lru_链表长什么样的同学可以回头看下 lru_ 链表的示意图

在往下看LRUCache各个接口的实现之前,我们先来看下LRUHandle的定义:

struct LRUHandle {void* value;void (*deleter)(const Slice&, void* value);LRUHandle* next_hash; // 如果两个缓存项的 hash 值相同,那么它们会被放到一个 hash 桶中,next_hash 就是桶里的下一个缓存项LRUHandle* next; // LRU 链表中的下一个(更新的)缓存项LRUHandle* prev; // LRU 链表中的上一个(更旧的)缓存项size_t charge;  // 该缓存项的大小size_t key_length; // key 的长度bool in_cache;     // 该缓存项是否还在 Cache 中uint32_t refs;     // 引用次数 uint32_t hash;     // key 的 hash 值char key_data[1];  // keySlice key() const {// next_ is only equal to this if the LRU handle is the list head of an// empty list. List heads never have meaningful keys.assert(next != this);return Slice(key_data, key_length);}
};
LRUHandle::key, LRUHandle::hash, LRUHandle::value

LRUHandle::key是该缓存项的KeyLRUHandle::hashkey的哈希值,而LRUHandle::value是是缓存数据的指针。

LRUHandle::valuevoid*类型的,可以存储任意类型的数据,客户端需要自己管理它的生命周期。

LRUHandle::next_hash

LRUHandle::next_hash可能会有同学还没搞懂,它是用来解决哈希冲突的。

前面我们讲到,LRUCachetable_哈希表和lru_链表组成。

当我们往LRUCache中插入一个缓存项LRUHandle时,会将该LRUHandlelru_链表里插入,同时也会往table_哈希表里插入{key, LRUHandle}

示意图如下:

+-----------------------------------------------------------------------------------+
|   +----------------+  +----------------+ +----------------+ +----------------+    |
|   |    Bucket1     |  |    Bucket2     | |    Bucket3     | |    Bucket4     |    |
|   |                |  |                | |                | |                |    |
|   | +------------+ |  | +------------+ | | +------------+ | | +------------+ |    |
|   | | LRUHandle1 | |  | | LRUHandle4 | | | | LRUHandle5 | | | | LRUHandle6 | |    |
|   | +------------+ |  | +------------+ | | +------------+ | | +------------+ |    |
|   |       |next_hash  |                | |                | |                |    |
|   |       |        |  |                | |                | |                |    |
|   | +-----v------+ |  |                | |                | |                |    |
|   | | LRUHandle2 | |  |                | |                | |                |    |
|   | +------------+ |  |                | |                | |                |    |
|   |       |next_hash  |                | |                | |                |    |
|   |       |        |  |                | |                | |                |    |
|   | +-----v------+ |  |                | |                | |                |    |
|   | | LRUHandle3 | |  |                | |                | |                |    |
|   | +------------+ |  |                | |                | |                |    |
|   +----------------+  +----------------+ +----------------+ +----------------+    |
|                                                                                   |
|                                  LRUCache::table_                                 |
+-----------------------------------------------------------------------------------+

假设LRUHandle1LRUHandle2LRUHandle3Key互不相同,分别为key1, key2, key3,但它们的哈希值恰好都是1,那么它们会被一起放到Bucket1的链表中,然后用next_hash依次连起来。

当我们要在table_寻找Keykey2LRUHandle时,会先计算key2的哈希值,找到对应的Bucket,也就是Bucket1

LRUHandle::next, LRUHandle::prev

LRUHandle::nextLRUHandle::prevLRUHandle的双向链表指针,用于构成lru_链表,这应该比较好理解,见下图。

+------------+      next    +------------+
|            |  +---------> |            |
|  LRUHandle |              |  LRUHandle |
|            |  <---------+ |            |
+------------+      prev    +------------+
LRUHandle::charge

由于LRUHandle中只存储了value的指针,无法自己计算出value的大小,所以需要客户端自己计算出value的大小,然后记录到LRUHandle::charge中。

LRUHandle::in_cache

LRUHandle::in_cachetrue,则表示该缓存项还在Cache中,可能在lru_链表中,也可能在in_use_链表中。

LRUHandle::refs

该缓存项的引用次数,当引用次数为1时,表示该缓存项还在Cache中,但是没有正在被客户端使用。

当引用次数大于1时,表示该缓存项正在被客户端使用。

LRUHandle::deleter

当该缓存项被移出Cache时,会查看下该缓存项的引用计数是否为1,如果是的话,会调用LRUHandle::deleter来销毁缓存项中的缓存数据,也就是value

如果移出时引用计数不为1,那么暂时先不调用LRUHandle::deleter来将value销毁,因为还有客户端在使用这个缓存项。

当客户端使用完毕后,会调用LRUCache::Release(LRUHandle*)来将引用计数减一,当引用计数减为0了,则调用LRUHandle::deleter来销毁value

LRUHandle::key_data, LRUHandle::key_length

以数组的方式存储keyLRUHandle::key_data存储key的内容,LRUHandle::key_length存储key的长度。

但为什么key_data[1]的长度只有1呢?这是 C/C++ 的一个常用技巧,感兴趣的可以移步翻看柔性数组。

HandleTable

了解完LRUHandle之后,在看LRUCache的实现之前,我们还需要来看下HandleTable的定义。

前面我们说过,LRUCache的核心数据结构是lru_链表和table_哈希表,table_哈希表是由HandleTable实现的。

HanldeTable是一个用数组实现的哈希表,数组里存放的是LRUHandle*LRUHandle的指针。

class HandleTable {// ...
private:uint32_t length_;   // 哈希表数组 list_[] 的大小uint32_t elems_;    // 哈希表中存放的元素个数LRUHandle** list_; // 哈希表的数组 list_[]
};

为什么哈希表中装的是LRUHandle*而不是LRUHandle呢?

节省空间呀,lru_链表已经存了LRUHandle了,table_哈希表只需要存LRUHandle的指针就行了。

我们来看下 HandleTable 的核心接口:

class HandleTable {public:HandleTable() : length_(0), elems_(0), list_(nullptr) { Resize(); }~HandleTable() { delete[] list_; }LRUHandle* Lookup(const Slice& key, uint32_t hash);// 插入一个新的 LRUHandle, 返回一个和这个新 LRUHandle 相同 Key 的老 LRUHandle,// 如果存在的话。LRUHandle* Insert(LRUHandle* h);// 从哈希表中移除一个指定 Key 的 LRUHandle。LRUHandle* Remove(const Slice& key, uint32_t hash);
};
HandleTable::Insert(LRUHandle* h)

先查找待插入项h在哈希表中的待插入位置,然后对该位置使用反引用赋值。

假设h应该插入到list_[i]的位置,通过LRUHandle** ptr = FindPointer(h->key(), h->hash)获取到list_[i]的地址,然后对*ptr进行赋值。

*ptr = h相当于list_[i] = h,这样就完成了h的插入。

LRUHandle* Insert(LRUHandle* h) {// 找到 key 对应的 LRUHandle* 在 Hash 表中的位置。// 如果哈希表中存在相同 key 的缓存项,那么返回老的 LRUHandle* // 在 Hash 表中的位置。// 如果哈希表中不存在相同 key 的缓存项,那么返回新的 LRUHandle*// 需要插入到 Hash 表中的位置。LRUHandle** ptr = FindPointer(h->key(), h->hash);// 先把老的 LRUHandle* 保存下来,最后返回给客户端。LRUHandle* old = *ptr;// 如果 old 存在,就用新的 LRUHandle* 替换掉 old。h->next_hash = (old == nullptr ? nullptr : old->next_hash);*ptr = h;if (old == nullptr) {/// 如果 old 不存在,表示哈希表中需要新插入一个 LRUHandle*。// 此时需要更新哈希表的元素个数,如果元素个数超过了哈希表的长度,// 则需要对哈希表进行扩容。++elems_;if (elems_ > length_) {Resize();}}return old;
}
HandleTable::Lookup(const Slice& key, uint32_t hash)

使用FindPointer来查找key对应的LRUHandle*在哈希表中的位置。

LRUHandle* Lookup(const Slice& key, uint32_t hash) { return *FindPointer(key, hash); }
HandleTable::Remove(const Slice& key, uint32_t hash)
LRUHandle* Remove(const Slice& key, uint32_t hash) {// 找到 key 对应的 LRUHandle* 在 Hash 表中的位置。LRUHandle** ptr = FindPointer(key, hash);LRUHandle* result = *ptr;if (result != nullptr) {// 如果找到了,那么需要将该 LRUHandle* 从 Hash 表中移除,// 并且更新哈希表的元素个数。*ptr = result->next_hash;--elems_;}return result;
}
HandleTable::FindPointer(const Slice& key, uint32_t hash)

FindPointerHandleTable的核心方法,用于查找key对应的LRUHandle*在哈希表中的位置。

LRUHandle** FindPointer(const Slice& key, uint32_t hash) {// key 的 hash 值模上哈希表的长度,得到 key 在哈希表中的位置。// 这个位置其实是哈希冲突链表的头节点,遍历这个冲突链表,找到// key 对应的 LRUHandle*。LRUHandle** ptr = &list_[hash & (length_ - 1)];while (*ptr != nullptr && ((*ptr)->hash != hash || key != (*ptr)->key())) {ptr = &(*ptr)->next_hash;}return ptr;
}
HandleTable::Resize()

往哈希表中插入新元素后,如果哈希表的元素个数超过了哈希表的长度,那么需要对哈希表进行扩容。

创建一个新哈希表,大小是老哈希表的两倍,然后将老哈希表中的所有元素逐一 hash 到新哈希表中。最后销毁掉老哈希表,用新哈希表替换掉老哈希表。

void Resize() {// 哈希表扩容后的最小长度是 4uint32_t new_length = 4;// 将哈希表的长度以指数增长的方式扩大,// 一直扩大到可以容纳下哈希表里的所有// 元素为止。 while (new_length < elems_) {new_length *= 2;}// 创建一张新哈希表,将老哈希表里的所有元素逐一 hash 到新哈希表中。LRUHandle** new_list = new LRUHandle*[new_length];memset(new_list, 0, sizeof(new_list[0]) * new_length);uint32_t count = 0;for (uint32_t i = 0; i < length_; i++) {LRUHandle* h = list_[i];while (h != nullptr) {// 如果存在 hash 冲突,那么将冲突的 LRUHandle* 插入到冲突链表的尾部。LRUHandle* next = h->next_hash;uint32_t hash = h->hash;LRUHandle** ptr = &new_list[hash & (new_length - 1)];h->next_hash = *ptr;*ptr = h;h = next;count++;}}assert(elems_ == count);// 销毁老哈希表,用新哈希表替换掉老哈希表。delete[] list_;list_ = new_list;length_ = new_length;
}

LRUCache::Insert(const Slice& key, uint32_t hash, void* value, size_t charge, void (*deleter)(const Slice& key, void* value))

Cache::Handle* LRUCache::Insert(const Slice& key, uint32_t hash, void* value, size_t charge,void (*deleter)(const Slice& key, void* value)) {MutexLock l(&mutex_);// 构造一个 LRUHandle 节点LRUHandle* e = reinterpret_cast<LRUHandle*>(malloc(sizeof(LRUHandle) - 1 + key.size()));e->value = value;e->deleter = deleter;e->charge = charge;e->key_length = key.size();e->hash = hash;e->in_cache = false;// 提前把引用计数先加一,因为 Insert 结束后需要把创建出来的 LRUHandle 地址// 返回给客户端,客户端对该 LRUHandle 的引用需要加一。e->refs = 1;  // std::memcpy(e->key_data, key.data(), key.size());// 如果打开数据库时配置了禁止使用 Cache,则创建出来的 Cache Capacity 就会是 0。if (capacity_ > 0) {// 这里的引用计数加一表示该 LRUHandle 在 Cache 中,是 Cache 对 LRUHandle// 的引用。e->refs++;  // e->in_cache = true;// 把 LRUHandle 节点按照 LRU 的策略插入到 in_use_ 链表中。LRU_Append(&in_use_, e);usage_ += charge;// 把 LRUHandle 节点插入到 Hash 表中。// 如果存在相同 key 的缓存项,那么`table_.Insert(e)`会返回老的缓存项。// 如果存在老的缓存项,那么需要将老的缓存项从 Cache 中移除。FinishErase(table_.Insert(e));} else {// capacity_ == 0 表示禁止使用 Cache,所以这里不需要把 LRUHandle 节点插入到// 链表中。e->next = nullptr;}// 如果插入新的 LRUHandle 节点后,Cache 的总大小超过了容量,那么需要将最老的// LRUHandle 节点移除,直到 Cache 的总大小不溢出容量。while (usage_ > capacity_ && lru_.next != &lru_) {// +->oldest <-> youngest <-> lru_<-+// +--------------------------------+LRUHandle* old = lru_.next;assert(old->refs == 1);bool erased = FinishErase(table_.Remove(old->key(), old->hash));if (!erased) {  // 防止编译报 Warning: unused variableassert(erased);}}return reinterpret_cast<Cache::Handle*>(e);
}

LRUCache::Lookup(const Slice& key, uint32_t hash)

Cache::Handle* LRUCache::Lookup(const Slice& key, uint32_t hash) {MutexLock l(&mutex_);// 到 Hash 表中查找 key 对应的缓存项指针。LRUHandle* e = table_.Lookup(key, hash);// 如果找到了缓存项,那么需要将缓存项的引用次数加一,// 然后返回该缓存项指针。if (e != nullptr) {Ref(e);}return reinterpret_cast<Cache::Handle*>(e);
}

LRUCache::Release(Cache::Handle* handle)

void LRUCache::Release(Cache::Handle* handle) {MutexLock l(&mutex_);// 将缓存项的引用次数减一。Unref(reinterpret_cast<LRUHandle*>(handle));
}

Unref(LRUHandle*)的实现如下:

void LRUCache::Unref(LRUHandle* e) {assert(e->refs > 0);// 将缓存项的引用次数减一。e->refs--;if (e->refs == 0) {  // Deallocate.// 如果引用计数减少后为 0,调用 deleter 销毁该缓存项。assert(!e->in_cache);(*e->deleter)(e->key(), e->value);free(e);} else if (e->in_cache && e->refs == 1) {// No longer in use; move to lru_ list.//// 如果引用计数减少后为 1,表示该缓存项已经没有正在使用的客户端了,// 那么需要将该缓存项从 in_use_ 链表中移除,然后插入回 lru_ 链表中。LRU_Remove(e);LRU_Append(&lru_, e);}
}

LRU_Remove(e)的含义是把e从所在链表移除。如果ein_use_链表中,那么就从in_use_链表中移除,如果elru_链表中,那么就从lru_链表中移除。

LRUCache::Erase(const Slice& key, uint32_t hash)

void LRUCache::Erase(const Slice& key, uint32_t hash) {MutexLock l(&mutex_);// 先从 Hash 表中移除 key 对应的缓存项,然后调用 FinishErase// 将缓存项从 Cache 中移除。FinishErase(table_.Remove(key, hash));
}

FinishErase(LRUHandle*)的实现如下:

bool LRUCache::FinishErase(LRUHandle* e) {if (e != nullptr) {assert(e->in_cache);// 将缓存项 e 从 in_use_ 或 lru_ 链表中移除。LRU_Remove(e);e->in_cache = false;usage_ -= e->charge;// 将引用计数减一,如果减一后为零,则销毁该缓存项。Unref(e);}return e != nullptr;
}

LRUCache::FinishErase(e)LRUCache::Erase(key, hash)的不同之处是:

  • LRUCache::FinishErase(e)不负责将 e 从 table_ 中移除,
  • LRUCache::Erase(key, hash)负责。

LRUCache::Prune()

void LRUCache::Prune() {MutexLock l(&mutex_);// 遍历 lru_ 链表,将该链表上的所有缓存项从 Cache 中移除。while (lru_.next != &lru_) {LRUHandle* e = lru_.next;assert(e->refs == 1);bool erased = FinishErase(table_.Remove(e->key(), e->hash));if (!erased) {assert(erased);}}
}

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

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

相关文章

【Elasticsearch查询】查询环境

文章目录 Search查询环境routing&#xff08;路由&#xff09;自适应选择副本策略全局检索超时检索取消并发搜索terminate_aftersearch_typepreferencebatched_reduce_sizeSource禁止_source字段metrics用例数据从source中筛选字段查询 Storestored_fields映射查询 track_score…

从单体服务到微服务:多模式 Web 应用开发记录<三>预初始化属性

相关文章&#xff1a; 多模式 Web 应用开发记录<一>背景&全局变量优化多模式 Web 应用开发记录<二>自己动手写一个 Struts 开头先看一个简单的例子&#xff0c;这是 ftl 文件的一个表单&#xff1a; <form id"validateForm" action"#&quo…

【第十天】C++函数对象/仿函数、谓词、适配器及常见algorithm算法

一、函数对象 重载了函数调用运算符()的类 实例化的对象叫函数对象&#xff0c;也叫仿函数。 如果函数对象 有一个参数 叫&#xff1a;一元函数对象/仿函数如果函数对象 有二个参数 叫&#xff1a;二元函数对象/仿函数如果函数对象 有三个及以上参数 叫&#xff1a;多元函数对…

windows下基于docker-desktop 安装 mysql 5.7

0.背景 docker-desktop v4.27.1Windows 11 22H2 docker-desktop 需要wsl的支持,一般win11新机子都默认开启了,安装docker-desktop 一路next即可.win10老版本需要自行安装开启wsl.一个小建议是,如果你的win10安装wsl时各种错误无法解决,建议升级到win11大概率解决,本人亲身经历…

K8S存储卷与PV,PVC

一、前言 Kubernetes&#xff08;K8s&#xff09;中的存储卷是用于在容器之间共享数据的一种机制。存储卷可以在多个Pod之间共享数据&#xff0c;并且可以保持数据的持久性&#xff0c;即使Pod被重新调度或者删除&#xff0c;数据也不会丢失。 Kubernetes支持多种类型的存储卷…

Three.js-03Vite打包入门

1.安装 说明&#xff1a;创建文件以后&#xff0c;按照提示进行操作。如cd文件夹&#xff0c;npm i ,npm run dev等操作。 npm create vitelatest 2.安装three npm i three.js 4.打开npm官网 说明:搜索three第三方库。按照案例进行操作。 5.修改App.vue文件 <script set…

2024年随想

今天2月最后一天了&#xff0c;明天就是3月了&#xff0c;年也正式算过完了吧。我也到了新的单位工作。新的开始新的工作。很高兴下个月&#xff0c;我自己的小孩就要出生了&#xff0c;我也要正式成为一名父亲了。2024年加油。

Win10Ubuntu22.04+FRP0.54搭建内网穿透

1. 下载FRP安装包&#xff0c;FRP为开源软件&#xff0c;在GITHUB可以直接下载&#xff08;选择当前最新的0.54版本&#xff09;&#xff1a;https://github.com/fatedier/frp/releases 下载windows版本安装包&#xff1a;frp_0.54.0_windows_amd64.zip 下载Linux版本安装包&am…

swift 监听状态栏frame变化

针对状态栏高度做一些操作 import UIKitclass ViewController: UIViewController {private var statusBarFrameObservation: NSKeyValueObservation?override func viewDidLoad() {super.viewDidLoad()if let statusBarManager UIApplication.shared.windows.first?.window…

golang的接口探索

1、接口是什么? 在Go语言中,接口是一种类型。抽象地定义了一组方法签名,但不实现这些方法。接口类型指定了一个值应该有哪些方法,因此,任何具有这些方法的类型都自动满足该接口。 在 Go 语言的语义上,只要某个类型实现了所定义的一组方法集,则就认为其就是同一种类型,…

Rocky Linux 运维工具 chown

一、chown 的简介 chown 用于更改文件或目录的所有者&#xff08;owner&#xff09;身份。通过 chown 命令&#xff0c;可以将文件或目录的所有权转移给另一个用户或组&#xff0c;从而控制对该文件或目录的访问和权限。 二、chown 的参数说明 使用语法&#xff1a;chown [新所…

算法刷题day19

目录 引言一、因数平方和二、爬树的甲壳虫三、改变数组元素 引言 这几道题主要都是考察数学&#xff0c;如果数学学的不好&#xff0c;推不出来公式&#xff0c;或者是你的数学思维不好都是做不出来的&#xff0c;所以说数学真的很重要&#xff0c;不过其实能考的数学也就那几…

手撕LRU缓存——LinkedHashMap简易源码

题目链接&#xff1a;https://leetcode.cn/problems/lru-cache/description/?envTypestudy-plan-v2&envIdtop-100-liked 原理非常简单&#xff0c;一个双端链表配上一个hash表。 首先我们要知道什么是LRU就是最小使用淘汰。怎么淘汰&#xff0c;链表尾部就是最不常用的直接…

Google大模型Bard更名Gemini,现在实力如何?(VS gpt系列)

名人说&#xff1a;一花独放不是春&#xff0c;百花齐放花满园。——《增广贤文》 作者&#xff1a;Code_流苏(CSDN)&#xff08;一个喜欢古诗词和编程的Coder&#x1f60a;&#xff09; 目录 一、简要介绍1、Gemini是什么&#xff1f;2、主要特点3、Gemini的版本4、应用潜力5、…

zookeeper启动报错

启动zookeeper报错 从报错中可以看到 Invalid config, exiting abnormally 意思是&#xff1a;配置无效&#xff0c;异常退出 在往上看是没有zoo.cof这个配置文件 2024-02-27 14:47:03,285 [myid:] - ERROR [main:o.a.z.s.q.QuorumPeerMain99] - Invalid config, exiting…

基于AMDGPU-ROCm的深度学习环境搭建

在风起云涌的AI江湖&#xff0c;NVIDIA凭借其CUDA生态和优秀的硬件大杀四方&#xff0c;立下赫赫战功&#xff0c;而另一家公司AMD也不甘示弱&#xff0c;带着他的生态解决方案ROCm开始了与不世出的NVIDA的正面硬钢&#xff0c;"ROCm is the answer to CUDA", AMD官网…

2月29日,每日信息差

&#x1f396; 素材来源官方媒体/网络新闻 &#x1f384; 小米汽车门店开建&#xff0c;首批销售网点至少六十家 &#x1f30d; 中国民航局&#xff1a;到2035年我国将建成航空运输强国 &#x1f30b; 中国石化&#xff1a;已累计建成充电站超6000座 &#x1f381; ofo 被强制执…

前端监控及搭建前端监控

前端监控怎么做&#xff1f; 前端也需要监控&#xff1f;技术人不可忽视的前端监控最全指南 一.前端监控的目的 为什么要进行前端监控&#xff1f; 是一种用于捕获&#xff0c;分析和报告网站或应用程序中存在异常&#xff0c;错误和性能问题的方法。通过监控&#xff0c;…

java界面代码

package day02;import javax.swing.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener;public class lei4j extends JFrame implements ActionListener {//创建文本框JTextField tiaomu new JTextField(40);//创建密码文本框JPasswordField JK ne…

Java——数组的定义与使用

目录 一.数组的基本概念 1.什么是数组 2.数组的创建及初始化 3.数组的使用 二.数组是引用类型 1.初始JVM的内存分布 2.基本类型变量与引用类型变量的区别 3.再谈引用变量 4.认识 null 三.数组的应用场景 1.保存数据 2.作为函数的参数 2.1参数传基本数据类型 2.…