惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Apple Machine Learning Research
Apple Machine Learning Research
爱范儿
爱范儿
博客园_首页
博客园 - 【当耐特】
V
Visual Studio Blog
博客园 - 叶小钗
月光博客
月光博客
美团技术团队
J
Java Code Geeks
小众软件
小众软件
Y
Y Combinator Blog
博客园 - Franky
Martin Fowler
Martin Fowler
博客园 - 聂微东
Microsoft Azure Blog
Microsoft Azure Blog
IT之家
IT之家
MyScale Blog
MyScale Blog
人人都是产品经理
人人都是产品经理
Microsoft Security Blog
Microsoft Security Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
酷 壳 – CoolShell
酷 壳 – CoolShell
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
云风的 BLOG
云风的 BLOG

OhYee 博客

小鹏辅助驾驶测评|OhYee 博客 小鹏非支持手机开启自动解锁|OhYee 博客 使用函数计算实现 301 重定向|OhYee 博客 针对 HTML 内容使用 Ant Design 图片弹框|OhYee 博客 博客进程泄露及僵尸进程解决|OhYee 博客 蓝易云服务器体验|OhYee 博客 SSH 调起本地 VSCode|OhYee 博客 【2022 秋招内推】阿里云后端研发工程师|OhYee 博客 使用函数计算获取 IP 地址信息|OhYee 博客 正确获取客户端 IP/HTTP Header 也可能重复|OhYee 博客 评测 Oculus Quest2 及 BigScreen|OhYee 博客 NextJS 热重载保留状态|OhYee 博客 如何优雅地贴 gist 代码|OhYee 博客 Linux 精细化文件权限|OhYee 博客 VSCode 容器开发环境|OhYee 博客 Clash 的不兼容更新排查|OhYee 博客 Zeek 导出 PCAP|OhYee 博客 记一次 ssh 配置问题|OhYee 博客 Git Commit 规范化工具|OhYee 博客 谈谈《星之卡比-探索发现》|OhYee 博客 VSCode 快捷键绑定 Shell 命令|OhYee 博客 ASN.1 语法及 X.509 证书格式解析解析|OhYee 博客 腾讯企业邮箱忽略 MX 记录发信|OhYee 博客 Chrome/Edge 标签组插件|OhYee 博客 【应届内推】阿里云后端研发工程师|OhYee 博客 损坏的 Typecho 备份处理为 JSON|OhYee 博客 VS Code VIM 插件高效使用|OhYee 博客 SSH 正反向代理|OhYee 博客 Let's Encrypt 根证书过期引发的问题|OhYee 博客 OpenWRT 忽略内核依赖|OhYee 博客
LRU + Expired Map|OhYee 博客
2021-10-02 · via OhYee 博客

LRU + Expired Map

目前博客的很多地方都需要有一个临时的缓存机制,比如:

  • Markdown 渲染部分如果内容未改变,可以重用之前渲染的结果
  • 评论区发布部分添加一个定时过期记录,可以避免短时间内发出重复的评论
  • 头像获取接口,可以缓存一定数量的头像,避免每次都重新获取

这里对应的应该是两种删除机制

  • 通过 LRU 限定存储上限,如果超出上限则删除最近未被访问的内容
  • 设置过期时间,如果超过过期时间则删除

两种机制本质上都是一个最小堆,通过比较 LRU 的计数器或是时间戳即可

最小堆

借助 Go 自带的 container/heap 可以实现一个最小堆,在这里除去常规的内容外,还需要维护每个元素的下标。有了下标后,就可以实现删除指定的元素了。

插入时间复杂度为 O(logn)O(logn),删除时间复杂度为 O(logn)O(logn),POP 时间复杂度为 O(1)O(1)

在使用时,只需要将时间戳或是计数器作为最小堆的排序标准即可

package lru

import (
	"container/heap"
	"math/rand"
)

type keyValue struct {
	key   string
	value int64
	pos   int
}

type keyValueHeap []*keyValue

var _ heap.Interface = (*keyValueHeap)(nil)

func newKeyValueHeap(l int) *keyValueHeap {
	e := keyValueHeap(make([]*keyValue, 0, l))
	return &e
}
func (h keyValueHeap) At(i int) int64 { return h[i].value }
func (h keyValueHeap) Len() int       { return len(h) }
func (h keyValueHeap) Swap(i, j int) {
	h[i], h[j] = h[j], h[i]
	h[i].pos = i
	h[j].pos = j
}
func (h keyValueHeap) Less(i, j int) bool { return h[i].value < h[j].value }
func (h *keyValueHeap) Push(item interface{}) {
	*h = append(*h, item.(*keyValue))
	l := h.Len()
	(*h)[l-1].pos = l - 1
}
func (h *keyValueHeap) Pop() interface{} {
	l := h.Len()
	t := (*h)[l-1]
	(*h) = (*h)[:l-1]
	t.pos = -1
	return t
}

type Heap struct {
	heap *keyValueHeap
	m    map[string]*keyValue
}

func NewHeap() *Heap {
	return &Heap{
		heap: newKeyValueHeap(0),
		m:    make(map[string]*keyValue),
	}
}
func (h *Heap) Len() int {
	return len(h.m)
}

func (h *Heap) Push(key string, value int64) {
	if item, exists := h.m[key]; exists {
		item.value = value
		heap.Fix(h.heap, item.pos)
		return
	}

	item := &keyValue{
		key:   key,
		value: value,
	}
	h.m[key] = item
	heap.Push(h.heap, item)
}

func (h *Heap) Pop() string {
	if h.heap.Len() == 0 {
		return ""
	}
	item := heap.Pop(h.heap).(*keyValue)
	delete(h.m, item.key)
	h.gc()
	return item.key
}

func (h *Heap) Has(key string) bool {
	_, exists := h.m[key]
	return exists
}

func (h *Heap) Remove(key string) {
	item, exist := h.m[key]
	if exist {
		heap.Remove(h.heap, item.pos)
		delete(h.m, key)
	}
}

func (h *Heap) PopUntil(value int64) []string {
	keys := make([]string, 0)
	for h.Len() > 0 && h.heap.At(0) <= value {
		keys = append(keys, h.Pop())
	}
	return keys
}

func (h *Heap) gc() {
	// 10% 概率执行一次 gc
	if rand.Float64() > 0.1 {
		return
	}

	temp := h.m
	h.m = make(map[string]*keyValue)
	for k, v := range temp {
		h.m[k] = v
	}
}

LRU

由于 LRU 还需要维护一个计数器,因此需要再封装一层

实现一个可以自增的计数器就可以记录每个元素的访问顺序,数越小则在最小堆中更前,更早会被移除

package lru

import (
	"sync/atomic"
)

type LRU struct {
	heap    *Heap
	counter int64
	cap     int
}

func NewLRU(cap int) *LRU {
	return &LRU{
		counter: 0,
		heap:    NewHeap(),
		cap:     cap,
	}
}

func (l *LRU) Push(key string) string {
	id := atomic.AddInt64(&l.counter, 1)

	poped := ""
	for l.heap.Len() >= l.cap && !l.Has(key) {
		poped = l.Pop()
	}

	l.heap.Push(key, id)
	return poped
}

func (l *LRU) Pop() string {
	return l.heap.Pop()
}

func (l *LRU) Has(key string) bool {
	return l.heap.Has(key)
}

func (l *LRU) Remove(key string) {
	l.heap.Remove(key)
}

Map

将上述的内容封装入 Map 中,插入的时候检查 LRU,访问的时候检查 Expired 即可。

package lru

import (
	"math/rand"
	"time"
)

type Map struct {
	m map[string]interface{}
	e *Heap
	l *LRU
}

func NewMap() *Map {
	return &Map{
		m: make(map[string]interface{}),
		e: nil,
		l: nil,
	}
}

func (m *Map) WithLRU(cap int) *Map {
	m.l = NewLRU(cap)
	return m
}

func (m *Map) WithExpired() *Map {
	m.e = NewHeap()
	return m
}

func (m *Map) Put(key string, value interface{}) {
	if m.l != nil {
		poped := m.l.Push(key)
		if poped != "" {
			m.remove(poped)
		}
	}
	m.m[key] = value
}

func (m *Map) PutWithExpired(key string, value interface{}, duration time.Duration) {
	m.Put(key, value)
	if m.e != nil {
		m.e.Push(key, time.Now().Add(duration).Unix())
	}
}

func (m *Map) Get(key string) (value interface{}, exists bool) {
	m.removeExpired()
	value, exists = m.m[key]
	return
}

func (m *Map) Len() int {
	m.removeExpired()
	return len(m.m)
}

func (m *Map) Keys() []string {
	m.removeExpired()
	keys := make([]string, 0, len(m.m))
	for k := range m.m {
		keys = append(keys, k)
	}
	return keys
}

func (m *Map) Delete(key string) {
	m.removeExpired()
	m.remove(key)
}

func (m *Map) gc() {
	// 10% 概率执行一次 gc
	if rand.Float64() > 0.1 {
		return
	}

	temp := m.m
	m.m = make(map[string]interface{})
	for k, v := range temp {
		m.m[k] = v
	}
}

func (m *Map) remove(key string) {
	if m.l != nil {
		m.l.Remove(key)
	}
	if m.e != nil {
		m.e.Remove(key)
	}
	delete(m.m, key)
	m.gc()
}

func (m *Map) removeExpired() {
	if m.e != nil {
		now := time.Now()
		keys := m.e.PopUntil(now.Unix())
		for _, k := range keys {
			m.remove(k)
		}
	}
}

一些其他问题

根据 runtime: shrink map as elements are deleted · Issue #20135 · golang/go,这里 Map 可能不会被 gc,这就违背了 LRU 和 Expired 的意义

因此参照 Issue 中的代码,以 10% 的概率触发一次 map 的拷贝,释放掉不再需要的空间