【翻译】EF Core 3.1.x, 5.x & 6.x Second Level Cache Interceptor

原文地址

GitHub:EFCoreSecondLevelCacheInterceptor

二级缓存拦截器

第二级缓存是缓存查询。EF命令请求的结果将存储在缓存中,因此相同的EF命令请求将从缓存中检索数据,而不是再次去数据库执行它们。

通过NuGet安装

要安装EFCORESondLevelCacheInterceptor,请在包管理工具的控制台中运行以下命令:

PM> Install-Package EFCoreSecondLevelCacheInterceptor

您还可以在NuGet上查看软件包页面。

用法(1和2为必填项)

1-注册首选缓存提供程序:

使用内置内存缓存提供程序
CacheManager.Core
在这里插入图片描述

namespace EFCoreSecondLevelCacheInterceptor.AspNetCoreSample
{
    
    
    public class Startup
    {
    
    
        private readonly string _contentRootPath;

        public Startup(IConfiguration configuration, IWebHostEnvironment env)
        {
    
    
            _contentRootPath = env.ContentRootPath;
            Configuration = configuration;
        }

        public IConfiguration Configuration {
    
     get; }

        public void ConfigureServices(IServiceCollection services)
        {
    
    
            services.AddEFSecondLevelCache(options =>
                options.UseMemoryCacheProvider().DisableLogging(true).UseCacheKeyPrefix("EF_")

            // 请使用“CacheManager.Core”或者“EasyCaching.Redis”用于Redis缓存提供程序
            );

            var connectionString = Configuration["ConnectionStrings:ApplicationDbContextConnection"];
            if (connectionString.Contains("%CONTENTROOTPATH%"))
            {
    
    
                connectionString = connectionString.Replace("%CONTENTROOTPATH%", _contentRootPath);
            }
            services.AddConfiguredMsSqlDbContext(connectionString);

            services.AddControllersWithViews();
        }
    }
}

使用EasyCaching.Core 作为Redis缓存提供程序

在这里,您可以使用EasyCaching.Core。作为一个高度可配置的缓存管理器。要使用其内存缓存机制,请将此项添加到.csproj文件中:

  <ItemGroup>
    <PackageReference Include="EasyCaching.InMemory" Version="1.1.0" />
  </ItemGroup>

然后注册其所需的服务:

namespace EFSecondLevelCache.Core.AspNetCoreSample
{
    
    
    public class Startup
    {
    
    
        public void ConfigureServices(IServiceCollection services)
        {
    
    
            const string providerName1 = "InMemory1";
            services.AddEFSecondLevelCache(options =>
                    options.UseEasyCachingCoreProvider(providerName1, isHybridCache: false).DisableLogging(true).UseCacheKeyPrefix("EF_")
            );

            // 添加内存缓存服务提供程序
            // 更多信息:https://easycaching.readthedocs.io/en/latest/In-Memory/
            services.AddEasyCaching(options =>
            {
    
    
                // 将内存缓存与您自己的配置一起使用
                options.UseInMemory(config =>
                {
    
    
                    config.DBConfig = new InMemoryCachingOptions
                    {
    
    
                        //扫描时间,默认值为60秒
                        ExpirationScanFrequency = 60,
                        // 缓存项的总数,默认值为10000
                        SizeLimit = 100,

                        // 是否从缓存读取对象时启用深度克隆,默认值为true。
                        EnableReadDeepClone = false,
                        // 是否将对象写入缓存时启用深度克隆,默认值为false。
                        EnableWriteDeepClone = false,
                    };
                    // 最大随机秒将添加到缓存的过期时间,默认值为120
                    config.MaxRdSecond = 120;
                    // 是否启用日志记录,默认值均为false
                    config.EnableLogging = false;
                    // 互斥锁键的存活时间(毫秒),默认为5000
                    config.LockMs = 5000;
                    // 互斥锁键处于存活状态时,它将休眠一段时间,默认值为300
                    config.SleepMs = 300;
                }, providerName1);
            });
        }
    }
}

如果您想使用Redis作为EasyCaching.Core的首选缓存提供程序。首先安装以下软件包:

  <ItemGroup>
    <PackageReference Include="EasyCaching.Redis" Version="1.1.0" />
  </ItemGroup>

然后注册其所需的服务:

namespace EFSecondLevelCache.Core.AspNetCoreSample
{
    
    
    public class Startup
    {
    
    
        public void ConfigureServices(IServiceCollection services)
        {
    
    
            const string providerName1 = "Redis1";
            services.AddEFSecondLevelCache(options =>
                    options.UseEasyCachingCoreProvider(providerName1, isHybridCache: false).DisableLogging(true).UseCacheKeyPrefix("EF_")
            );

            // 更多信息: https://easycaching.readthedocs.io/en/latest/Redis/
            services.AddEasyCaching(option =>
            {
    
    
                option.UseRedis(config =>
                {
    
    
                    config.DBConfig.AllowAdmin = true;
                    config.DBConfig.SyncTimeout = 10000;
                    config.DBConfig.AsyncTimeout = 10000;
                    config.DBConfig.Endpoints.Add(new EasyCaching.Core.Configurations.ServerEndPoint("127.0.0.1", 6379));
                }, providerName1);
            });
        }
    }
}

使用CacheManager.Core作为缓存提供程序[未积极维护]

在这里,您还可以使用CacheManager.Core作为一个高度可配置的缓存管理器。要使用其内存缓存机制,请将以下条目添加到.csproj文件中:

  <ItemGroup>
    <PackageReference Include="CacheManager.Core" Version="1.2.0" />
    <PackageReference Include="CacheManager.Microsoft.Extensions.Caching.Memory" Version="1.2.0" />
    <PackageReference Include="CacheManager.Serialization.Json" Version="1.2.0" />
  </ItemGroup>

然后注册其所需的服务:

namespace EFSecondLevelCache.Core.AspNetCoreSample
{
    
    
    public class Startup
    {
    
    
        public void ConfigureServices(IServiceCollection services)
        {
    
    
            services.AddEFSecondLevelCache(options =>
                options.UseCacheManagerCoreProvider().DisableLogging(true).UseCacheKeyPrefix("EF_")
            );

            // 添加内存缓存服务提供程序
            services.AddSingleton(typeof(ICacheManager<>), typeof(BaseCacheManager<>));
            services.AddSingleton(typeof(ICacheManagerConfiguration),
                new CacheManager.Core.ConfigurationBuilder()
                        .WithJsonSerializer()
                        .WithMicrosoftMemoryCacheHandle(instanceName: "MemoryCache1")
                        .Build());
        }
    }
}

如果您想使用Redis作为CacheManager.Core的首选缓存提供程序。首先安装CacheManager.StackExchange.Redis包并注册其所需服务:

// 添加Redis缓存服务提供程序
var jss = new JsonSerializerSettings
{
    
    
    NullValueHandling = NullValueHandling.Ignore,
    ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
    TypeNameHandling = TypeNameHandling.Auto,
    Converters = {
    
     new SpecialTypesConverter() }
};

const string redisConfigurationKey = "redis";
services.AddSingleton(typeof(ICacheManagerConfiguration),
    new CacheManager.Core.ConfigurationBuilder()
        .WithJsonSerializer(serializationSettings: jss, deserializationSettings: jss)
        .WithUpdateMode(CacheUpdateMode.Up)
        .WithRedisConfiguration(redisConfigurationKey, config =>
        {
    
    
            config.WithAllowAdmin()
                .WithDatabase(0)
                .WithEndpoint("localhost", 6379)
                // 允许keyspace通知在项目退出/过期时做出反应.
                // 确保所有服务器配置正确,并且“notify keyspace events”至少设置为“Exe”,否则CacheManager将无法检索任何事件
                // 您也可以尝试使用'notify keyspace events'的'Egx'或'eA'值。
                // 详见 https://redis.io/topics/notifications#configuration for configuration details.
                .EnableKeyspaceEvents();
        })
        .WithMaxRetries(100)
        .WithRetryTimeout(50)
        .WithRedisCacheHandle(redisConfigurationKey)
        .Build());
services.AddSingleton(typeof(ICacheManager<>), typeof(BaseCacheManager<>));

services.AddEFSecondLevelCache(options =>
    options.UseCacheManagerCoreProvider().DisableLogging(true).UseCacheKeyPrefix("EF_")
);

以下是SpecialTypesConverter的定义。

使用自定义缓存提供程序

如果不想使用上述缓存提供程序,请实现自定义IEFCacheServiceProvider,然后使用options.UseCustomCacheProvider() 方法引用它

2-将SecondLevelCacheInterceptor添加到DbContextOptionsBuilder管道:

    public static class MsSqlServiceCollectionExtensions
    {
    
    
        public static IServiceCollection AddConfiguredMsSqlDbContext(this IServiceCollection services, string connectionString)
        {
    
    
            services.AddDbContextPool<ApplicationDbContext>((serviceProvider, optionsBuilder) =>
                    optionsBuilder
                        .UseSqlServer(
                            connectionString,
                            sqlServerOptionsBuilder =>
                            {
    
    
                                sqlServerOptionsBuilder
                                    .CommandTimeout((int)TimeSpan.FromMinutes(3).TotalSeconds)
                                    .EnableRetryOnFailure()
                                    .MigrationsAssembly(typeof(MsSqlServiceCollectionExtensions).Assembly.FullName);
                            })
                        .AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>()));
            return services;
        }
    }

注意:一些数据库提供程序不支持特殊字段,如DateTimeOffset、TimeSpan等。对于这些场景,您将需要相关的转换器。

3-设置缓存无效:

此库不需要任何缓存无效设置。它使用拦截器监视所有CRUD操作,然后自动使相关缓存项无效。但如果要手动使整个缓存无效,请注入IEFCacheServiceProvider服务,然后调用其_cacheServiceProvider.ClearAllCachedEntries()方法,或以此方式指定根缓存键,这些键是前缀+表名的集合:

// 使用指定表名的部分缓存无效
// 当您使用SqlTableDependency监视数据库的更改时,这非常有用
_cacheServiceProvider.InvalidateCacheDependencies(new EFCacheKey(new HashSet<string>() 
{
    
    
   "EF_TableName1", // "EF_" is the cache key's prefix
   "EF_TableName2" 
} {
    
      KeyHash = "empty" }));

4-缓存正常查询的结果,如:

var post1 = context.Posts
                   .Where(x => x.Id > 0)
                   .OrderBy(x => x.Id)
                   .FirstOrDefault();

我们可以使用新的Cacheable()扩展方法:

var post1 = context.Posts
                   .Where(x => x.Id > 0)
                   .OrderBy(x => x.Id)
                   .Cacheable(CacheExpirationMode.Sliding, TimeSpan.FromMinutes(5))
                   .FirstOrDefault();  // Async methods are supported too.

注意:可缓存方法在表达式树中的位置无关紧要。它只是添加了标准的TagWith方法来将该查询标记为可缓存。稍后,SecondLevelCacheInterceptor将使用此标记来标识可缓存查询。

此外,还可以全局设置Cacheable()方法的设置:

services.AddEFSecondLevelCache(options => options.UseMemoryCacheProvider(CacheExpirationMode.Absolute, TimeSpan.FromMinutes(5)).DisableLogging(true).UseCacheKeyPrefix("EF_"));

在这种情况下,上述查询将变为:

var post1 = context.Posts
                   .Where(x => x.Id > 0)
                   .OrderBy(x => x.Id)
                   .Cacheable()
                   .FirstOrDefault();  // Async methods are supported too.

如果显式指定Cacheable()方法的设置,例如Cacheable(CacheExpirationMode.Sliding,TimeSpan.FromMinutes(5)),则其设置将覆盖全局设置。

缓存所有查询

要缓存系统的所有查询,只需设置CacheAllQueries()方法:

namespace EFCoreSecondLevelCacheInterceptor.AspNetCoreSample
{
    
    
    public class Startup
    {
    
    
        public void ConfigureServices(IServiceCollection services)
        {
    
    
            services.AddEFSecondLevelCache(options =>
            {
    
    
                options.UseMemoryCacheProvider().DisableLogging(true).UseCacheKeyPrefix("EF_");
                options.CacheAllQueries(CacheExpirationMode.Absolute, TimeSpan.FromMinutes(30));
            });

            // ...

这将把整个系统的查询放入缓存。在这种情况下,不需要调用Cacheable()方法。如果你指定Cacheable()方法,其设置将覆盖此全局设置。如果要从此全局缓存中排除某些查询,请对它们应用NotCacheable()方法。

缓存一些查询

要根据实体类型或表名缓存某些系统查询,请使用CacheQueriesContainingTypes或CacheQuerysContaining TableNames方法:

namespace EFCoreSecondLevelCacheInterceptor.AspNetCoreSample
{
    
    
    public class Startup
    {
    
    
        public void ConfigureServices(IServiceCollection services)
        {
    
    
            services.AddEFSecondLevelCache(options =>
            {
    
    
                options.UseMemoryCacheProvider().DisableLogging(true).UseCacheKeyPrefix("EF_")
                    /*.CacheQueriesContainingTypes(
                        CacheExpirationMode.Absolute, TimeSpan.FromMinutes(30), TableTypeComparison.Contains,
                        typeof(Post), typeof(Product), typeof(User)
                        )*/
                    .CacheQueriesContainingTableNames(
                        CacheExpirationMode.Absolute, TimeSpan.FromMinutes(30), TableNameComparison.ContainsOnly,
                        "posts", "products", "users"
                        );
            });

            // ...

这将把指定系统的查询放入缓存。在这种情况下,不需要调用Cacheable()方法。如果指定Cacheable()方法,其设置将覆盖此全局设置。如果要从此全局缓存中排除某些查询,请对它们应用NotCacheable()方法。

跳过某些查询的缓存

要跳过基于SQL命令缓存某些系统查询,请设置SkipCachingCommands谓词:

namespace EFCoreSecondLevelCacheInterceptor.AspNetCoreSample
{
    
    
    public class Startup
    {
    
    
        public void ConfigureServices(IServiceCollection services)
        {
    
    
            services.AddEFSecondLevelCache(options =>
            {
    
    
                options.UseMemoryCacheProvider().DisableLogging(true).UseCacheKeyPrefix("EF_")
                        // 如何跳过缓存特定命令
                       .SkipCachingCommands(commandText =>
                                commandText.Contains("NEWID()", StringComparison.InvariantCultureIgnoreCase));
            });
            // ...

根据某些查询的结果跳过缓存

要跳过基于某些系统查询结果的缓存,请设置SkipCacheResults谓词:

namespace EFCoreSecondLevelCacheInterceptor.AspNetCoreSample
{
    
    
    public class Startup
    {
    
    
        public void ConfigureServices(IServiceCollection services)
        {
    
    
            services.AddEFSecondLevelCache(options =>
            {
    
    
                options.UseMemoryCacheProvider().DisableLogging(true).UseCacheKeyPrefix("EF_")
                        // 不要缓存空值。如果没有必要,请删除此可选设置。
                        .SkipCachingResults(result =>
                                result.Value == null || (result.Value is EFTableRows rows && rows.RowsCount == 0));
            });
            // ...

根据某些查询的表名跳过缓存它们

要不基于某些系统查询的实体类型或表名缓存它们,请使用CacheAllQueriesExceptContainingTypes或CacheAllQuery ExceptcontainingTableNames方法:

namespace EFCoreSecondLevelCacheInterceptor.AspNetCoreSample
{
    
    
    public class Startup
    {
    
    
        public void ConfigureServices(IServiceCollection services)
        {
    
    
            services.AddEFSecondLevelCache(options =>
            {
    
    
                options.UseMemoryCacheProvider().DisableLogging(true).UseCacheKeyPrefix("EF_")
                    /*.CacheAllQueriesExceptContainingTypes(
                        CacheExpirationMode.Absolute, TimeSpan.FromMinutes(30),
                        typeof(Post), typeof(Product), typeof(User)
                        )*/
                    .CacheAllQueriesExceptContainingTableNames(
                        CacheExpirationMode.Absolute, TimeSpan.FromMinutes(30),
                        "posts", "products", "users"
                        );
            });

            // ...

这不会将指定系统的查询放入缓存。在这种情况下,不需要调用Cacheable()方法。如果指定Cacheable()方法,其设置将覆盖此全局设置。

跳过使给定查询的相关缓存项无效

有时,您不想立即使缓存无效,例如当您更新一篇文章的赞数或视图数时。在这种情况下,要跳过使给定CRUD命令的相关缓存项无效,请设置SkipCacheInvalizationCommands谓词:

namespace EFCoreSecondLevelCacheInterceptor.AspNetCoreSample
{
    
    
    public class Startup
    {
    
    
        public void ConfigureServices(IServiceCollection services)
        {
    
    
            services.AddEFSecondLevelCache(options =>
            {
    
    
                options.UseMemoryCacheProvider().DisableLogging(true).UseCacheKeyPrefix("EF_")
                    .SkipCacheInvalidationCommands(commandText =>
                                // 如何跳过使此查询的相关缓存项无效
                                commandText.Contains("NEWID()", StringComparison.InvariantCultureIgnoreCase));
            });
            // ...

它有用吗?!

您应该使日志记录系统能够看到缓存拦截器的后台情况。首先设置DisableLogging(false):

 services.AddEFSecondLevelCache(options =>
                options.UseMemoryCacheProvider().DisableLogging(false).UseCacheKeyPrefix("EF_")

然后在appsettings中将日志级别更改为Debug。json文件:

{
    
    
  "Logging": {
    
    
    "LogLevel": {
    
    
      "Default": "Debug",
      "System": "Debug",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Debug"
    }
  }
}

现在,在多次运行查询之后,您应该有以下记录行:

Suppressed result with a TableRows[ee20d2d7-ffc7-4ff9-9484-e8d4eecde53e] from the cache[KeyHash: EB153BD4, CacheDependencies: Page.].
Using the TableRows[ee20d2d7-ffc7-4ff9-9484-e8d4eecde53e] from the cache.

批注:

  • 使用TableRows消息抑制结果意味着缓存拦截器工作正常。
  • 下一个执行的DbCommand没有任何意义,它将始终由EF记录。
  • 一开始,EF将执行大量内部命令来运行迁移等。忽略这些命令,因为对于频繁运行的查询,您将看到TableRows消息的结果。
  • 此外,您还应该使用真正的数据库探查器进行验证。它将记录第一次执行的查询,然后在第二次运行时,您将不再看到它。

示例

Console App Sample
ASP.NET Core App Sample
Tests

使用手册

何时使用

查询缓存的好选择是全局站点设置和公共数据,例如不经常更改的文章或评论。缓存特定于用户的数据也是有益的,只要缓存相对于用户基数的大小足够频繁地过期,使得内存消耗保持可接受。经常超过缓存寿命的小数据,如用户的照片路径,最好保存在存储在cookie中的用户声明中,而不是保存在该缓存中。

范围

此缓存的范围是应用程序,而不是当前用户。它不使用会话变量。因此,在检索缓存的每用户数据时,请确保查询中包含如下代码:Where(x=>…&&x.UserId==id)。

无效

通过使用此库的DbContext更改(插入、更新或删除)实体时,将更新此缓存。如果通过其他方式(如存储过程或触发器)更新数据库,则缓存将过时。

交易

为了避免复杂性,显式事务(context.Database.BeginTransaction())中的所有查询都不会被缓存。但是由于其CRUD操作,高速缓存将失效。

猜你喜欢

转载自blog.csdn.net/weixin_44231544/article/details/126170829
今日推荐