{"id":23960398,"url":"https://github.com/harris-h/fast-cache","last_synced_at":"2026-06-17T17:33:03.521Z","repository":{"id":270865409,"uuid":"911690682","full_name":"Harris-H/fast-cache","owner":"Harris-H","description":"cache algorithm implemented based on go, supporting multiple algorithms such as GClock,FIFO,LFU, LRU, LRU-K, 2Q, etc.","archived":false,"fork":false,"pushed_at":"2025-01-08T12:19:52.000Z","size":1097,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-02-24T12:19:12.770Z","etag":null,"topics":["2q-cache","clock","fifo-cache","go","lfu-cache","lru","lru-cache","lru-k"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Harris-H.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-01-03T16:04:56.000Z","updated_at":"2025-01-08T12:19:55.000Z","dependencies_parsed_at":"2025-01-03T17:22:13.871Z","dependency_job_id":"7c7f0f47-025c-4bd4-8efd-b83fb7316230","html_url":"https://github.com/Harris-H/fast-cache","commit_stats":null,"previous_names":["harris-h/fast-cache"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Harris-H%2Ffast-cache","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Harris-H%2Ffast-cache/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Harris-H%2Ffast-cache/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Harris-H%2Ffast-cache/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Harris-H","download_url":"https://codeload.github.com/Harris-H/fast-cache/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240475239,"owners_count":19807292,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["2q-cache","clock","fifo-cache","go","lfu-cache","lru","lru-cache","lru-k"],"created_at":"2025-01-06T19:27:53.004Z","updated_at":"2026-06-17T17:33:03.466Z","avatar_url":"https://github.com/Harris-H.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# fast-cache\n\n该项目基于[golang-lru](https://github.com/hashicorp/golang-lru)和[go-generics-cache](https://github.com/Code-Hex/go-generics-cache)二次开发，是其简化版，并进行了一些修改。\n\n\n\n## 1 特性\n\n- **支持FIFO**\n- **支持3种时钟算法**\n  - **GClock**\n  - **Clock-Sweep(based on postgresql)**\n  - **WSClock(Working set clock)**\n\n- **支持LRU**\n- **支持LFU**\n- **支持改进的2Q**\n- **支持LRU-K**\n- **支持回调函数EvictCallback**\n- 支持缓存由新到旧遍历Key、Value(由reverse参数驱动)\n- 对Resize()函数添加错误处理(当size为负数报错)\n- 新增AddMany方法，可以一次性添加多个(key,value)对，提高性能。\n- 新增RemoveMany方法，一次性删除多个(key,value)对。\n\n\n\n## 2 使用示例\n\n### FIFO\n\n```go\nfunc TestExampleNewCache(t *testing.T) {\n\tc, err := NewFIFO[string, int](128, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"err: %v\", err)\n\t}\n\tc.Add(\"a\", 1)\n\tc.Add(\"b\", 2)\n\tav, aok := c.Get(\"a\")\n\tbv, bok := c.Get(\"b\")\n\tcv, cok := c.Get(\"c\")\n\tfmt.Println(av, aok)\n\tfmt.Println(bv, bok)\n\tfmt.Println(cv, cok)\n\tc.Remove(\"a\")\n\t_, aok2 := c.Get(\"a\")\n\tif !aok2 {\n\t\tfmt.Println(\"key 'a' has been deleted\")\n\t}\n\t// update\n\tc.Add(\"b\", 3)\n\tnewbv, _ := c.Get(\"b\")\n\tfmt.Println(newbv)\n\t// Output:\n\t// 1 true\n\t// 2 true\n\t// 0 false\n\t// key 'a' has been deleted\n\t// 3\n}\n```\n\n### GClock\n\n```go\nfunc TestExampleNewCache(t *testing.T) {\n\tc, err := NewClock[string, int](128, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"err: %v\", err)\n\t}\n\tc.Add(\"a\", 1)\n\tc.Add(\"b\", 2)\n\tav, aok := c.Get(\"a\")\n\tbv, bok := c.Get(\"b\")\n\tcv, cok := c.Get(\"c\")\n\tfmt.Println(av, aok)\n\tfmt.Println(bv, bok)\n\tfmt.Println(cv, cok)\n\tc.Delete(\"a\")\n\t_, aok2 := c.Get(\"a\")\n\tif !aok2 {\n\t\tfmt.Println(\"key 'a' has been deleted\")\n\t}\n\t// update\n\tc.Add(\"b\", 3)\n\tnewbv, _ := c.Get(\"b\")\n\tfmt.Println(newbv)\n}\n```\n\n### LRU\n\n```go\npackage main\n\nimport (\n\t\"fast-cache/lru\"\n\t\"fmt\"\n)\n\nfunc main() {\n\tl, _ := lru.New[int, string](5)\n\tl.AddMany([]int{1, 2, 3, 4, 5}, []string{\"Java\", \"Go\", \"Python\", \"C++\", \"C\"})\n\tkeys := l.Keys(false)\n\tfmt.Println(\"keys: \", keys)\n\tkeysOrderedByNew := l.Keys(true)\n\tfmt.Println(\"keysOrderedByNew 1: \", keysOrderedByNew)\n\tvalue, ok := l.Get(3)\n\tif ok {\n\t\tfmt.Println(\"key: \", 3, \" value: \", value)\n\t}\n\tfmt.Println(\"keysOrderedByNew 2: \", keysOrderedByNew)\n\tfmt.Println(\"Add (6,Rust): \")\n\tl.Add(6, \"Rust\")\n\tkeysOrderedByNew = l.Keys(true)\n\tfmt.Println(\"keysOrderedByNew 3: \", keysOrderedByNew)\n}\n```\n\n### LFU\n\n```go\nfunc TestSet(t *testing.T) {\n\t// set size is 1\n\tcache, err := NewLFU[string, int](1, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"err: %v\", err)\n\t}\n\tcache.Add(\"foo\", 1)\n\tif got := cache.Len(); got != 1 {\n\t\tt.Fatalf(\"invalid length: %d\", got)\n\t}\n\tif got, ok := cache.Get(\"foo\"); got != 1 || !ok {\n\t\tt.Fatalf(\"invalid value got %d, cachehit %v\", got, ok)\n\t}\n\n\t// if over the size\n\tcache.Add(\"bar\", 2)\n\tif got := cache.Len(); got != 1 {\n\t\tt.Fatalf(\"invalid length: %d\", got)\n\t}\n\tbar, ok := cache.Get(\"bar\")\n\tif bar != 2 || !ok {\n\t\tt.Fatalf(\"invalid value bar %d, cachehit %v\", bar, ok)\n\t}\n\n\t// checks deleted oldest\n\tif _, ok := cache.Get(\"foo\"); ok {\n\t\tt.Fatalf(\"invalid delete oldest value foo %v\", ok)\n\t}\n\n\t// valid: if over the cap but same key\n\tcache.Add(\"bar\", 100)\n\tif got := cache.Len(); got != 1 {\n\t\tt.Fatalf(\"invalid length: %d\", got)\n\t}\n\tbar, ok = cache.Get(\"bar\")\n\tif bar != 100 || !ok {\n\t\tt.Fatalf(\"invalid replacing value bar %d, cachehit %v\", bar, ok)\n\t}\n}    \n```\n\n### 2Q\n\n```go\nfunc Test2Q(t *testing.T) {\n\tl, err := lru.New2Q[int, string](5)\n\tif err != nil {\n\t\tt.Fatalf(\"err: %v\", err)\n\t}\n\tl.Add(1, \"Java\")\n\tl.Add(2, \"Go\")\n\tl.Add(3, \"Python\")\n\tl.Add(4, \"C++\")\n\tl.Add(5, \"C\")\n\tkeys := l.Keys(false)\n\tt.Logf(\"keys: %v\", keys)\n\tkeysOrderedByNew := l.Keys(true)\n\tt.Logf(\"keysOrderedByNew 1: %v\", keysOrderedByNew)\n\tvalue, ok := l.Get(3)\n\tif ok {\n\t\tt.Logf(\"key: %v value: %v\", 3, value)\n\t}\n\tt.Logf(\"keysOrderedByNew 2: %v\", l.Keys(true))\n\tl.Add(6, \"Rust\")\n\tkeysOrderedByNew = l.Keys(true)\n\tt.Logf(\"keysOrderedByNew 3: %v\", keysOrderedByNew)\n\tl.Remove(5)\n\tt.Logf(\"keysOrderedByNew 4: %v\", l.Keys(true))\n}\n```\n\n### LRU-K\n\n```go\nfunc TestLRUK(t *testing.T) {\n\tl, _ := lru.NewLruK[int, string](5, 2)\n\tl.Add(1, \"Java\")\n\tl.Add(2, \"Go\")\n\tl.Add(3, \"Python\")\n\tl.Add(4, \"C++\")\n\tl.Add(5, \"C\")\n\tkeys := l.Keys(false)\n\tfmt.Println(\"keys: \", keys)\n\tfmt.Println(\"keysOrderedByNew 1: \", l.Keys(true))\n\tvalue, ok := l.Get(3)\n\tif ok {\n\t\tfmt.Println(\"key: \", 3, \" value: \", value)\n\t}\n\tfmt.Println(\"keysOrderedByNew 2: \", l.Keys(true))\n\tfmt.Println(\"Add (6,Rust): \")\n\tl.Add(6, \"Rust\")\n\tfmt.Println(\"keysOrderedByNew 3: \", l.Keys(true))\n\tfmt.Println(\"Get key: 6\")\n\tfmt.Println(l.Get(6))\n\tfmt.Println(\"keysOrderedByNew 4: \", l.Keys(true))\n\tfmt.Println(\"Remove key: 5\")\n\tl.Remove(5)\n\tfmt.Println(\"keysOrderedByNew 5: \", l.Keys(true))\n\tfmt.Println(l.Get(4))\n\tfmt.Println(\"keysOrderedByNew 6: \", l.Keys(true))\n\n\tfmt.Println(l.Values(true))\n}\n```\n\n\n\n## 3 数据结构\n\n### LFU\n\n\u003cimg src=\".\\assets\\lfu.png\" alt=\"lfu\" width=\"30%\" /\u003e\n\n LFU（Least Frequently Used）算法根据数据的访问频率来决定缓存数据的替换。最少被访问的数据会被移除。\n\n### GCLOCK\n\n朴素CLOCK：一圈页，一个指针指向某页，要替换某页时，看指向的那页的访问位是不是1，如果不是就将这页替换掉，如果是则置0，然后移到下一页继续看。\n\n本项目基于GLOCK算法：相对于Clock标志位采用的是二进制0和1表示，Gclock的标志位采用的是一个整数，意味着理论上可以一直增加到无穷大。\n\n给每页一个refcount，当hit的时候增加它的值，当指针扫过的时候减这个值，减到0就可以替换掉了。好处是可以保留更多的历史访问信息，更精准地把很少访问的页找出来。\n\n### Clock-Sweep\n\n\u003cimg src=\".\\assets\\Clock-Sweep.png\" alt=\"Clock-Sweep\" style=\"zoom:50%;\" /\u003e\n\n- 1)：nextVictimBuffer 指向第一个描述符 （buffer_id 1）。但是，由于此描述符已固定(pinned)，因此会跳过此描述符。\n- 2)：nextVictimBuffer 指向第二个描述符 （buffer_id 2）。此描述符未固定(unpinned)，但其usage_count为 2。因此，usage_count 减少 1，并且 nextVictimBuffer 前进到第三个候选项。\n-  nextVictimBuffer 指向第三个描述符 （buffer_id 3）。此描述符未固定，其usage_count为 0。因此，这是这一轮选择的结果。\n\n\u003e每次从上次位置开始轮询，然后检查buffer 的引用次数 refcount 和访问次数 usagecount。\n\u003e\n\u003e1. 如果 refcount，usagecount 都为零，那么直接返回。\n\u003e2. 如果 refcount 为零，usagecount 不为零，那么将其usagecount 减1，遍历下一个buffer。\n\u003e3. 如果 refcount 不为零，则遍历下一个。\n\u003e\n\u003eclock sweep 算法是一个死循环算法，直到找出一个 refcount，usagecount 都为零的buffer。\n\n### WSClock(**Working set clock**)\n\n\u003cimg src=\".\\assets\\wsclock.png\" alt=\"wsclock\" width=\"50%\" /\u003e\n\n当缓存已满，需要替换页面时，WSClock 算法会检查指针指向的页面:\n\n- 如果 R 位为 1:表示该页面在工作集中，将其 R 位重置为 0，然后指针移动到下一个页面。\n- 如果 R 位为 0:则需要进一步检查该页面的生存时间(age)。如果生存时间大于设定阈值$t$，则可以替换该页面，并将新页面插入;如果生存时间小于或等于$t$则继续查找下一个页面。\n- 如果循环一圈后仍未找到合适的替换对象，则替换第一个R位为0的页面。\n\n### LRU-K\n\n\u003eLRU-K中的K代表最近使用的次数，因此LRU可以认为是LRU-1。LRU-K的主要目的是为了解决LRU算法“缓存污染”的问题，其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。\n\n相比LRU，LRU-K需要多维护一个队列，用于记录所有缓存数据被访问的历史。只有当数据的访问次数达到K次的时候，才将数据放入缓存。当需要淘汰数据时，LRU-K会淘汰第K次访问时间距当前时间最大的数据。详细实现如下。\n\n\u003cimg src=\".\\assets\\lruk.png\" alt=\"lruk\" width=\"50%\" /\u003e\n\n　(1). 数据第一次被访问，加入到访问历史列表；\n\n　(2). 如果数据在访问历史列表里后没有达到K次访问，则按照一定规则（FIFO，LRU）淘汰；\n\n　(3). 当访问历史队列中的数据访问次数达到K次后，将数据索引从历史队列删除，将数据移到缓存队列中，并缓存此数据，缓存队列重新按照时间排序；\n\n　(4). 缓存数据队列中被再次访问后，重新排序；\n\n　(5). 需要淘汰数据时，淘汰缓存队列中排在末尾的数据，即：淘汰“倒数第K次访问离现在最久”的数据。\n\n　LRU-K具有LRU的优点，同时能够避免LRU的缺点，实际应用中LRU-2是综合各种因素后最优的选择，LRU-3或者更大的K值命中率会高，但适应性差，需要大量的数据访问才能将历史访问记录清除掉。\n\n- 本项目的访问历史队列基于LRU淘汰，当访问历史队列数据次数达到k，则移动到LRU缓存队列，并不再统计次数，而是按照LRU进行淘汰。\n\n### 2Q\n\nsimple 2Q算法类似LRU-2，不同点在于2Q将LRU-2算法中的访问历史队列（仅作记录不缓存数据）改为FIFO缓存队列。即，simple 2Q算法有两个缓存队列，一个FIFO队列，一个LRU队列。\n\n\u003cimg src=\".\\assets\\2q.png\" alt=\"2q\" width=\"50%\" /\u003e\n\n(1). 新访问的数据插入到FIFO队列；\n\n(2). 如果数据在FIFO队列中一直没有被再次访问，则最终按照FIFO规则淘汰；\n\n(3). 如果数据在FIFO队列中被再次访问，则将数据移到LRU队列头部；\n\n(4). 如果数据在LRU队列再次被访问，则将数据移到LRU队列头部；\n\n(5). LRU队列淘汰末尾的数据。\n\n- FIFO缓存队列在LRU队列缓存之前充当过滤器，任何尝试进入LRU缓存的数据都必须首先通过此传入缓冲区。\n- 只有当再次访问一个 Item 时，它才会从FIFO队列提升到 LRU 缓存队列。\n\n而本项目是基于优化的2q：\n\n- 当FIFO队列已满，并且尝试再加入一个新数据到FIFO中时，此时会淘汰FIFO的队头数据，我们不会立即驱逐该项目，而是将其移动到另一个缓冲区中，我们称之为驱逐缓冲区(Evict Buffer)。\n  - 该缓冲区将保留已经被淘汰的数据，直到它也已满，如果此时Evict Buffer中有数据被再次访问，则将其从Evice Buffer中删除，并加入到LRU队列中。\n\n\u003e 如果数据遵循整齐且可预测的正态分布，则LRU可能工作正常。\n\u003e\n\u003e 但现实世界很少如此这样，它充满了长尾场景，例如搜索查询、电子商务推荐，以及少数项目获得大量关注而其余项目仍然是小众项目的任何内容。\n\u003e\n\u003e 2Q可帮助缓存专注于重要的命中，从而提高性能并免于不必要的麻烦。\n\n## 4 待完善\n\n- 2q可获取当前Evict Buffer的数据\n- Benchmark测试\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fharris-h%2Ffast-cache","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fharris-h%2Ffast-cache","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fharris-h%2Ffast-cache/lists"}