Skip to content

红黑树+小根堆实现的基于优先级的本地缓存#20

Merged
flycash merged 18 commits into
ecodeclub:mainfrom
KelipuTe:main
Oct 13, 2023
Merged

红黑树+小根堆实现的基于优先级的本地缓存#20
flycash merged 18 commits into
ecodeclub:mainfrom
KelipuTe:main

Conversation

@KelipuTe
Copy link
Copy Markdown
Contributor

红黑树+小根堆实现的基于优先级的本地缓存

缓存本体用红黑树,数据用缓存结点做一层封装
通过缓存结点可以得到一个权重值出来,这个权重值用小根堆处理

小根堆的结构是,权重作为排序,然后数据用一个优先级结点做一层封装
额外维护一个map,map[权重]优先级结点,解决小根堆查找时需要遍历的问题
小根堆和这个map,做一个封装

小根堆自带asc排序,至于desc排序,我内置了一个 (max int64)/2 作位权重最大值,作差就可以用小根堆实现desc

优先级结点里面有一个map用于保存缓存结点的指针,缓存结点里也会维护一个优先级结点的指针
双向的指针,方便删除缓存或者发生淘汰时,双向的查找

lru,lfu,内存大小,自定义优先级,暂时没想到一个统一的抽象能把这四个都塞进去

ekit 那边实现了一个小根堆结构,同时需要把internal里面的红黑树也放出来

@KelipuTe
Copy link
Copy Markdown
Contributor Author

KelipuTe commented Sep 25, 2023

....\ekit\queue\concurrent_array_blocking_queue.go:21:2: missing go.sum entry for module providing package golang.org/x/sync/semaphore (imported by github.com/ecodeclub/ekit/queue); to add:
go get github.com/ecodeclub/ekit/queue@v0.0.7
提示要执行go get,gomod和gosum变了,多了个golang.org/x/sync

基本没啥大的改动,优先级队列的peak、入队、出队三个操作可以无缝替换小根堆的查看堆顶、插入结点、提去堆顶三个操作

@flycash
Copy link
Copy Markdown
Contributor

flycash commented Sep 25, 2023

要先等 ekit 那边暴露了优先级队列,直接使用那边的优先级队列。

Copy link
Copy Markdown
Contributor

@flycash flycash left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

你是不是没有处理过期时间?支持过期?
这个实现看起来没特别大的问题,但是太复杂了。我想到一个比较简单的方式,可以简化实现,并且降低本身内存结构的消耗。

也就是保持两个结构,一个红黑树,放数据;一个是优先级队列,放优先级信息。

运作方式就是:

  • 过期时间控制采用懒惰 + 定时删除的策略。也就是在 GET 的时候会检测有没有过期,如果过期了的话直接返回不存在,同时执行红黑树上的删除。
  • 红黑树上的删除,就是把 Data 字段(也就是用户的数据)重置为 nil,对应的 key 也置为 nil。但是优先级队列上的不动,或者说是把这个节点标记位已经被删除。
  • 这时候,这种被删除的节点就只存在优先级队列了。
  • 当触发了淘汰策略的时候,就从优先级队列里面淘汰元素。遵循这种逻辑:不断从队列里面取出来元素,如果节点本身被标记位已经删除了,就下一个;否则就是真的淘汰了一个节点。
  • 启动一个 goroutine,遍历红黑树,懒惰删除过期数据。

Comment thread memory/rbtree_client.go Outdated
Comment thread memory/rbtree_client.go Outdated
Comment thread memory/rbtree_client.go Outdated
Comment thread memory/cache.go Outdated
Comment thread memory/cache_priority.go Outdated
Comment thread memory/cache_priority.go Outdated
Comment thread memory/cache_priority.go Outdated
Comment thread memory/cache_priority.go Outdated
Comment thread memory/rbtree_client.go Outdated
@KelipuTe
Copy link
Copy Markdown
Contributor Author

1、数据结构简化之后,逻辑确实简单多了。

2、lru和lfu这两种策略和优先级策略作用的对象不同,还是上面那个问题,priorityType这个字段我没想到好办法绕过去。

3、定时删除过期数据那里,怎么设置时间间隔呢?sleep一秒还是无限循环中间不停,不停总有点费cpu的感觉。

4、优先级队列的初始化大小还有set用的MapSet的初始化大小那两个init参数,option模式好像也解决不了用户不传的问题,还是说这里默认传个0也是没太大问题的

@codecov
Copy link
Copy Markdown

codecov Bot commented Sep 27, 2023

Codecov Report

Merging #20 (ae12d23) into main (cc85ba2) will increase coverage by 2.18%.
The diff coverage is 100.00%.

@@            Coverage Diff             @@
##             main      #20      +/-   ##
==========================================
+ Coverage   96.21%   98.39%   +2.18%     
==========================================
  Files           2        4       +2     
  Lines         264      624     +360     
==========================================
+ Hits          254      614     +360     
  Misses          7        7              
  Partials        3        3              
Files Coverage Δ
memory/priority/rbtree_cache_node.go 100.00% <100.00%> (ø)
memory/priority/rbtree_priority_cache.go 100.00% <100.00%> (ø)

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

Copy link
Copy Markdown
Contributor

@flycash flycash left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

创建一个 priority 的子包,然后把这个代码丢进去。而后,额外提供两个 New 方法,NewLRUTreeCache 和 NewLFUTreeCache。这样,就可以删除 WithPriorityType 这个选项。用户不能指定。用户通过 NewLRU 或者 LFU 创建的就是对应的缓存,而直接通过 RBTreePriority 创建的,就是普通的优先级。

Comment thread memory/rbtree_priority_cache.go Outdated
Comment on lines +32 to +42
ErrWrongPriorityType = errors.New("ecache: 错误的优先级类型")
ErrOnlyKVCanSet = errors.New("ecache: 只有 kv 类型的数据,才能执行 Set")
ErrOnlyKVCanGet = errors.New("ecache: 只有 kv 类型的数据,才能执行 Get")
ErrOnlyKVNXCanSetNX = errors.New("ecache: 只有 SetNX 创建的数据,才能执行 SetNX")
ErrOnlyKVCanGetSet = errors.New("ecache: 只有 kv 类型的数据,才能执行 GetSet")
ErrOnlyListCanLPUSH = errors.New("ecache: 只有 list 类型的数据,才能执行 LPush")
ErrOnlyListCanLPOP = errors.New("ecache: 只有 list 类型的数据,才能执行 LPop")
ErrOnlySetCanSAdd = errors.New("ecache: 只有 set 类型的数据,才能执行 SAdd")
ErrOnlySetCanSRem = errors.New("ecache: 只有 set 类型的数据,才能执行 SRem")
ErrOnlyNumCanIncrBy = errors.New("ecache: 只有数字类型的数据,才能执行 IncrBy")
ErrOnlyNumCanDecrBy = errors.New("ecache: 只有数字类型的数据,才能执行 DecrBy")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不着急做成公有的,可以都保持私有的。

@flycash
Copy link
Copy Markdown
Contributor

flycash commented Sep 27, 2023

有咩有可能提供一个 PriorityStrateg,把内部和 LRU,LFU 的操作都封装到里面?比如说更新优先级,或者获取优先级。

@flycash flycash removed the 等依赖 label Oct 4, 2023
@KelipuTe
Copy link
Copy Markdown
Contributor Author

KelipuTe commented Oct 4, 2023

把之前保存优先级数据的结构扩展了一下,把和优先级有关系的操作都从缓存结构里面抽出来整到里面去了,
但是lru和lfu那里,优先级是受缓存结构的get动作影响的,所以需要单独提供一个方法给缓存结构判断要不要重新设置优先级

@flycash
Copy link
Copy Markdown
Contributor

flycash commented Oct 5, 2023

我发现一个新的问题,就是在 ekit 的优先级队列里面,调整优先级并不会触发堆结构调整。这是不是说,你这里调整了优先级之后,其实效果不对?

@flycash
Copy link
Copy Markdown
Contributor

flycash commented Oct 5, 2023

应该是咩有效果。

@flycash
Copy link
Copy Markdown
Contributor

flycash commented Oct 5, 2023

如果是没有效果的话,就要把 LRU 和 LFU 删掉,后面再单独各自写一个。

@KelipuTe
Copy link
Copy Markdown
Contributor Author

KelipuTe commented Oct 5, 2023

ekit 的那个优先级队列直接改优先级数值是没用的。
我这里的逻辑是删掉之前的数据(变空结点,就留着不动),然后补一个新的结点(这里会触发小根堆重新排序)。
然后在触发淘汰的时候,再去处理优先级队列里面的空结点。
lru和lfu单独写了测试,这么操作是有用的。

Copy link
Copy Markdown
Contributor

@flycash flycash left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我感觉这种实现太复杂了,性价比不高。这里要改为:

  1. 不要支持 LRU 和 LFU,后面单独提供新的实现。
  2. 认为节点的优先级不会发生改变。即便用户乱搞,改变了也没关系,因为我们说按照优先级淘汰,就算是淘汰错了,也没有特别大的影响。从我之前使用优先级淘汰策略来看,的确不存在修改的场景。
  3. 对应的,这里面的 priority _node 和 priority_strategy 应该都用不上了

对了,把现有的代码放好,说不定将来我又需要了,还能找回来。

@KelipuTe
Copy link
Copy Markdown
Contributor Author

KelipuTe commented Oct 7, 2023

优先级结点priority _node和优先级策略priority_strategy这两个结构就直接移除了。

优先级的代码就都揉到缓存结构上去了,优先级队列里面装的直接就是缓存结点,缓存结点需要额外存一下本身的优先级,要不然每次都要重新算。

缓存过期自动清理逻辑需要删除的时候,因为优先级队列不支持随机删除,所以采用的是清空结点上的数据(key="",val是个any所以我给了struct{}{}),然后结点本身不动的策略。等到触发淘汰的时候再说,出队发现是空的就循环继续看下一个就好。

Comment thread memory/priority/rbtree_cache_node.go Outdated
Comment thread memory/priority/rbtree_priority_cache.go Outdated
Comment thread memory/priority/rbtree_cache_node.go
Comment thread memory/priority/rbtree_priority_cache.go Outdated
Comment thread memory/priority/rbtree_priority_cache.go Outdated
Comment thread memory/priority/rbtree_priority_cache.go Outdated
Comment thread memory/priority/rbtree_priority_cache.go Outdated
Comment thread memory/priority/rbtree_priority_cache.go Outdated
Comment thread memory/priority/rbtree_priority_cache.go Outdated
Comment thread memory/priority/rbtree_priority_cache.go Outdated
Copy link
Copy Markdown
Contributor

@flycash flycash left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice,只剩下一点点小问题了

Comment thread memory/priority/rbtree_priority_cache.go Outdated
Comment thread memory/priority/rbtree_priority_cache.go Outdated
Comment thread memory/priority/rbtree_priority_cache.go Outdated
Comment thread memory/priority/rbtree_priority_cache.go Outdated
Copy link
Copy Markdown
Contributor

@flycash flycash left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

后续一些小地方我再琢磨琢磨,自己改进。非常感谢

@flycash
Copy link
Copy Markdown
Contributor

flycash commented Oct 11, 2023

大哥,解决冲突!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants