name: multiversx-cache-patterns description: 用于MultiversX智能合约的基于Drop写回缓存的Gas优化缓存模式。适用于构建每个交易读取/写入多个存储值的合约、DeFi协议或任何对Gas敏感的合约。
MultiversX缓存模式
为什么需要缓存?
- 存储读取/写入是MultiversX智能合约中最昂贵的操作
- 一个读取5个存储值并写回3个的端点需要8次存储操作
- 使用缓存:进入时5次读取 + 退出时3次写入 = 相同,但函数内部的中间读取是免费的(在内存中)
模式1:基于Drop特质的写回缓存
核心模式:在进入时将状态加载到结构体中,在内存中修改,通过Drop在作用域退出时提交。
multiversx_sc::imports!();
use multiversx_sc::derive_imports::*;
pub struct StorageCache<'a, C>
where
C: crate::storage::StorageModule,
{
sc_ref: &'a C,
pub field_a: BigUint<C::Api>,
pub field_b: BigUint<C::Api>,
pub field_c: BigUint<C::Api>,
}
impl<'a, C> StorageCache<'a, C>
where
C: crate::storage::StorageModule,
{
pub fn new(sc_ref: &'a C) -> Self {
StorageCache {
field_a: sc_ref.field_a().get(),
field_b: sc_ref.field_b().get(),
field_c: sc_ref.field_c().get(),
sc_ref,
}
}
}
impl<C> Drop for StorageCache<'_, C>
where
C: crate::storage::StorageModule,
{
fn drop(&mut self) {
// 将所有可变字段提交回存储
self.sc_ref.field_a().set(&self.field_a);
self.sc_ref.field_b().set(&self.field_b);
self.sc_ref.field_c().set(&self.field_c);
}
}
在端点中的使用
#[endpoint]
fn deposit(&self) {
let payment = self.call_value().single();
let mut cache = StorageCache::new(self);
// 所有读取/写入都在内存中 - 初始加载后免费
cache.field_a += payment.amount.as_big_uint();
cache.field_b += &self.calculate_shares(&cache, payment.amount.as_big_uint());
// cache.drop() 在此处自动调用 - 写入存储
}
模式2:只读缓存(无Drop)
用于视图函数或读取密集的操作,不需要写回:
pub struct ReadCache<'a, C: crate::storage::StorageModule> {
sc_ref: &'a C,
pub total_supply: BigUint<C::Api>,
pub total_reserve: BigUint<C::Api>,
pub config_params: YourConfigType<C::Api>,
}
impl<'a, C: crate::storage::StorageModule> ReadCache<'a, C> {
pub fn new(sc_ref: &'a C) -> Self {
ReadCache {
total_supply: sc_ref.total_supply().get(),
total_reserve: sc_ref.total_reserve().get(),
config_params: sc_ref.config_params().get(),
sc_ref,
}
}
// 无Drop实现 - 不写回任何内容
}
模式3:带计算方法的缓存
当缓存需要从缓存字段计算衍生值时,直接在缓存结构体上添加方法:
pub struct StateCache<'a, C>
where
C: crate::storage::StorageModule + crate::math::MathModule,
{
sc_ref: &'a C,
pub total_deposited: BigUint<C::Api>,
pub total_shares: BigUint<C::Api>,
pub fee_rate_bps: u64,
}
impl<C> StateCache<'_, C>
where
C: crate::storage::StorageModule + crate::math::MathModule,
{
/// 从缓存字段计算 - 无额外存储读取
pub fn exchange_rate(&self) -> BigUint<C::Api> {
if self.total_shares == 0u64 {
return BigUint::from(1u64);
}
&self.total_deposited / &self.total_shares
}
/// 使用缓存数据和合约的数学模块计算
pub fn calculate_fee(&self, amount: &BigUint<C::Api>) -> BigUint<C::Api> {
(amount * self.fee_rate_bps) / 10_000u64
}
}
关键洞察:缓存可以持有对合约的引用(sc_ref)并调用其模块方法。这允许您使用缓存字段和共享数学特质计算衍生值 — 而无需额外存储读取。
选择性写回
当只有某些字段可变时,避免写回未更改的字段:
impl<C> Drop for StorageCache<'_, C>
where
C: crate::storage::StorageModule,
{
fn drop(&mut self) {
// 只写回可能更改的字段
self.sc_ref.balance().set(&self.balance);
self.sc_ref.total_shares().set(&self.total_shares);
// 不要写回:self.config_params(在此上下文中为只读,永不更改)
}
}
何时使用 vs 直接存储访问
| 场景 | 方法 |
|---|---|
| 端点读取3+个存储值 | 使用缓存 |
| 单个存储读取/写入 | 直接访问即可 |
| 视图函数读取多个值 | 只读缓存(无Drop) |
| 多个端点共享相同状态 | 创建共享缓存结构体 |
| 异步调用边界 | 在异步调用前手动删除缓存 |
反模式
1. 跨异步边界缓存
// 错误 - async_call_and_exit() 终止执行,drop() 从不运行!
// 缓存写入丢失,不是过时。
fn bad_async(&self) {
let mut cache = StorageCache::new(self);
cache.balance += &deposit;
// 这终止执行 — cache.drop() 从不触发!
self.tx().to(&other).typed(Proxy).call()
.callback(self.callbacks().on_done())
.async_call_and_exit();
// cache 从未删除 — 余额更改丢失!
}
// 正确 - 在异步调用前手动删除缓存
fn good_async(&self) {
let deposit = self.call_value().egld_value().clone_value();
{
let mut cache = StorageCache::new(self);
cache.balance += &deposit;
// cache.drop() 在此处作用域结束时触发 — 写入提交
}
// 现在安全进行异步调用
self.tx().to(&other).typed(Proxy).call()
.callback(self.callbacks().on_done())
.async_call_and_exit();
}
坏 — 在异步边界持有缓存
// 不要:缓存从未删除 — 写入静默丢失
fn bad(&self) {
let mut cache = StorageCache::new(self);
cache.balance += &amount;
self.tx().to(&other).typed(Proxy).call()
.callback(self.callbacks().on_done())
.async_call_and_exit(); // 执行停止 — drop() 从不运行!
}
好 — 克隆/作用域在异步前
// 做:作用域缓存以便drop()在异步调用前触发
fn good(&self) {
{
let mut cache = StorageCache::new(self);
cache.balance += &amount;
} // drop() 在此处触发 — 写入提交到存储
self.tx().to(&other).typed(Proxy).call()
.callback(self.callbacks().on_done())
.async_call_and_exit();
}
2. 在Drop中忘记字段
// 错误 - 忘记写回field_c
impl<C> Drop for StorageCache<'_, C> {
fn drop(&mut self) {
self.sc_ref.field_a().set(&self.field_a);
self.sc_ref.field_b().set(&self.field_b);
// 错误:field_c更改丢失!
}
}
3. 写回不可变配置
// 错误 - 配置很少更改,不要每次都写回
impl<C> Drop for Cache<'_, C> {
fn drop(&mut self) {
self.sc_ref.config_params().set(&self.config_params); // 不必要的写入!
}
}
模板:入门缓存
multiversx_sc::imports!();
use multiversx_sc::derive_imports::*;
pub struct StorageCache<'a, C>
where
C: crate::storage::StorageModule,
{
sc_ref: &'a C,
// 在此处添加您的缓存字段
pub field_a: BigUint<C::Api>,
pub field_b: BigUint<C::Api>,
}
impl<'a, C> StorageCache<'a, C>
where
C: crate::storage::StorageModule,
{
pub fn new(sc_ref: &'a C) -> Self {
StorageCache {
field_a: sc_ref.field_a().get(),
field_b: sc_ref.field_b().get(),
sc_ref,
}
}
}
impl<C> Drop for StorageCache<'_, C>
where
C: crate::storage::StorageModule,
{
fn drop(&mut self) {
self.sc_ref.field_a().set(&self.field_a);
self.sc_ref.field_b().set(&self.field_b);
}
}