内存缓存MemoryCache实现了ICache接口,Redis同样实现了ICache接口,两者在缓存操作上达到了高度抽象统一。应用设计时一律使用ICache接口,开发环境装配为MemoryCache,生产环境根据分布式需要可以装配为Redis。如果应用系统没有分布式需求,继续使用MemoryCache更好。

Nuget包:NewLife.Core

源码:https://github.com/NewLifeX/X/blob/master/NewLife.Core/Caching/MemoryCache.cs

视频:https://www.bilibili.com/video/BV1aW4y1H7CR

超高性能

MemoryCache核心是并发字典ConcurrentDictionary,由于省去了序列化和网络通信,使得它具有千万级超高性能(普通台式机实测2.87亿tps)。

MemoryCache支持过期时间,默认容量10万个,未过期key超过该值后,每60秒根据LRU清理溢出部分。

常用于进程内千万级以下数据缓存场景。

本机测试数据如下(I9-10900K):

Test v1.0.0.1130 Build 2021-01-31 19:33:32 .NETCoreApp,Version=v5.0
Test 
Memory性能测试[顺序],批大小[0],逻辑处理器 20 个
测试 10,000,000 项,  1 线程
赋值 耗时     934ms 速度 10,706,638 ops
读取 耗时     989ms 速度 10,111,223 ops
删除 耗时     310ms 速度 32,258,064 ops
累加 耗时     584ms 速度 17,123,287 ops
测试 20,000,000 项,  2 线程
赋值 耗时     927ms 速度 21,574,973 ops
读取 耗时   1,024ms 速度 19,531,250 ops
删除 耗时     319ms 速度 62,695,924 ops
累加 耗时     594ms 速度 33,670,033 ops
测试 40,000,000 项,  4 线程
赋值 耗时   1,011ms 速度 39,564,787 ops
读取 耗时   1,039ms 速度 38,498,556 ops
删除 耗时   1,636ms 速度 24,449,877 ops
累加 耗时     608ms 速度 65,789,473 ops
测试 80,000,000 项,  8 线程
赋值 耗时     989ms 速度 80,889,787 ops
读取 耗时   1,227ms 速度 65,199,674 ops
删除 耗时   1,858ms 速度 43,057,050 ops
累加 耗时     675ms 速度 118,518,518 ops
测试 200,000,000 项, 20 线程
赋值 耗时   1,644ms 速度 121,654,501 ops
读取 耗时   1,807ms 速度 110,680,686 ops
删除 耗时   2,936ms 速度 68,119,891 ops
累加 耗时   1,569ms 速度 127,469,725 ops
测试 200,000,000 项, 64 线程
赋值 耗时   1,686ms 速度 118,623,962 ops
读取 耗时   1,877ms 速度 106,553,010 ops
删除 耗时     695ms 速度 287,769,784 ops
累加 耗时   1,585ms 速度 126,182,965 ops
总测试数据:2,200,000,042


ICache接口

ICache是缓存抽象接口,主要实现是MemoryCache和Redis

/// <summary>缓存接口</summary>
public interface ICache
{
    #region 属性
    /// <summary>名称</summary>
    String Name { get; }

    /// <summary>默认缓存时间。默认0秒表示不过期</summary>
    Int32 Expire { get; set; }

    /// <summary>获取和设置缓存,永不过期</summary>
    /// <param name="key"></param>
    /// <returns></returns>
    Object this[String key] { get; set; }

    /// <summary>缓存个数</summary>
    Int32 Count { get; }

    /// <summary>所有键</summary>
    ICollection<String> Keys { get; }
    #endregion

    #region 基础操作
    /// <summary>是否包含缓存项</summary>
    /// <param name="key"></param>
    /// <returns></returns>
    Boolean ContainsKey(String key);

    /// <summary>设置缓存项</summary>
    /// <param name="key">键</param>
    /// <param name="value">值</param>
    /// <param name="expire">过期时间,秒。小于0时采用默认缓存时间<seealso cref="Expire"/></param>
    /// <returns></returns>
    Boolean Set<T>(String key, T value, Int32 expire = -1);

    /// <summary>设置缓存项</summary>
    /// <param name="key">键</param>
    /// <param name="value">值</param>
    /// <param name="expire">过期时间</param>
    /// <returns></returns>
    Boolean Set<T>(String key, T value, TimeSpan expire);

    /// <summary>获取缓存项</summary>
    /// <param name="key">键</param>
    /// <returns></returns>
    T Get<T>(String key);

    /// <summary>批量移除缓存项</summary>
    /// <param name="keys">键集合</param>
    /// <returns></returns>
    Int32 Remove(params String[] keys);

    /// <summary>清空所有缓存项</summary>
    void Clear();

    /// <summary>设置缓存项有效期</summary>
    /// <param name="key">键</param>
    /// <param name="expire">过期时间</param>
    Boolean SetExpire(String key, TimeSpan expire);

    /// <summary>获取缓存项有效期</summary>
    /// <param name="key">键</param>
    /// <returns></returns>
    TimeSpan GetExpire(String key);
    #endregion

    #region 集合操作
    /// <summary>批量获取缓存项</summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="keys"></param>
    /// <returns></returns>
    IDictionary<String, T> GetAll<T>(IEnumerable<String> keys);

    /// <summary>批量设置缓存项</summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="values"></param>
    /// <param name="expire">过期时间,秒。小于0时采用默认缓存时间<seealso cref="Expire"/></param>
    void SetAll<T>(IDictionary<String, T> values, Int32 expire = -1);

    /// <summary>获取列表</summary>
    /// <typeparam name="T">元素类型</typeparam>
    /// <param name="key">键</param>
    /// <returns></returns>
    IList<T> GetList<T>(String key);

    /// <summary>获取哈希</summary>
    /// <typeparam name="T">元素类型</typeparam>
    /// <param name="key">键</param>
    /// <returns></returns>
    IDictionary<String, T> GetDictionary<T>(String key);

    /// <summary>获取队列</summary>
    /// <typeparam name="T">元素类型</typeparam>
    /// <param name="key">键</param>
    /// <returns></returns>
    IProducerConsumer<T> GetQueue<T>(String key);

    /// <summary>获取栈</summary>
    /// <typeparam name="T">元素类型</typeparam>
    /// <param name="key">键</param>
    /// <returns></returns>
    IProducerConsumer<T> GetStack<T>(String key);

    /// <summary>获取Set</summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="key"></param>
    /// <returns></returns>
    ICollection<T> GetSet<T>(String key);
    #endregion

    #region 高级操作
    /// <summary>添加,已存在时不更新</summary>
    /// <typeparam name="T">值类型</typeparam>
    /// <param name="key">键</param>
    /// <param name="value">值</param>
    /// <param name="expire">过期时间,秒。小于0时采用默认缓存时间<seealso cref="Cache.Expire"/></param>
    /// <returns></returns>
    Boolean Add<T>(String key, T value, Int32 expire = -1);

    /// <summary>设置新值并获取旧值,原子操作</summary>
    /// <remarks>
    /// 常常配合Increment使用,用于累加到一定数后重置归零,又避免多线程冲突。
    /// </remarks>
    /// <typeparam name="T">值类型</typeparam>
    /// <param name="key">键</param>
    /// <param name="value">值</param>
    /// <returns></returns>
    T Replace<T>(String key, T value);

    /// <summary>尝试获取指定键,返回是否包含值。有可能缓存项刚好是默认值,或者只是反序列化失败,解决缓存穿透问题</summary>
    /// <typeparam name="T">值类型</typeparam>
    /// <param name="key">键</param>
    /// <param name="value">值。即使有值也不一定能够返回,可能缓存项刚好是默认值,或者只是反序列化失败</param>
    /// <returns>返回是否包含值,即使反序列化失败</returns>
    Boolean TryGetValue<T>(String key, out T value);

    /// <summary>累加,原子操作</summary>
    /// <param name="key">键</param>
    /// <param name="value">变化量</param>
    /// <returns></returns>
    Int64 Increment(String key, Int64 value);

    /// <summary>累加,原子操作</summary>
    /// <param name="key">键</param>
    /// <param name="value">变化量</param>
    /// <returns></returns>
    Double Increment(String key, Double value);

    /// <summary>递减,原子操作</summary>
    /// <param name="key">键</param>
    /// <param name="value">变化量</param>
    /// <returns></returns>
    Int64 Decrement(String key, Int64 value);

    /// <summary>递减,原子操作</summary>
    /// <param name="key">键</param>
    /// <param name="value">变化量</param>
    /// <returns></returns>
    Double Decrement(String key, Double value);
    #endregion

    #region 事务
    /// <summary>提交变更。部分提供者需要刷盘</summary>
    /// <returns></returns>
    Int32 Commit();

    /// <summary>申请分布式锁</summary>
    /// <param name="key">要锁定的key</param>
    /// <param name="msTimeout"></param>
    /// <returns></returns>
    IDisposable AcquireLock(String key, Int32 msTimeout);
    #endregion

    #region 性能测试
    /// <summary>多线程性能测试</summary>
    /// <param name="rand">随机读写。顺序,每个线程多次操作一个key;随机,每个线程每次操作不同key</param>
    /// <param name="batch">批量操作。默认0不分批,分批仅针对随机读写,对顺序读写的单key操作没有意义</param>
    Int64 Bench(Boolean rand = false, Int32 batch = 0);
    #endregion
}


基本用法

添删改查基本功能,Get/Set/Count/ContainsKey/Remove

var ic = new MemoryCache();
var key = "Name";
var key2 = "Company";

ic.Set(key, "大石头");
ic.Set(key2, "新生命");
Assert.Equal("大石头", ic.Get<String>(key));
Assert.Equal("新生命", ic.Get<String>(key2));

var count = ic.Count;
Assert.True(count >= 2);

// Keys
var keys = ic.Keys;
Assert.True(keys.Contains(key));

// 过期时间
ic.SetExpire(key, TimeSpan.FromSeconds(1));
var ts = ic.GetExpire(key);
Assert.True(ts.TotalSeconds > 0 && ts.TotalSeconds < 2, "过期时间");

var rs = ic.Remove(key2);
Assert.Equal(1, rs);

Assert.False(ic.ContainsKey(key2));

ic.Clear();
Assert.True(ic.Count == 0);

其中Set的第三个参数支持过期时间,单位秒。

集合操作

SetAll/GetAll 是高吞吐的关键,其中SetAll第二参数支持过期时间,单位秒

var ic = new MemoryCache();

var dic = new Dictionary<String, String>
{
    ["111"] = "123",
    ["222"] = "abc",
    ["大石头"] = "学无先后达者为师"
};

ic.SetAll(dic);
var dic2 = ic.GetAll<String>(dic.Keys);

Assert.Equal(dic.Count, dic2.Count);
foreach (var item in dic)
{
    Assert.Equal(item.Value, dic2[item.Key]);
}


高级操作

MemoryCache有几个非常好用的高级操作,全部都是线程安全:

  • Add。添加,已存在时不更新,常用于锁争夺。例如,可用于判断指定订单是否处理过,加上过期时间,就是我们经常说的多少小时去重。
  • Replace。设置新值并获取旧值,原子操作
  • TryGetValue。尝试获取指定键,返回是否包含值。有可能缓存项刚好是默认值
  • Increment。累加
  • Decrement。累减


缓存过期策略

MemoryCache内置LRU淘汰算法,当缓存项超过最大值Capacity(默认10万)时,剔除最久未使用的缓存项,以避免内存占用过大。

缓存项未达到最大值Capacity时,MemoryCache定时检查并剔除过期项。