BoundedLruCache Cache Utility
BoundedLruCache<TKey, TValue> is a thread-safe LRU (Least Recently Used) cache implementation with capacity limits and an optional segment strategy.
Namespace
Section titled “Namespace”using MiCake.Util.Cache;Constructor
Section titled “Constructor”public BoundedLruCache( int maxSize = 1000, // Maximum number of cache entries int? segments = null, // Number of segments (to improve concurrency performance) bool useLockFreeApproximation = false // Whether to use the lock-free approximation algorithm)Parameter description:
maxSize: the maximum number of cache entries. When this number is exceeded, the least recently used items are removedsegments: the number of segments, used to improve concurrency performance. Small caches (< 16) automatically use a single segmentuseLockFreeApproximation: whether to use the lock-free approximation algorithm, suitable for high-concurrency scenarios
Core Methods
Section titled “Core Methods”GetOrAdd - Get or Add a Cache Item
Section titled “GetOrAdd - Get or Add a Cache Item”var cache = new BoundedLruCache<string, Product>(maxSize: 500);
var product = cache.GetOrAdd("product-1", key =>{ // Executed on a cache miss return LoadProductFromDatabase(key);});
// Async versionvar product = await cache.GetOrAdd("product-1", async key =>{ return await LoadProductFromDatabaseAsync(key);});TryGetValue - Try to Get a Cache Item
Section titled “TryGetValue - Try to Get a Cache Item”if (cache.TryGetValue("product-1", out var product)){ Console.WriteLine($"Cache hit: {product.Name}");}else{ Console.WriteLine("Cache miss");}AddOrUpdate - Add or Update a Cache Item
Section titled “AddOrUpdate - Add or Update a Cache Item”// Add a new item or update an existing onecache.AddOrUpdate("product-1", newProduct);Remove - Remove a Cache Item
Section titled “Remove - Remove a Cache Item”bool removed = cache.Remove("product-1");if (removed){ Console.WriteLine("Cache item removed");}Clear - Clear the Cache
Section titled “Clear - Clear the Cache”cache.Clear();Properties
Section titled “Properties”| Property | Description |
|---|---|
Count |
The current number of cache items |
MaxSize |
The maximum capacity |
Usage Examples
Section titled “Usage Examples”Basic Usage
Section titled “Basic Usage”// Create a cache instancevar cache = new BoundedLruCache<int, Product>(maxSize: 1000);
// Get or addvar product = cache.GetOrAdd(productId, id => _repository.FindAsync(id).Result);
// Check whether it existsif (cache.TryGetValue(productId, out var cachedProduct)){ return cachedProduct;}
// Dispose after usecache.Dispose();High-Concurrency Scenarios
Section titled “High-Concurrency Scenarios”// Use segments and the lock-free approximation algorithm to improve performancevar cache = new BoundedLruCache<string, Product>( maxSize: 10000, segments: 4, // 4 segments useLockFreeApproximation: true);
// Thread-safe cache operationsParallel.For(0, 1000, i =>{ var product = cache.GetOrAdd($"product-{i}", key => { return new Product { Id = i, Name = $"Product {i}" }; });});Using in a Service
Section titled “Using in a Service”public class ProductService : IScopedService{ private readonly BoundedLruCache<int, Product> _cache; private readonly IRepository<Product, int> _repository;
public ProductService(IRepository<Product, int> repository) { _repository = repository; _cache = new BoundedLruCache<int, Product>(maxSize: 500); }
public async Task<Product> GetProduct(int id) { return await _cache.GetOrAdd(id, async productId => { var product = await _repository.FindAsync(productId); if (product == null) throw new NotFoundException("Product", productId); return product; }); }
public void InvalidateCache(int productId) { _cache.Remove(productId); }
public void ClearCache() { _cache.Clear(); }}Registering as a Singleton
Section titled “Registering as a Singleton”public class MyModule : MiCakeModule{ public override void ConfigureServices(ModuleConfigServiceContext context) { // Register as a singleton context.Services.AddSingleton(sp => new BoundedLruCache<string, CachedData>(maxSize: 1000));
base.ConfigureServices(context); }}Cache Invalidation Strategy
Section titled “Cache Invalidation Strategy”public class CacheService{ private readonly BoundedLruCache<string, CachedItem> _cache;
public CacheService() { _cache = new BoundedLruCache<string, CachedItem>(maxSize: 1000); }
public CachedItem GetOrCreate(string key, TimeSpan expiration) { return _cache.GetOrAdd(key, k => { var item = new CachedItem { Data = LoadData(k), ExpiresAt = DateTime.UtcNow.Add(expiration) }; return item; }); }
public void RemoveExpired() { // Periodically clean up expired items // Note: BoundedLruCache does not support built-in expiration, this needs to be implemented manually }}How LRU Works
Section titled “How LRU Works”The LRU (Least Recently Used) algorithm automatically removes the least recently accessed cache items:
Cache capacity: 3
1. Add A → [A]2. Add B → [B, A]3. Add C → [C, B, A]4. Access A → [A, C, B] // A is moved to the front5. Add D → [D, A, C] // B is removed (least recently used)Segment Strategy
Section titled “Segment Strategy”When the cache capacity is large, using segments can reduce lock contention:
// No segments (suitable for small caches)var smallCache = new BoundedLruCache<string, int>(maxSize: 100);
// 4 segments (suitable for medium caches)var mediumCache = new BoundedLruCache<string, int>( maxSize: 1000, segments: 4);
// 8 segments (suitable for large caches)var largeCache = new BoundedLruCache<string, int>( maxSize: 10000, segments: 8);Best Practices
Section titled “Best Practices”1. Set the Capacity Reasonably
Section titled “1. Set the Capacity Reasonably”// ✅ Correct: set the capacity based on actual requirementsvar cache = new BoundedLruCache<int, Product>( maxSize: EstimateRequiredCapacity());
// ❌ Wrong: capacity too small causes frequent evictionvar cache = new BoundedLruCache<int, Product>(maxSize: 10);
// ❌ Wrong: capacity too large consumes too much memoryvar cache = new BoundedLruCache<int, Product>(maxSize: 1000000);2. Register as a Singleton
Section titled “2. Register as a Singleton”// ✅ Correct: register as a singleton in the DI containerservices.AddSingleton<BoundedLruCache<string, CachedData>>(sp => new BoundedLruCache<string, CachedData>(maxSize: 1000));
// ❌ Wrong: create a new instance every timeservices.AddScoped<BoundedLruCache<string, CachedData>>(sp => new BoundedLruCache<string, CachedData>(maxSize: 1000));3. Release Resources Promptly
Section titled “3. Release Resources Promptly”// ✅ Correct: use using or call Dispose manuallyusing (var cache = new BoundedLruCache<int, Data>(maxSize: 100)){ // Use the cache}
// Orvar cache = new BoundedLruCache<int, Data>(maxSize: 100);try{ // Use the cache}finally{ cache.Dispose();}4. Cache Data That Doesn’t Change Frequently
Section titled “4. Cache Data That Doesn’t Change Frequently”// ✅ Suitable for caching- Configuration data- Dictionary data- Product information- Basic user information
// ❌ Not suitable for caching- Real-time data- Frequently updated data- Large objects (> 1MB)5. Idempotent Factories in Lock-Free Mode
Section titled “5. Idempotent Factories in Lock-Free Mode”// ⚠️ In lock-free mode, the factory method may be called multiple timesvar cache = new BoundedLruCache<int, Product>( maxSize: 1000, useLockFreeApproximation: true);
// ✅ Correct: use an idempotent factorycache.GetOrAdd(id, k => _repository.Find(k)); // multiple calls return the same result
// ❌ Wrong: a non-idempotent factorycache.GetOrAdd(id, k =>{ var product = new Product(); product.Id = GenerateNewId(); // generates a different ID on each call return product;});Performance Considerations
Section titled “Performance Considerations”| Scenario | Configuration suggestion |
|---|---|
| Small cache (< 100) | Default configuration is fine |
| Medium cache (100-1000) | segments: 2-4 |
| Large cache (> 1000) | segments: 4-8 |
| Extremely high concurrency | useLockFreeApproximation: true |
Important Notes
Section titled “Important Notes”- Capacity limit: when the cache is full, the least recently accessed items are automatically removed
- Segment strategy: small caches (maxSize < 16) use a single segment to guarantee deterministic LRU semantics
- Lock-free mode: the factory method may be called multiple times, it is recommended to use an idempotent factory
- Thread safety: all operations are thread-safe
- Memory management: release cache instances that are no longer used promptly
