SkillAgentSearch skills...

Ecache

🦄【轻量级本地内存缓存】🤏代码少于300行⌚️30s接入🚀高性能、极简设计、并发安全🌈支持LRU 和 LRU-2模式 🦖支持分布式一致性 [ecache] Extremely easy, ultra fast, concurrency-safe and support distributed consistency. Similar to bigcache, cachego, freecache, gcache, gocache, groupcache, lrucache.

Install / Use

/learn @orca-zhang/Ecache

README

English README | 英文说明

🦄 ecache

<p align="center"> <a href="#"> <img src="https://github.com/orca-zhang/ecache/raw/master/doc/logo.svg"> </a> </p> <p align="center"> <a href="/go.mod#L3" alt="go version"> <img src="https://img.shields.io/badge/go%20version-%3E=1.11-brightgreen?style=flat"/> </a> <a href="https://goreportcard.com/badge/github.com/orca-zhang/ecache" alt="goreport"> <img src="https://goreportcard.com/badge/github.com/orca-zhang/ecache"> </a> <a href="https://orca-zhang.semaphoreci.com/projects/ecache" alt="buiding status"> <img src="https://orca-zhang.semaphoreci.com/badges/ecache.svg?style=shields"> </a> <a href="https://codecov.io/gh/orca-zhang/ecache" alt="codecov"> <img src="https://codecov.io/gh/orca-zhang/ecache/branch/master/graph/badge.svg?token=F6LQbADKkq"/> </a> <a href="https://github.com/orca-zhang/ecache/blob/master/LICENSE" alt="license MIT"> <img src="https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat"> </a> <a href="https://app.fossa.com/projects/git%2Bgithub.com%2Forca-zhang%2Fcache?ref=badge_shield" alt="FOSSA Status"> <img src="https://app.fossa.com/api/projects/git%2Bgithub.com%2Forca-zhang%2Fcache.svg?type=shield"/> </a> <a href="https://benchplus.github.io/gocache/dev/bench/" alt="continuous benchmark"> <img src="https://img.shields.io/badge/benchmark-click--me-brightgreen.svg?style=flat"/> </a> </p> <p align="center">一款极简设计、高性能、并发安全、支持分布式一致性的轻量级内存缓存</p>

特性

  • 🤏 代码量<300行、30s完成接入
  • 🚀 高性能、极简设计、并发安全
  • 🌈 支持LRULRU-2两种模式
  • 🦖 额外小组件支持分布式一致性

基准性能

:snail: 代表很慢, :airplane: 代表快, :rocket: 代表非常快

👁️‍🗨️点我看用例 👁️‍🗨️点我看结果 (除了缓存命中率数值越低越好)

<table style="text-align: center"> <tr> <td></td> <td><a href="https://github.com/allegro/bigcache">bigcache</a></td> <td><a href="https://github.com/FishGoddess/cachego">cachego</a></td> <td><a href="https://github.com/orca-zhang/ecache"><strong>ecache🌟</strong></a></td> <td><a href="https://github.com/coocood/freecache">freecache</a></td> <td><a href="https://github.com/bluele/gcache">gcache</a></td> <td><a href="https://github.com/patrickmn/go-cache">gocache</a></td> </tr> <tr> <td>PutInt</td> <td>:airplane:</td> <td></td> <td>:rocket:</td> <td>:rocket:</td> <td>:airplane:</td> <td>:airplane:</td> </tr> <tr> <td>GetInt</td> <td>:airplane:</td> <td>:airplane:</td> <td>:rocket:</td> <td></td> <td>:airplane:</td> <td>:airplane:</td> </tr> <tr> <td>Put1K</td> <td>:airplane:</td> <td>:airplane:</td> <td>:rocket:</td> <td>:rocket:</td> <td>:rocket:</td> <td>:airplane:</td> </tr> <tr> <td>Put1M</td> <td>:snail:</td> <td></td> <td>:rocket:</td> <td>:snail:</td> <td>:airplane:</td> <td>:airplane:</td> </tr> <tr> <td>PutTinyObject</td> <td>:airplane:</td> <td></td> <td>:rocket:</td> <td>:rocket:</td> <td>:airplane:</td> <td></td> </tr> <tr> <td>ChangeOutAllInt</td> <td>:airplane:</td> <td></td> <td>:rocket:</td> <td>:rocket:</td> <td>:airplane:</td> <td>:airplane:</td> </tr> <tr> <td>HeavyReadInt</td> <td>:rocket:</td> <td>:rocket:</td> <td>:rocket:</td> <td></td> <td></td> <td>:rocket:</td> </tr> <tr> <td>HeavyReadIntGC</td> <td>:airplane:</td> <td>:rocket:</td> <td>:rocket:</td> <td></td> <td>:airplane:</td> <td>:airplane:</td> </tr> <tr> <td>HeavyWriteInt</td> <td>:rocket:</td> <td>:airplane:</td> <td>:rocket:</td> <td>:rocket:</td> <td></td> <td>:airplane:</td> </tr> <tr> <td>HeavyWriteIntGC</td> <td>:rocket:</td> <td></td> <td>:airplane:</td> <td>:airplane:</td> <td></td> <td></td> </tr> <tr> <td>HeavyWrite1K</td> <td>:snail:</td> <td>:airplane:</td> <td>:rocket:</td> <td>:rocket:</td> <td></td> <td>:airplane:</td> </tr> <tr> <td>HeavyWrite1KGC</td> <td>:snail:</td> <td>:airplane:</td> <td>:rocket:</td> <td>:rocket:</td> <td></td> <td>:airplane:</td> </tr> <tr> <td>HeavyMixedInt</td> <td>:rocket:</td> <td>:airplane:</td> <td>:rocket:</td> <td></td> <td>:airplane:</td> <td>:rocket:</td> </tr> <tr> <td colspan="7"> <a href="https://github.com/FishGoddess/cachego"><strong>FishGoddess/cachego</strong></a> 和 <a href="https://github.com/patrickmn/go-cache"><strong>patrickmn/go-cache</strong></a> 是简单的map+过期时间的实现,所以没有命中率测试 </td> </tr> <tr> <td colspan="7"> <a href="https://github.com/kpango/gache"><strong>kpango/gache</strong></a> & <a href="https://github.com/hlts2/gocache"><strong>hlts2/gocache</strong></a> 性能表现不是很好,所以从列表中剔除 </td> </tr> <tr> <td colspan="7"> <a href="https://github.com/patrickmn/go-cache"><strong>patrickmn/go-cache</strong></a> 是FIFO模式,其他的库都是LRU模式 </td> </tr> </table>

gc pause测试结果 代码由bigcache提供(数值越低越好)

目前正在生产环境大流量验证中

  • [已验证]公众号后台(几百QPS):用户信息、订单信息、配置信息
  • [已验证]推送系统(几万QPS):可调整系统配置、信息去重、固定信息缓存
  • [已验证]评论系统(几万QPS):用户信息、分布式一致性组件

如何使用

引入包(预计5秒)

import (
    "time"

    "github.com/orca-zhang/ecache"
)

定义实例(预计5秒)

可以放置在任意位置(全局也可以),建议就近定义

var c = ecache.NewLRUCache(16, 200, 10 * time.Second)

设置缓存(预计5秒)

c.Put("uid1", o) // `o`可以是任意变量,一般是对象指针,存放固定的信息,比如`*UserInfo`

查询缓存(预计5秒)

if v, ok := c.Get("uid1"); ok {
    return v.(*UserInfo) // 不用类型断言,咱们自己控制类型
}
// 如果内存缓存没有查询到,下面再回源查redis/db

删除缓存(预计5秒)

在信息发生变化的地方

c.Del("uid1")

下载包(预计5秒)

非go modules模式:
sh> go get -u github.com/orca-zhang/ecache

go modules模式:
sh> go mod tidy && go mod download

运行吧

🎉 完美搞定 🚀 性能直接提升X倍!
sh> go run <你的main.go文件>

参数说明

  • NewLRUCache
    • 第一个参数是桶的个数,用来分散锁的粒度,每个桶都会使用独立的锁,最大值为65535,支持65536个实例
      • 不用担心,随意设置一个就好,ecache会找一个合适的数字便于后面掩码计算
    • 第二个参数是每个桶所能容纳的item个数上限,最大值为65535
      • 意味着ecache全部写满的情况下,应该有第一个参数 X 第二个参数个item,最多能支持存储42亿个item
    • [可选]第三个参数是每个item的过期时间
      • ecache使用内部计时器提升性能,默认100ms精度,每秒校准
      • 不传或者传0,代表永久有效

最佳实践

  • 支持任意类型的值
    • 提供Put/PutInt64/PutBytes三种方法,适应不同场景,需要与Get/GetInt64/GetBytes配对使用(后两种方法GC开销较小)
    • 复杂对象优先存放指针(注意⚠️一旦放进去不要再修改其字段,即使再拿出来也是,item有可能被其他人同时访问)
      • 如果需要修改,解决方案:取出字段每个单独赋值,或者用copier做一次深拷贝后在副本上修改
      • 也可以存放对象(相对于直接存对象指针性能差一些,因为拿出去有拷贝)
      • 缓存的对象尽可能越往业务上层越大越好(节省内存拼装和组织时间)
  • 如果不想因为类似遍历的请求把热数据刷掉,可以改用LRU-2模式,可能有很少的损耗(💬 什么是LRU-2
    • LRU2LRU的大小设置分别为1/4和3/4效果较好
  • 一个实例可以存储多种类型的对象,试试key格式化的时候加上前缀,用冒号分割
  • 并发访问量大的场景,试试2561024个桶,甚至更多
  • 可以当作缓冲队列用于合并更新以减少刷盘次数(数据可以重建或容忍断电丢失的情况下)
    • 具体使用方式是挂载Inspector监听驱逐事件
    • 终末或定时调用Walk将数据刷到存储

特别场景

整型键、整型值和字节数组

// 整型键
c.Put(strconv.FormatInt(d, 10), o) // d为`int64`类型

// 整型值
c.PutInt64("uid1", int64(1))
if d, ok := c.GetInt64("uid1"); ok {
    // d为`int64`类型的1
}

// 字节数组
c.PutBytes("uid1", b)// b为`[]byte`类型
if b, ok := c.GetBytes("uid1"); ok {
    // b为`[]byte`类型
}

LRU-2模式

直接在NewLRUCache()后面跟.LRU2(<num>)就好,参数<num>代表LRU-2热队列的item上限个数(每个桶)

var c = ecache.NewLRUCache(16, 200, 10 * time.Second).LRU2(1024)

空缓存哨兵(不存在的对象不用再回源)

// 设置的时候直接给`nil`就好
c.Put("uid1", nil)
// 读取的时候,也和正常差不多
if v, ok := c.Get("uid1"); ok {
  if v == nil { // 注意⚠️这里需要判断是不是空缓存哨兵
    return nil  // 是空缓存哨兵,那就返回没有信息或者也可以让`uid1`不出现在待回源列表里
  }
  return v.(*UserInfo)
}
// 如果内存缓存没有查询到,下面再回源查redis/db

需要修改部分数据,且用对象指针方式存储时

比如,我们从ecache中获取了*UserInfo类型的用户信息缓存v,需要修改其状态字段

import (
    "github.com/jinzhu/copier"
)
o := &UserInfo{}
copier.Copy(o, v) // 从`v`复制到`o`
o.Status = 1      // 修改副本的字段

注入监听器

// inspector - 可以用来做统计或者缓冲队列等
//   `action`:PUT, `status`: evicted=-1, updated=0, added=1
//   `action`:GET, `status`: miss=0, hit=1
//   `action`:DEL, `status`: miss=0, hit=1
//   `iface`/`bytes`只有在`status`不为0或者`action`为PUT时才不为nil
type inspector func(action int, key string, iface *interface{}, bytes []byte, status int)
  • 使用方式
cache.Inspect(func(action int, key string, iface *interface{}, bytes []byte, status int) {
  // TODO: 实现你想做的事情
  //     监听器会根据注入顺序依次执行
  //     注意⚠️如果有耗时操作,尽量另开channel保证不阻塞当前协程

  // - 如何获取正确的值 -
  //   - `Put`:      `*iface`
  //   - `PutBytes`: `bytes`
  //   - `PutInt64`: `ecache.ToInt64(bytes)`
})

遍历所有元素

  // 只会遍历缓存中存在且未过期的项
  cache.Walk(func(key string, iface *interface{}, bytes []byte, expireAt int64) bool {
    // `key`是值,`iface`/`bytes`是值,`expireAt`是过期时间

    // - 如何获取正确的值 -
    //   - `Put`:      `*iface`
    //   - `PutBytes`: `bytes`
    //   - `PutInt64`: `ecache.ToInt64(bytes)`
    return true // 是否继续遍历
  })

统计缓存使用情况

实现超级简单,注入inspector后,每个操作只多了一次原子操作,具体看代码

引入stats包
import (
    "github.com/orca-zhang/ecache/stats"
)

绑定缓存实例

名称为自定义的池子名称,内部会按名称聚合
注意⚠️绑定可以放在全局

var _ = stats.Bind("user", c)
var _ = stats.Bind("user", c0, c1, c2)
var _ = stats.Bind("token", caches...)

获取统计信息

stats.Stats().Range(func(k, v interface{}) bool {
    fmt.Printf("stats: %s %+v\n", k, v) // k是池子名称,v是(*stats.StatsNode)类型
    // 其中统计了各种事件的次数,使用`HitRate`方法可以获得缓存命中率
    return true
})

分布式一致性组件

View on GitHub
GitHub Stars267
CategoryCustomer
Updated13d ago
Forks28

Languages

Go

Security Score

100/100

Audited on Mar 15, 2026

No findings