- 内部可变 - 毋须为并发编程而使用
Arc<RwLock<Cache<...>>
,用Cache<...>
或者AsyncCache<...>
就够了。 - 异同两制 -
stretto
通过crossbeam
实现同步版本, 也支持runtime agnostic异步。但是本质是统一的。- 在同步版本中,缓存会开启两个额外的操作系统线程。一个是策略线程,另一个为写入线程;
- 在异步版本中,缓存会开启两个额外的绿色协程。一个为策略协程,另一个为写入协程。
- 写入策略 -
stretto
仅会存储键值对中的值,并不会存储键。 - 高命中率 - 在
Dgraph
开发者独树一帜的录入/撤除策略的加持下,Ristretto 的性能在同级下是坠吼的,跑得比谁都快。- 录入:TinyLFU 算法 - 更高的性能,仅需为每个计数器额外 +12bits。
- 撤除:SampledLFU 算法 - 性能比肩 LRU,但在搜索与数据库追踪上更胜一筹。
- 高吞吐量 - 多种操作处理冲突,带来催人跑的高带宽。
- 基于权重 - 插入大权重的新缓存项可以淘汰多个低权重的缓存项。(权重可以是任何属性)
- 完全并行 - 在并行中性能仅会略微降低。新线程?开,都可以开。
- 可选度量 - 可选的吞吐量、命中率或者其他统计指标的度量衡。
- Simple API - 考察、设定您理想的
CacheBuilder
/AsyncCacheBuilder
参数,然后起飞!🚀
- 使用同步缓存
[dependencies]
stretto = "0.8"
或
[dependencies]
stretto = { version = "0.8", features = ["sync"] }
- 使用异步缓存
[dependencies]
stretto = { version = "0.8", features = ["async"] }
- 同步异步同时使用
[dependencies]
stretto = { version = "0.8", features = ["full"] }
use stretto::{Cache, DefaultKeyBuilder};
use std::time::Duration;
fn main() {
let c = Cache::new(12960, 1e6 as i64, DefaultKeyBuilder::default()).unwrap();
// 设定一个键为 "a", 权为 1 的值
c.insert("a", "a", 1);
// 设定一个键为 "a",权为 1 的带生存期的值
c.insert_with_ttl("b", "b", 1, Duration::from_secs(3));
// 等待值存入缓存中
c.wait().unwrap();
// 当尝试访问值时,会返回一个包含了 RwLockReadGuard 的 ValueRef
// 当完成使用这个值时,ValueRef 需要释放
let v = c.get(&"a").unwrap();
assert_eq!(v.value(), &"a");
// 手动释放
v.release(); // 或者析构 v
// 离开作用域后锁会被自动释放
{
// 当尝试访问值时,会返回一个包含了 RwLockReadGuard 的 ValueRef
// 当完成使用这个值时,ValueRef 需要释放
let mut v = c.get_mut(&"a").unwrap();
v.write("aa");
assert_eq!(v.value(), &"aa");
// 释放值
}
// 如果只对 v 操作一次
let v = c.get_mut(&"a").unwrap();
v.write_once("aaa");
let v = c.get(&"a").unwrap();
assert_eq!(v.value(), &"aaa");
v.release();
// 缓存清零
c.clear().unwrap();
// 等待所有操作完成
c.wait().unwrap();
assert!(c.get(&"a").is_none());
}
use stretto::{AsyncCache, DefaultKeyBuilder};
use std::time::Duration;
#[tokio::main]
async fn main() {
// 在这个例子中, 我们使用tokio运行时, 所以需要将tokio::spawn作为spawner在构建缓存的时候
let c = AsyncCache::new(12960, 1e6 as i64, DefaultKeyBuilder::default(), tokio::spawn).unwrap();
// 设定一个键为 "a" 权为 1 的值
c.insert("a", "a", 1).await;
// 设定一个键为 "a",权为 1,生存期为 1s 的值
c.insert_with_ttl("b", "b", 1, Duration::from_secs(1)).await;
// 等待值存入缓存中
c.wait().await.unwrap();
// 当尝试访问值时,会返回一个包含了 RwLockReadGuard 的 ValueRef
// 当完成使用这个值时,ValueRef 需要释放
let v = c.get(&"a").unwrap();
assert_eq!(v.value(), &"a");
// 释放值
v.release(); // 或者直接析构 v
// 离开作用域时锁会自动释放
{
// 当尝试访问值时,会返回一个包含了 RwLockReadGuard 的 ValueRef
// 当完成使用这个值时,ValueRef 需要释放
let mut v = c.get_mut(&"a").unwrap();
v.write("aa");
assert_eq!(v.value(), &"aa");
// 释放值
}
// 如果只对 v 操作一次
let v = c.get_mut(&"a").unwrap();
v.write_once("aaa");
let v = c.get(&"a").unwrap();
println!("{}", v);
assert_eq!(v.value(), &"aaa");
v.release();
// 缓存清空
c.clear().await.unwrap();
// 等待操作完成
c.wait().await.unwrap();
assert!(c.get(&"a").is_none());
}
如果希望定制缓存,请使用 CacheBuilder
来创建 Cache
对象。
num_counters
(计数器数)是用于保存录入与淘汰信息的 4 位访问计数器的数目。Dgraph 的开发者们在将其设为约 10 倍于缓存容量的时候获得了不错的性能。
比如,在每个缓存项的权为 1 且 max_cost
设定为 100 时,应将 num_counters
设为 1,000;或者如果缓存项权值不等,而期望缓存可以容纳约 10,000 项时,应将 num_counter
设为 100,000——应当考虑的是可以装满缓存的唯一键值数量而非 max_cost
的值。
max_cost
(最大权值和)是缓存是否进行撤除操作的参考。在 max_cost
为 100 时,如果插入一个权为 1 的项使得缓存内总权值之和为 101,那么一个缓存项会被淘汰。
max_cost
可以被用于表示缓存的最大体积(字节)。举个例子,如果 max_cost
为 1,000,000 (1 MB,1 兆字节) 而缓存已经装入 1,000 个 1 KB 的项,一个被接收的新缓存项会导致 5 个 1KB 的缓存项被撤除。
权值可以是任意属性,亦即 max_cost
也可以指代任何属性的权值的和的最大值。
pub trait KeyBuilder {
type Key: Hash + Eq + ?Sized;
/// hash_index 用于将键哈希运算成一个 u64 值
fn hash_index<Q>(&self, key: &Q) -> u64
where
Self::Key: core::borrow::Borrow<Q>,
Q: Hash + Eq + ?Sized;
/// 如果希望使用一个 128 位哈希,需要实现此方法。
/// 默认返回 0
fn hash_conflict<Q>(&self, key: &Q) -> u64
where
Self::Key: core::borrow::Borrow<Q>,
Q: Hash + Eq + ?Sized;
{ 0 }
/// 将键进行哈希运算,返回 128 位哈希结果。
fn build_key<Q>(&self, k: &Q) -> (u64, u64)
where
Self::Key: core::borrow::Borrow<Q>,
Q: Hash + Eq + ?Sized;
{
(self.hash_index(k), self.hash_conflict(k))
}
}
KeyBuilder
(键生成器)是使用于所有的键的哈希算法。Stretto
并不会存储键的真正的值,
而是会将其使用 KeyBuilder
处理。
Stretto
内建了两套默认的键生成器,
一套为 TransparentKeyBuilder
(透明键生成器),另一套为 DefaultKeyBuilder
(默认键生成器)。
只有当键类型实现了 TransparentKey
特性时,才可以使用相比 DefaultKeyBuider
更快的 TransparentKeyBuilder
。
用户可以通过实现 KeyBuilder
特质另起炉灶,自己实现一套键生成器。
注意当希望使用 128 位哈希时请将 (u64, u64)
中的两项都用到。如果只想使用 64 位哈希可以将元组中第一个(索引为 0)的值置 0。
buffer_size
(缓存大小)是插入缓存的大小。Dgraph 的开发者们发现设为 32 × 1024 (的整倍数?)时性能很好。
如果偶然发现插入性能大幅下降,同时出现较多冲突(通常并不会),请尝试将该值设定为更高的 32 × 1024 的整倍数。缓存的内部机制调教得当,用户一般不会需要修改该值。
Metrics(度量)应当在需要实时日志记录多种状态信息的时候设置为 true
。之所以并未设定成默认启用,是因为可能会降低 10% 的吞吐量。
设定为 true
时缓存将会忽略存储值的内部开销,这在开销不以比特为单位时很有用。不过谨记这会导致更高的内存占用。
默认情况下缓存会每 500 毫秒清理一次过期的值
pub trait UpdateValidator: Send + Sync + 'static {
type Value: Send + Sync + 'static;
/// should_update 在一个已经存在于缓存中的值被更新时调用
fn should_update(&self, prev: &Self::Value, curr: &Self::Value) -> bool;
}
默认状态下,缓存总是会更新已经在缓存中的值。 该特性用于确认该值是否被更新。
pub trait CacheCallback: Send + Sync + 'static {
type Value: Send + Sync + 'static;
/// on_exit 在一个值被移除 (remove) 出缓存的时候调用。
/// 可以用于实现手动内存释放。
/// 在撤除 (evict) 或者拒绝 (reject) 值的时候亦会被调用
fn on_exit(&self, val: Option<Self::Value>);
/// on_evict 在撤除值的时候会被调用,同时会将哈希键、值和权传给函数。
fn on_evict(&self, item: Item<Self::Value>) {
self.on_exit(item.val)
}
/// on_reject 会被 policy 为每个所拒绝的值调用
fn on_reject(&self, item: Item<Self::Value>) {
self.on_exit(item.val)
}
}
CacheCallBack(缓存回调)被用于定制在事件发生时对值的额外操作。
pub trait Coster: Send + Sync + 'static {
type Value: Send + Sync + 'static;
/// cost 函数对值进行求值并返回对应的权重,该函数
/// 会在一个新值插入或一个值更新为 0 权值时被调用
fn cost(&self, val: &Self::Value) -> i64;
}
Cost
是一个可以传给 CacheBuilder
进行运行时权重求值的特征,并且仅仅对未丢弃的 insert
函数调用使用——这在计算权值相当耗时或者耗资源时非常有用,尤其是当用户不想在迟早被析构的值上浪费时间时。
用户可以通过如下方法使得 Stretto 使用自己定制的 Coster 特征:
- 将
Coster
值设定为自己的Coster
实现; - 在插入新缓存项或更新缓存项,调用
insert
时,将cost
设为 0。
缓存的哈希器,默认为 SipHasher
。
- 感谢 Dgraph 的开发者们,提供了如此亦可赛艇的 Ristretto Go 语言实现。
除非您明确说明,任何由您有意提交以纳入本项目的贡献,如Apache-2.0许可证所定义的,应按上述规定进行双重许可,没有任何附加条款或条件。