file interface ICache { void PrintKeys(); void Set(string key, object? value); bool TryGetValue<TValue>(string key, [MaybeNullWhen(false)] out TValue value); } file sealed class LruCache(int maxSize) : ICache { private readonly ConcurrentDictionary<string, object?> _store = new(); private readonly PriorityQueue<string, long> _priorityQueue = new PriorityQueue<string, long>(); private readonly JsonSerializerOptions _jsonSerializerOptions = new() { WriteIndented = true }; public ICollection<string> Keys => _store.Keys; public void PrintKeys() { Console.WriteLine("PrintKeys:"); Console.WriteLine(JsonSerializer.Serialize(_store, _jsonSerializerOptions)); } public void Set(string key, object? value) { if (_store.ContainsKey(key)) { _store[key] = value; UpdateKeyAccess(key); return; } while (_store.Count >= maxSize) { var keyToRemove = _priorityQueue.Dequeue(); _store.TryRemove(keyToRemove, out _); } _store[key] = value; UpdateKeyAccess(key); } public bool TryGetValue<TValue>(string key, [MaybeNullWhen(false)] out TValue value) { if (!_store.TryGetValue(key, out var cacheValue)) { value = default; return false; } UpdateKeyAccess(key); // 堆代码 duidaima.com value = (TValue)cacheValue!; return true; } private void UpdateKeyAccess(string key) { _priorityQueue.Remove(key, out _, out _); _priorityQueue.Enqueue(key, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); } }cache 主要定义了 Get/Set 两个方法,LruCache 设置了最大的 cache 数量,简单的按照 key 的数量来计算,没有计算实际的内存占用等用了一个 ConcurrentDictionary 来存 cache,这里的 cache 出于简单考虑不设置过期时间,只根据 cache keys limit 来计算,使用 PriorityQueue 来保存缓存最近一次访问的时间以根据访问的时间来决定需要驱逐的时候谁应该被驱逐。
var cache = new LruCache(2); cache.Set("name", "test"); cache.Set("age", "10"); cache.PrintKeys(); ConsoleHelper.HandleInputLoop(x => { if (x.StartsWith("get:")) { var key = x["get:".Length..]; cache.TryGetValue(key, out string? value); Console.WriteLine($"key: {key}, value: {value}"); return; } cache.Set(x, "Hello .NET"); cache.PrintKeys(); }, "Input something to test lruCache, starts with 'get:' to try get cache value, otherwise set cache, input q to exit"); ConsoleHelper.ReadLine();ConsoleHelper.HandInputLoop 是一个帮助方法,可以不断的从 console 读取 Input 并进行一定的处理,默认输出 q 来退出循环,我们执行一下看下效果
首先先打印了一下缓存的数据,我们开始设置了 age 和 name 两个缓存值,然后通过输入循环来设置新的缓存和访问某一个缓存。
1.首先我们设置了一个 aa 缓存,因为我们设置了最多保留两个缓存而且缓存中已经有了两个缓存,这样我们就需要驱逐一个 key 才能设置新的 key,首先加入的 key 被认为是最早访问的一个 key 应该被驱逐,所以 name 被移除了,然后新的 key aa 被加入到缓存中。
2.之后我们再次设置了 aa ,因为之前已经在缓存里了,无需驱逐缓存,可以直接更新已有的缓存并更新最后访问的时间file sealed record CacheAccessEntry { public int AccessCount { get; set; } public long AccessTimestamp { get; set; } } file sealed class CacheAccessEntryComparer(Func<CacheAccessEntry, long>? priorityFactory) : IComparer<CacheAccessEntry> { public CacheAccessEntryComparer() : this(null) { } public int Compare(CacheAccessEntry? x, CacheAccessEntry? y) { ArgumentNullException.ThrowIfNull(x); ArgumentNullException.ThrowIfNull(y); if (priorityFactory is not null) { return priorityFactory(x).CompareTo(priorityFactory(y)); } if (x.AccessCount == y.AccessCount) { return x.AccessTimestamp == y.AccessTimestamp ? 0 : (x.AccessTimestamp > y.AccessTimestamp ? 1 : -1) ; } return x.AccessCount > y.AccessCount ? 1 : -1; } } file sealed class LruCacheV2(int maxSize) : ICache { private readonly ConcurrentDictionary<string, object?> _store = new(); private static readonly CacheAccessEntryComparer CacheEntryComparer = new(); private readonly PriorityQueue<string, CacheAccessEntry> _priorityQueue = new(CacheEntryComparer); public ICollection<string> Keys => _store.Keys; public void PrintKeys() { Console.WriteLine("PrintKeys:"); Console.WriteLine(JsonSerializer.Serialize(_store)); } public void Set(string key, object? value) { if (_store.ContainsKey(key)) { _store[key] = value; UpdateKeyAccess(key); return; } while (_store.Count >= maxSize) { var keyToRemove = _priorityQueue.Dequeue(); _store.TryRemove(keyToRemove, out _); } _store[key] = value; UpdateKeyAccess(key); } public bool TryGetValue<TValue>(string key, [MaybeNullWhen(false)] out TValue value) { if (!_store.TryGetValue(key, out var cacheValue)) { value = default; return false; } UpdateKeyAccess(key); value = (TValue)cacheValue!; return true; } private void UpdateKeyAccess(string key) { _priorityQueue.Remove(key, out _, out var entry); // 堆代码 duidaima.com entry ??= new(); entry.AccessTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); entry.AccessCount += 1; _priorityQueue.Enqueue(key, entry); } }使用示例和前面类似,只需要把 LruCache 改成 LruCacheV2 即可
var cache = new LruCacheV2(2); cache.Set("name", "test"); cache.Set("age", "10"); cache.PrintKeys(); ConsoleHelper.HandleInputLoop(x => { if (x.StartsWith("get:")) { var key = x["get:".Length..]; cache.TryGetValue(key, out string? value); Console.WriteLine($"key: {key}, value: {value}"); return; } cache.Set(x, "Hello .NET"); cache.PrintKeys(); }, "Input something to test lruCache, starts with 'get:' to try get cache value, otherwise set cache, input q to exit");执行结果如下: