W-TinyLFU 算法实现

前言

不同于常见的 LRU 或 LFU,Window TinyLFU 是一种非常高效的缓存设计方案。先来看下 LRU 和 LFU 算法的缺点:

LFU 缺点:

  • 需要为每个记录项维护频率信息,这将消耗大量的内存空间
  • 可能存在旧数据长期不被淘汰(一开始频繁访问该数据,后来不访问了,也不被淘汰占用缓存)

LRU 缺点:

  • 不能反馈访问频率,对热点数据的命中率不如 LFU

W-TinyLFU 综合了两者的长处:高命令率、低内存占用。

参考链接:

  • Caffeine 详解 —— Caffeine 的 Window TinyLfu
  • 论文《TinyLFU: A Highly Ecient Cache Admission Policy》阅读笔记

W-TinyLFU 的基础 TinyLFU

TinyLFU 是一种空间利用率很高的数据结构,可以在较大访问量的场景下近似的替代 LFU 的频率统计信息。

它的原理与 BloomFilter 类似,把多个 bit 位看作一个整体,用来统计一个 key 的使用频率,TinyLFU 中的 key 通过多次不同的 hash 计算映射到多个 bit 组。在读取时,取映射的所有值中的最小的值作为 key 的使用频率。

在 Caffeine 中,维护了一个 4-bit CountMinSketch 来记录 key 的使用频率,这意味着 key 最大使用频率为 15。

为了可以移除旧事件,TinyLFU 还采用一种衰减机制,借助 reset 操作:每次 get 数据时都会给计数器加上 1,当计数器到达一个尺寸 W 时,把所有记录的 Sketch 数值都除以 2。

W-TinyLFU 的窗口设计

然而在对同一对象的「稀疏突发」的场景下,TinyLFU 会出现问题。在这种情况下,突发的 key 无法建立足够的频率以保存在缓存中,从而导致不断的 cache miss。Caffeine 通过设计 W-TinyLFU 的策略(包含两个缓存区域)解决了这个问题。

主缓存(main cache)使用 SLRU 逐出策略和 TinyLFU 准入策略(TinyLFU 的准入和淘汰策略是:新增一个元素时,判断使用该元素替换一个旧元素,是否可以提升缓存命中率),而窗口缓存(window cache)采用 LRU 逐出策略而没有任何准入策略。

主缓存根据 SLRU 策略静态划分为 A1 和 A2 两个区域,80% 的空间分配给热门项目(A2),并从 20% 非热门项目中挑选 victim。所有请求的 key 都会被允许进入窗口缓存,而窗口缓存的 victim 则有机会进入主缓存(主缓存没满直接进入;满了的话,比较窗口缓存的 victim 和非热门项目的 victim 的出现频率,留下较高的内个)。

窗口缓存的大小初始为总缓存大小的 1%,主缓存的大小为 99%。

W-TinyLFU 方案如下所示:

W-TinyLFU

具体实现

LRU

以下是代码实现,只有重要部分:

这就是传统的 LRU,需要注意的是当满了再 add 时,没有直接将链表末尾元素删除,再将新元素添加到链表头部,而是采用了交换的方式,减少了内存的申请次数。

type windowLRU struct {data map[uint64]*list.Element // key 到相应元素的映射cap  int                      // lru 容量list *list.List
}type storeItem struct {stage    intkey      uint64value    interface{}
}// 向窗口 LRU 添加数据
func (lru *windowLRU) add(newitem storeItem) (eitem storeItem, evicted bool) {// 没满直接插入if lru.list.Len() < lru.cap {lru.data[newitem.key] = lru.list.PushFront(&newitem)return storeItem{}, false}// 满了的话需要淘汰链表最后一个evictItem := lru.list.Back()item := evictItem.Value.(*storeItem)// 在哈希表中删除淘汰的元素delete(lru.data, item.key)// 通过交换,链表最后一个元素内容变为要插入的 newitemeitem, *item = *item, newitemlru.data[item.key] = evictItemlru.list.MoveToFront(evictItem)return eitem, true
}func (lru *windowLRU) get(v *list.Element) {lru.list.MoveToFront(v)
}

SLRU

以下是代码实现,只有重要部分:

这是一个分段 LRU 实现,添加元素时先添加到非热门区域,当被 get 时再提升到热门区域。

type segmentedLRU struct {data                      map[uint64]*list.ElementstageColdCap, stageHotCap intstageCold, stageHot       *list.List
}const (STAGE_COLD = iotaSTAGE_HOTSTAGE_WINDOW
)func (slru *segmentedLRU) add(newitem storeItem) {// 默认插入到非热门区域newitem.stage = STAGE_COLD// 主缓存还没满,直接插入if slru.stageCold.Len() < slru.stageColdCap || slru.Len() < slru.stageColdCap+slru.stageHotCap {slru.data[newitem.key] = slru.stageCold.PushFront(&newitem)return}// 满了的话需要淘汰非热门链表的最后一个e := slru.stageCold.Back()item := e.Value.(*storeItem)// 在哈希表中删除淘汰的元素delete(slru.data, item.key)// 直接覆盖链表中的元素*item = newitemslru.data[item.key] = eslru.stageCold.MoveToFront(e)
}func (slru *segmentedLRU) get(v *list.Element) {item := v.Value.(*storeItem)// 已经在热门区域了,提到链表头部即可if item.stage == STAGE_HOT {slru.stageHot.MoveToFront(v)return}// 不在热门区域,但热门区域还有空间if slru.stageHot.Len() < slru.stageHotCap {slru.stageCold.Remove(v)item.stage = STAGE_HOTslru.data[item.key] = slru.stageHot.PushFront(item)return}// 此时,既不在热门区域,热门区域也没空间了// 需要将热门链表最后一个下放到非热门区域back := slru.stageHot.Back()bitem := back.Value.(*storeItem)*bitem, *item = *item, *bitembitem.stage = STAGE_HOTitem.stage = STAGE_COLDslru.data[item.key] = vslru.data[bitem.key] = backslru.stageCold.MoveToFront(v)slru.stageHot.MoveToFront(back)
}// 获取将淘汰的元素
func (slru *segmentedLRU) victim() *storeItem {if slru.Len() < slru.stageColdCap+slru.stageHotCap {return nil}v := slru.stageCold.Back()return v.Value.(*storeItem)
}

Count-Min Sketch

以下是代码实现,只有重要部分:

const (cmWidth = 4
)type cmSketch struct {rows [cmWidth]cmRowseed [cmWidth]uint64mask uint64
}func newCmSketch(numCounters int64) *cmSketch {// next2Power 获取最接近 numCounters 的 2 的幂numCounters = next2Power(numCounters)sketch := &cmSketch{mask: uint64(numCounters - 1)}source := rand.New(rand.NewSource(time.Now().UnixNano()))for i := 0; i < cmWidth; i++ {sketch.seed[i] = source.Uint64()sketch.rows[i] = newCmRow(numCounters)}return sketch
}func (s *cmSketch) Increment(hashed uint64) {for i := range s.rows {s.rows[i].increment((hashed ^ s.seed[i]) & s.mask)}
}// 获取出现次数近似值
func (s *cmSketch) Estimate(hashed uint64) int64 {min := byte(255)for i := range s.rows {val := s.rows[i].get((hashed ^ s.seed[i]) & s.mask)if val < min {min = val}}return int64(min)
}func (s *cmSketch) Reset() {for _, r := range s.rows {r.reset()}
}func (s *cmSketch) Clear() {for _, r := range s.rows {r.clear()}
}type cmRow []byte// 为每个计数分配 4 位
func newCmRow(numCounters int64) cmRow {return make(cmRow, numCounters/2)
}// 获取指定位置计数
func (r cmRow) get(n uint64) byte {// n/2 定位到相应字节// (n & 1) * 4 奇数为 4 偶数为 0return r[n/2] >> ((n & 1) * 4) & 0x0f
}// 增加指定位置计数
func (r cmRow) increment(n uint64) {i := n / 2s := (n & 1) * 4v := (r[i] >> s) & 0x0fif v < 15 {r[i] += 1 << s}
}// 减少为原来一半
func (r cmRow) reset() {for i := range r {r[i] = (r[i] >> 1) & 0x77}
}// 全部清空
func (r cmRow) clear() {for i := range r {r[i] = 0}
}

Cache

以下是代码实现,只有重要部分:

type Cache struct {lru       *windowLRUslru      *segmentedLRUc         *cmSketcht         int32 // 计数器threshold int32 // 用来记录临界值,当计数器等于临界值时调用 resetdata      map[uint64]*list.Element
}func NewCache(size int) *Cache {const lruPct = 1lruSz := (lruPct * size) / 100slruSz := int(float64(size) * ((100 - lruPct) / 100.0))slruO := int(0.2 * float64(slruSz))data := make(map[uint64]*list.Element, size)return &Cache{lru:  newWindowLRU(lruSz, data),slru: newSLRU(data, slruO, slruSz-slruO),c:    newCmSketch(int64(size)),data: data,}
}func (c *Cache) set(key, value interface{}) bool {keyHash := c.keyToHash(key)i := storeItem{stage:    STAGE_COLD,key:      keyHash,value:    value,}// 先向窗口 LRU 中插入eitem, evicted := c.lru.add(i)// 没有数据被淘汰,插入完成if !evicted {return true}// 有数据被淘汰,看主缓存中是否有数据被淘汰victim := c.slru.victim()// 没有的话,直接插入主缓存if victim == nil {c.slru.add(eitem)return true}// 此时说明主缓存满了,也有数据被淘汰// 比较窗口 lru 和 slru 淘汰的数据的频率,保留频率多的vcount := c.c.Estimate(victim.key)ocount := c.c.Estimate(eitem.key)if ocount < vcount {return true}c.slru.add(eitem)return true
}func (c *Cache) get(key interface{}) (interface{}, bool) {c.t++// 计数器到达临界时后,调用 Reset()if c.t == c.threshold {c.c.Reset()c.t = 0}keyHash := c.keyToHash(key)val, _ := c.data[keyHash]item := val.Value.(*storeItem)c.c.Increment(item.key)v := item.value// 调用相应部分的 get,提到链表的头部if item.stage == STAGE_WINDOW {c.lru.get(val)} else {c.slru.get(val)}return v, true
}

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

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

相关文章

让AI给你写代码,初体验(二)-写一个flask应用

这里我们准备让AI做一个稍微复杂一点任务&#xff0c;写一个前后应用&#xff0c;具体&#xff1a; 前台用html输入股票代码&#xff0c;后台通过akshare的接口程序获取该股票的实时价格&#xff0c;然后返回显示在html 我们先用AI对话看一下&#xff0c;AI会给我们什么编码建…

柯桥会计培训学校,会计职称考试,考中级会计怎么证明工作年限?

中级会计考试是会计从业人员的重要考试之一&#xff0c;对于中级考生来说&#xff0c;工作年限证明是必不可少的一步。因此&#xff0c;在考中级会计之前&#xff0c;需要对如何证明工作年限进行了解和掌握。 为大家整理了工作年限证明相关信息&#xff0c;一起来看看吧~ 一、…

Rocky Linux 运维工具 ls

一、ls 的简介 ​​ls​ 用于列出当前目录下的文件和目录&#xff0c;以及它们的属性信息。通过 ​ls​命令可以查看文件名、文件大小、创建时间等信息&#xff0c;并方便用户浏览和管理文件。 二、ls 的参数说明 序号参数描述1-a显示所有文件&#xff0c;包括以 ​.​开头的…

5G双域快网

目录 一、业务场景 二、三类技术方案 2.1、专用DNN方案 2.2、ULCL方案&#xff1a;通用/专用DNNULCL分流 2.3、 多DNN方案-定制终端无感分流方案 漫游场景 一、业务场景 初期双域专网业务可划分为三类业务场景&#xff0c;学校、政务、文旅等行业均已提出公/专网融合访问需…

【DDD】学习笔记-领域驱动设计对持久化的影响

资源库的实现 如何重用资源库的实现&#xff0c;以及如何隔离领域层与基础设施层的持久化实现机制&#xff0c;具体的实现还要取决于开发者对 ORM 框架的选择。Hibernate、MyBatis、jOOQ 或者 Spring Data JPA&#xff08;当然也包括基于 .NET 的 Entity Framework、NHibernat…

Acwing周赛记录

很难得参加一次周赛hhhhh这次参加的是第144场周赛&#xff0c;一共有三道题 AcWing 5473. 简单数对推理 给定两个整数数对&#xff0c;每个数对都包含两个 1∼9 之间的不同整数。 这两个数对恰好包含一个公共数&#xff0c;即恰好有一个整数同时包含于这两个数对。 给定这两…

选择排序,冒泡排序,插入排序,快速排序及其优化

目录 1 选择排序 1.1 原理 1.2 具体步骤 1.3 代码实现 1.4 优化 2 冒泡排序 2.1 原理 2.2 具体步骤 2.3 代码实现 2.4 优化 3 插入排序 3.1 原理 3.2 具体步骤 3.3 代码实现 3.4 优化 4. 快速排序 4.1 原理 4.2 具体步骤 4.3 代码实现 4.4 优化 为了讲…

Linux课程四课---Linux开发环境的使用(vim编辑器的相关)

作者前言 &#x1f382; ✨✨✨✨✨✨&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f382; ​&#x1f382; 作者介绍&#xff1a; &#x1f382;&#x1f382; &#x1f382; &#x1f389;&#x1f389;&#x1f389…

【MySQL】内置函数 -- 详解

一、日期函数 日期&#xff1a;年月日时间&#xff1a;时分秒 1、获得年月日 2、获得时分秒 3、获得时间戳 4、在日期的基础上加日期 5、在日期的基础上减去时间 6、计算两个日期之间相差多少天 7、获得当前时间 ⚪练习 &#xff08;1&#xff09;记录生日 &#xff08;2&…

Flask入门一(介绍、Flask安装、Flask运行方式及使用、虚拟环境、调试模式、配置文件、路由系统)

文章目录 一、Flask介绍二、Flask创建和运行1.安装2.快速使用3.Flask小知识4.flask的运行方式 三、Werkzeug介绍四、Jinja2介绍五、Click CLI 介绍六、Flask安装介绍watchdog使用python--dotenv使用&#xff08;操作环境变量&#xff09; 七、虚拟环境介绍Mac/linux创建虚拟环境…

家政按摩上门服务小程序搭建

家政按摩上门服务小程序支持技师入驻申请&#xff0c;用户可以通过在线下单预约家政服务&#xff0c;并根据距离、价格、销量好评度等条件进行筛选和选择。用户可以选择技师进行预约&#xff0c;并填写自己的服务地点和时间&#xff0c;享受上门服务。同时&#xff0c;技师也可…

数据分析-Pandas数据探查初步柱状图

数据分析-Pandas数据探查初步柱状图 数据分析和处理中&#xff0c;难免会遇到各种数据&#xff0c;那么数据呈现怎样的规律呢&#xff1f;不管金融数据&#xff0c;风控数据&#xff0c;营销数据等等&#xff0c;莫不如此。如何通过图示展示数据的规律&#xff1f; 数据表&am…

在Arcgis中删除过滤Openstreetmap道路属性表中指定highway类型道路

一、导出道路类型并分析 1. 导出道路类型 选中highway属性列&#xff0c;选择汇总→确定 2. 分析 用Excel打开输出表&#xff0c;包含的道路类型如下 0.空值’’ 车辆可行驶道路&#xff08;和bfmap的并集&#xff09; 空值&#xff08;无定义道路&#xff09; 二、…

基于Vue(提供Vue2/Vue3版本)和.Net Core前后端分离、强大、跨平台的快速开发框架

前言 今天大姚给大家推荐一款基于Vue&#xff08;提供Vue2/Vue3版本&#xff09;和.Net Core前后端分离、开源免费&#xff08;MIT License&#xff09;、强大、跨平台的快速开发框架&#xff0c;并且框架内置代码生成器&#xff08;解决重复性工作&#xff0c;提高开发效率&a…

element el-table表格内容宽度自适应,不换行,不隐藏

2024.2.27今天我学习了如何用el-table实现表格宽度的自适应&#xff0c;当我们动态渲染表格数据的时候&#xff0c;有时候因为内容太多会出现挤压换行的效果&#xff1a; 我们需要根据内容的最大长度设置动态的宽度&#xff0c;这边我在utils里面封装了一个js&#xff1a; //…

求两个向量之间的夹角

求两个向量之间的夹角 介绍Unity的API求向量夹角Vector3.AngleVector3.SignedAngle 自定义获取方法0-360度的夹角 总结 介绍 求两个向量之间的夹角方法有很多&#xff0c;比如说Unity中的Vector3.Angle&#xff0c;Vector3.SignedAngle等方法&#xff0c;具体在什么情况下使用…

高性能Server的基石:reactor反应堆模式

业务开发同学只关心业务处理流程。但是我们开发的程序都是运行服务端server上&#xff0c;服务端server接收到IO请求后&#xff0c;是如何处理请求并最终进入业务流程的呢&#xff1f;这里不得不提到reactor反应堆模型。reactor反应堆模型来源于大师Doug Lea在 《Sacalable io …

Unity中URP下实现水体(水面反射)

文章目录 前言一、原理1、法一&#xff1a;使用立方体纹理 CubeMap&#xff0c;作为反射纹理使用2、法二&#xff1a;使用反射探针生成环境反射图&#xff0c;所谓反射的采样纹理 二、实现水面反射1、定义和申明CubeMap2、反射向量需要什么3、计算 N ⃗ \vec{N} N 4、计算 V ⃗…

Mybatis | Mybatis的“入门程序“

Mybatis的入门程序 目录: Mybatis的入门程序一、查询数据根据表 “主键id” 查询数据模糊查询 二、添加数据三、更新数据四、删除数据 作者简介 &#xff1a;一只大皮卡丘&#xff0c;计算机专业学生&#xff0c;正在努力学习、努力敲代码中! 让我们一起继续努力学习&#xff0…

Freesia项目介绍

项目介绍 这是一个Spring Boot Vue的前后端分离项目&#xff0c;实现的是一个通用的后台管理系统。 框架使用 前端使用了layui-vue和layui-vue-admin&#xff0c;分别提供了组件和前端整体架构的支持。 后端使用Spring Boot框架管理 项目技术使用 前端 Layui-vue、Layui…