如何通过.net-core-2.1

时间:2018-07-20 22:50:55

标签: caching asp.net-core-2.0

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

在.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秒钟的充满活力的幸福注入请求之后,由于缓存未命中以及随后的数据加载,所缓存的项目已过期并且下一个请求被暂停。

啊!因此缓存有时只能工作。为什么不选择让它一直运行呢?

如何向以下功能添加功能

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

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

  1. 当缓存的项目过期时,它将被逐出并且不再可用;表示我无法退货。
  2. 更新需要数据库的缓存项会导致这些调用超出范围。

此缓存更新可以在注意到缓存未命中后立即开始,也可以通过主动监视下一项过期来开始。

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

2 个答案:

答案 0 :(得分:0)

我找到了一种可行的解决方案。

  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方法。请注意,我正在Func<IDataContext, Object>中使用委托(CacheEntry)作为值填充工厂。这允许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设置中将类注册为Singletons
        services.AddOptions();
        services.Configure<Settings>(Configuration);
        services.AddLogging();
        services.AddDbContext<DataContext>();
        services.AddSingleton<ICache, Cache>();
        services.AddSingleton<IHostedService, CacheUpdateService>();

答案 1 :(得分:0)

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