Чтение MemoryCache или InMemory автоматически для таблиц поиска в базе данных - PullRequest
0 голосов
/ 03 октября 2019

Вместо непосредственного чтения таблиц поиска в базе данных команда хочет применить (a) MemoryCache или (b) Entity Framework InMemoryDatabase / SQLite для уменьшения использования базы данных и увеличения производительности памяти.

Как автоматически переслать существующие таблицы поиска DbContext Linq-запросы для чтения из памяти? Таблицы поиска меняются только несколько раз в год, в основном они статические, AddressCategory, CustomerStatus, ProductType. База данных состоит из таблиц больших транзакций, которые должны быть считаны из базы данных, и таблиц поиска, которые должны быть считаны из памяти.

Примечание. Компания имеет существующие запросы Linq и хочет автоматически переслать существующий код в кэш.

Метод 1: предлагаемое решение EF InMemory / SQLite:

  1. Копирование / вставка оригинального DbContext (StoreContext) в новую копию DbContext, называемую StoryMemoryContext. В Startup.cs

    services.AddDbContext<StoreContext>(options => options.UseSqlServer(@"Server=localhost;Database=StoreDatabase";));
    
    services.AddDbContext<StoreMemoryContext>(options => options.UseInMemoryDatabase(databaseName: "StoreMemoryContext"));
    
    services.AddDbContext<CustomDbContext>(options => options.UseSqlServer(@"Server=localhost;Database=StoreDatabase")));
    
  2. Сохранить миниатюрные таблицы поиска Lookup в InMemoryDatabase 'StoryMemoryContext' (около 5 Мб максимум).

  3. Имейте CustomDbContext наследовать от оригинала, где Original StoreContext пересылает методы доступа к StoryMemoryContext.

Циклический код всех таблиц поиска с T4 или Powershell Automation

public class CustomDBContext : StoreContext
{
    public StoreMemoryContext _storeMemoryContext; = new StoreMemoryContext();
    public CustomDBContext()
    {
    }

    public override DbSet<ProductType> ProductType
    {
        set
        {
            base.ProductType = value;
            _storeMemoryContext.ProductType = value;
        }
        get
        {
            return _storeMemoryContext.ProductType;
        }
    }

Вопросы:

1) Будет ли работать этот метод Get? Есть вопросы для исследования? Не стесняйтесь пересматривать / редактировать в ответе. Открытое для любого решения не только это,

Get-аксессоры, кажется, работают.

  • При выполнении _customDbContext.ProductType.ToList() он читает InMemory EF.

  • _customDbContext.CustomerTransaction.Include(c => c.ProductType).ToListAsync() прочитает базу данных, соответствующую предполагаемому поведению. Не нужно читать транзакции клиентов из больших таблиц без просмотра из базы данных InMemory.

2) Установка Accessors работает только частично, так как существует множество способов изменить DBSet и DBContext: Add, AddRange, Удалить, Добавить Entity Graph (сложно отследить) Физические справочные таблицы меняются только пару раз в год. Мысль об ChangeTracker, однако для размещения оператора If после каждого SaveChanges() для проверки, изменилась ли таблица поиска / затем обновить базу данных InMemory, это может замедлить работу приложения, поскольку у нас 500 транзакций в секунду. (это то, что мне сказали - желая услышать больше мнений о ChangeTracker и влиянии на производительность / скорость) Примечание. Физические таблицы и таблицы поиска InMemory могут иметь 30-минутный период синхронизации между двумя в соответствии с требованиями клиента, поскольку таблицы поиска редко изменяются. Однако почти мгновенная синхронизация данных между двумя является целевым решением для вопроса (<5 секунд). </p>

Метод 2: MemoryCache не работает: Похоже, создайте Inherited / custom DbContext, где чтение переопределяет впередтаблицы поиска в MemoryCache. Хранение DBContext в MemoryCache хранит только IQueryable, а не фактические материализованные значения. Переход от контроллера к контроллеру обновляет DbContext и очищает кеш, не стесняйтесь пересматривать / редактировать в ответе.

public class CustomDBContext : EcommerceContext
{
    public CustomDBContext(IMemoryCache memoryCache)
    {
        _memoryCache = memoryCache;
    }

    public override DbSet<ProductType> ProductType
    {
        get
        {
            return _memoryCache.Get<DbSet<ProductType>>("ProductTypeMemoryCache");
        }
    }

Обновление:

Ответ от Дэвида Брауна не будет работать в соответствии с требованиями вопроса, как и использование типа

var pt2 = db.Cache.LK_ProductTypes; //from cache

Компания имеет многосуществующие запросы, и хотите переслать существующий код в кэш. Кроме того, программисты могут забыть применить Cache, поэтому приведенный ниже код должен автоматически пересылать

var pt1 = db.LK_ProductTypes.ToList() ;  

У нас есть смесь больших транзакционных таблиц, которые не должны кэшироваться, и небольших таблиц поиска.

Администратор базы данных непозволяют использовать таблицы, оптимизированные для памяти SQL Server, из-за некоторых ограничений. Память Оптимизированные ограничения

Ресурсы:

Нужно ли DbContext MemoryCache или Redis в Net Core?

Каксделать контекст данных Entity Framework доступным только для чтения

Net Core: автоматически обновлять кэш таблицы поиска после сохранения Entity Framework и UnitOfWorkPattern

Использование EF Core 2.2,

Ответы [ 2 ]

1 голос
/ 04 октября 2019

В настоящее время мы думаем о создании собственного dbcontext, где переопределенные чтения могут пересылать таблицы LK_ в MemoryCache. Будет ли это работать?

Нет. Это нарушит DbContext и не позволит вам писать запросы, объединяющие элементы поиска с некэшируемыми элементами. Так что вам понадобится отдельный кеш. Вы можете попробовать шаблон следующим образом:

public class Db : DbContext
{
    private IMemoryCache _memoryCache;
    public Db(IMemoryCache memoryCache)
    {
        _memoryCache = memoryCache;
    }

    public class EntityCache
    {
        private Db db;
        public EntityCache(Db db)
        {
            this.db = db;
        }

        public IList<LK_ProductType> LK_ProductTypes => db._memoryCache.GetOrCreate<IList<LK_ProductType>>("LK_ProductTypeMemoryCache",f => db.LK_ProductTypes.AsNoTracking().ToList());
    }
    public EntityCache Cache => new EntityCache(this);

    public DbSet<LK_ProductType> LK_ProductTypes { get; set; }

}

Использование будет выглядеть так:

using var db = new Db(memoryCache);
var pt1 = db.LK_ProductTypes.ToList();  //from database
var pt2 = db.Cache.LK_ProductTypes; //from cache

EF Core 3 представляет новую структуру перехвата запросов, которая должна позволять вводить результаты запросакеш в вашем DbContext. Вот действительно грубый пример того, как это будет работать.

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Linq;
using System.Threading;

namespace EfCore3Test
{

    class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    class Db : DbContext
    {

        string constr;
        private static QueryResultsCache cache { get; } =  new QueryResultsCache();
        private QueryResultsCache Cache { get; } = cache;

        public IList<T> CacheQueryResults<T>(IQueryable<T> query)
        {
            return Cache.ReadThrough(query);
        }
        public Db() : this("server=.;database=EfCore3Test;Integrated Security=true")
        { }

        public Db(string constr)
        {
            this.constr = constr;
        }
        static readonly ILoggerFactory loggerFactory = LoggerFactory.Create(builder => 
               {
                   builder.AddFilter((category, level) =>
                      category == DbLoggerCategory.Database.Command.Name
                      && level == LogLevel.Information).AddConsole();
               }
               );
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(constr, a => a.UseRelationalNulls(true));
            optionsBuilder.AddInterceptors(cache);

            optionsBuilder.UseLoggerFactory(loggerFactory);
            base.OnConfiguring(optionsBuilder);
        }
        public DbSet<Customer> Customers { get; set; }
    }

    public class QueryResultsCache : DbCommandInterceptor
    {
        class CacheEntry
        {
            public CacheEntry(DataTable dt)
            {
                this.Data = dt;
                this.LastRefresh = DateTime.Now;
            }
            public DateTime LastRefresh { get; set; }
            public DataTable Data { get; set; }
        }

        private ConcurrentDictionary<string, CacheEntry> resultCache = new ConcurrentDictionary<string, CacheEntry>();

        AsyncLocal<bool> cacheEntry = new AsyncLocal<bool>();
        public IList<T> ReadThrough<T>(IQueryable<T> query)
        {
            cacheEntry.Value = true;
            var results = query.ToList();
            cacheEntry.Value = false;
            return results;
        }
        public override InterceptionResult<DbDataReader> ReaderExecuting(
          DbCommand command,
          CommandEventData eventData,
          InterceptionResult<DbDataReader> result)
        {
            if (resultCache.ContainsKey(command.CommandText))
            {
                Console.WriteLine("Query Result from Cache");
                return InterceptionResult<DbDataReader>.SuppressWithResult(resultCache[command.CommandText].Data.CreateDataReader());
            }

            if (cacheEntry.Value)
            {
                using (var rdr = command.ExecuteReader())
                {
                    var dt = new DataTable();
                    dt.Load(rdr);
                    resultCache.AddOrUpdate(command.CommandText, s => new CacheEntry(dt), (s, d) => d);
                    Console.WriteLine("Cached Result Created");
                    return InterceptionResult<DbDataReader>.SuppressWithResult(dt.CreateDataReader());
                }
            }

            return result;
        }
    }
    class Program
    {

        static void Main(string[] args)
        {
            using var db = new Db();
            db.Database.EnsureCreated();

            var c = db.CacheQueryResults( db.Customers );

            for (int i = 0; i < 1000; i++)
            {
                var c2 = db.Customers.ToList();
            }


            Console.WriteLine(c);


        }
    }
}
0 голосов
/ 06 октября 2019

У вас уже есть рабочий Уровень доступа к данным (DAL), который выполняет поиск БД для вас. Оставьте это нетронутым.

Для кэширования создайте еще одну абстракцию сверх текущего DAL. Давайте назовем это Cache Access Layer (CAL). Эта CAL - это то, что ваше приложение должно использовать сейчас, а не DAL.

CAL теперь будет отвечать за:

  • Ответ на запросы данных;
  • Отправка данных из кеша при попадании;
  • Заполнение данных в кеше из БД при пропадании;
  • Запись данных в БД при их обновлении в кеше.

Архитектура вашего приложения будет выглядеть до и после изменений:

Architecture - Before and After.


Здесь , как это обычно делается в облаке. Варианты использования могут отличаться, но принципы одинаковы, а именно:

  • Решение о том, когда кэшировать;
  • Определение способа эффективного кэширования;
  • Кэширование с высокой динамикойdata;
  • Управление сроком действия данных в кеше.
...