【问题标题】:How to avoid cache misses with memory cache in .net-core-2.1如何在 .net-core-2.1 中使用内存缓存避免缓存未命中
【发布时间】:2018-07-20 22:50:55
【问题描述】:

如何在后台更新缓存以避免缓存未命中?

在 .net-core-2.1 中,我可以像这样添加内存缓存:

public class Startup
{    
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMemoryCache();
    }
}

那么使用起来就很简单了:

[Route("api")]
public class DataController : Controller
{
    private readonly IMemoryCache _cache;
    private readonly DataContext _dataContext;

    public DataController(IMemoryCache cache, DataContext dataContext)
    {
        _cache = cache;
        _dataContext = dataContext;
    }

    [HttpGet]
    [Route("GimmeCachedData")]
    public async Task<IActionResult> Get()
    {
        var cacheEntry = await
            _cache.GetOrCreateAsync("MyCacheKey", entry =>
            {
                entry.AbsoluteExpiration = DateTime.Now.AddSeconds(20);
                return Task.FromResult(_dataContext.GetOrders(DateTime.Now));
            });

        return Ok(cacheEntry);
    }
}

但是,在 20 秒的高速缓存驱动 bliss 注入请求之后,正如预期的那样,缓存项已过期,并且下一个请求由于缓存未命中和后续数据加载而停止。

啊!所以缓存有时只能工作。为什么不能让它一直工作?

如何添加功能:

  1. 退回旧物品(同时)并且
  2. 当项目过期或注意到过期时自动更新缓存,以便下一个请求将获得更新的值?

在尝试解决这个问题时,我在使用IHostedService 的实现过程中遇到了两个主要障碍:

  1. 当缓存项过期时,它会被驱逐并且不再可用;这意味着我不能退货。
  2. 更新需要数据库的缓存项会导致这些调用超出范围。

此缓存更新可以在发现缓存未命中后直接启动,也可以通过主动监控下一个过期项目来启动。

我尝试使用ConcurrentDictionary&lt;String, CacheItem&gt; 滚动我自己的缓存(将其添加为单例)。 CacheItem 类包含 ValueExpirationFactory 的属性(即:返回值委托)。但我发现,由于这个委托可能是在请求时设置并在IHostedService 后台线程中调用的,因此导致上下文超出范围异常。

【问题讨论】:

    标签: caching asp.net-core-2.0


    【解决方案1】:

    我找到了一个似乎可行的解决方案。

    1. 实现 IHostedService(扩展自 BackgroundService 类)。此类将充当由 .net 核心框架管理的后台线程。后台线程将保持缓存更新(通过调用ICache.UpdateCache,如下所述),以避免请求时缓存命中。
    public class CacheUpdateService : BackgroundService
    {
        private readonly ILogger<CacheUpdateService> _logger;
        private readonly IServiceProvider _serviceProvider;
        private readonly ICache _cache;
    
        public CacheUpdateService(ILogger<CacheUpdateService> logger, IServiceProvider serviceProvider, ICache cache)
        {
            _logger = logger;
            _serviceProvider = serviceProvider;
            _cache = cache;
        }
    
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            _logger.LogDebug("CacheUpdateService is starting.");
    
            stoppingToken.Register(Dispose);
    
            while (!stoppingToken.IsCancellationRequested)
            {
                try
                {
                    using (var scope = _serviceProvider.CreateScope())
                    {
                        var dataContext = scope.ServiceProvider.GetRequiredService<DataContext>();
    
                        // This tight loop calls the UpdateCache, which will block if no updates are necessary
                        await Task.Run(() => _cache.UpdateCache(dataContext), stoppingToken);
                    }
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Exception in the CacheUpdateService");
                }
            }
    
            _logger.LogDebug("CacheUpdateService has stopped.");
        }
    
        public override void Dispose()
        {
            using(var scope = _serviceProvider.CreateScope())
            {
                var scopedProcessingService = scope.ServiceProvider.GetRequiredService<ICache>();
    
                // Dispose here on ICache will release any blocks
                scopedProcessingService.Dispose();
            }
    
            base.Dispose();
        }
    }
    
    1. 下面的Cache 类实现了后台UpdateCache 方法,该方法将一次更新1 个过期项目。优先考虑最过期的一个。它还实现了请求范围的GetOrCreate 方法。注意我在CacheEntry 中使用了一个委托(Func&lt;IDataContext, Object&gt;)作为值人口工厂。这允许Cache 类注入一个适当范围的DataContext(从IHostedService 接收),它还允许调用者指定调用DataContext 的哪个方法以获取特定缓存键值的结果。请注意,我使用AutoResetEvent 来等待第一次数据填充以及启动下一次缓存刷新的计时器。此实现将在第一次调用该项目时遭受缓存未命中(我猜是在超过 1 小时未调用它之后;因为它将在 1 小时后被驱逐。)。
    public class CacheEntry
    {
        public String Key { get; set; }
        public Object Value { get; set; }
        public Boolean Updating { get; set; }
        public Int32 ExpirySeconds { get; set; }
        public DateTime Expiration { get; set; }
        public DateTime LastAccessed { get; set; }
        public Func<IDataContext, Object> ValueFactory { get; set; }
    }
    
    public interface ICache : IDisposable
    {
        void UpdateCache(IDataContext dataContext);
        T GetOrCreate<T>(String key, Func<IDataContext, T> factory, Int32 expirySeconds = 0) where T : class;
    }
    
    public class Cache : ICache
    {
        private readonly ILogger _logger;
        private readonly ConcurrentDictionary<String, CacheEntry> _cache;
        private readonly AutoResetEvent _governor; 
    
        public Cache(ILogger<Cache> logger)
        {
            _logger = logger;
            _cache = new ConcurrentDictionary<String, CacheEntry>();
            _governor = new AutoResetEvent(false);
        }
    
        public void Dispose()
        {
            _governor.Set();
        }
    
        public static Int32 CacheForHour => 3600;
        public static Int32 CacheForDay => 86400;
        public static Int32 CacheIndefinitely => 0;
    
        public void UpdateCache(IDataContext dataContext)
        {
            var evictees = _cache.Values
                .Where(entry => entry.LastAccessed.AddHours(1) < DateTime.Now)
                .Select(entry => entry.Key)
                .ToList();
    
            foreach (var evictee in evictees)
            {
                _logger.LogDebug($"Evicting: {evictee}...");
                _cache.Remove(evictee, out _);
            }
    
            var earliest = _cache.Values
                .Where(entry => !entry.Updating)
                .OrderBy(entry => entry.Expiration)
                .FirstOrDefault();
    
            if (earliest == null || earliest.Expiration > DateTime.Now)
            {
                var timeout = (Int32) (earliest?.Expiration.Subtract(DateTime.Now).TotalMilliseconds ?? -1);
                _logger.LogDebug($"Waiting {timeout}ms for next expiry...");
                _governor.WaitOne(timeout);
                return;
            }
    
            try
            {
                _logger.LogDebug($"Updating cache for: {earliest.Key}...");
                earliest.Updating = true;
                earliest.Value = earliest.ValueFactory(dataContext);
                earliest.Expiration = earliest.ExpirySeconds > 0
                    ? DateTime.Now.AddSeconds(earliest.ExpirySeconds)
                    : DateTime.MaxValue;
                _governor.Set();
            }
            finally
            {
                earliest.Updating = false;
            }
        }
    
        public T GetOrCreate<T>(String key, Func<IDataContext, T> factory, Int32 expirySeconds = -1) where T : class 
        {
            var success = _cache.TryGetValue(key, out var entry);
    
            if (success && entry.Value != null)
            {
                entry.LastAccessed = DateTime.Now;
                return (T) entry.Value;
            }
    
            if (entry == null)
            {
                _logger.LogDebug($"Adding new entry to the cache: {key}...");
                entry = new CacheEntry
                {
                    Key = key,
                    Expiration = DateTime.MinValue,
                    ExpirySeconds = expirySeconds,
                    LastAccessed = DateTime.Now,
                    ValueFactory = factory
                };
    
                _cache.TryAdd(key, entry);
    
                _governor.Set();
            }
    
            while (entry.Value == null)
            {
                _logger.LogDebug($"Waiting for 1st time cache update: {entry.Key}...");
                _governor.WaitOne();
            }
    
            return (T)entry.Value;
        }
    }
    
    1. 然后像这样创建DataContext 类。以Dapper 为例,从数据库中检索数据:
    public class DataContext : DbContext, IDataContext
    {
        private readonly IOptions<Settings> _settings;
        private String _databaseServer;
    
        public DataContext(IOptions<Settings> settings)
        {
            _settings = settings;
        }
    
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            base.OnConfiguring(optionsBuilder);
    
            optionsBuilder.UseSqlServer(_settings.Value.ConnectionString);
        }
    
        public IEnumerable<OrderInfo> GetOrders(DateTime date)
        {
            return Database.GetDbConnection().Query<OrderInfo>(
                $"SchemaName.usp_GetOrders",
                new {Date = date},
                commandType: CommandType.StoredProcedure);
        }
    }
    
    1. 在控制器中,ICache 被注入并使用如下:
        [HttpGet]
        [Route("Orders/{date}")]
        public IActionResult GetOrders(DateTime date)
        {
            var result = _cache.GetOrCreate(
                $"GetOrders_{date:yyyyMMdd}", 
                context => context.GetOrders(date),
                date.Date < DateTime.Today ? Cache.CacheIndefinitely : 20);
    
            return Ok(result);
        }
    
    1. 最后在 DI 设置中将类注册为单例
            services.AddOptions();
            services.Configure<Settings>(Configuration);
            services.AddLogging();
            services.AddDbContext<DataContext>();
            services.AddSingleton<ICache, Cache>();
            services.AddSingleton<IHostedService, CacheUpdateService>();
    

    【讨论】:

    • 这正是我想要的。
    【解决方案2】:

    我创建了一个类似的基于 IHostedService 的项目 https://github.com/dpatekar/CacheAnt 看一下,使用起来非常简单。 它也可以作为 NuGet 包使用。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-08-19
      • 2019-03-05
      • 2016-12-19
      • 2012-04-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-12-05
      相关资源
      最近更新 更多