百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

.NET EF Core千万级数据实践

mhr18 2024-11-26 12:18 16 浏览 0 评论

前言


.NET开发中操作数据库EF一直是我的首选,工作和学习也一直在使用。EF一定程度的提高了开发速度,开发人员专注业务,不用编写sql。方便的同时也产生了一直被人诟病的一个问题性能低下。


EF Core + MySql的组合越来越流行,所以本文数据库使用MySql完成相关示例。


说明


由于工作中也一直使用Sql Server,所以记录这篇文章时也学习了很多MySql的内容。

MySql安装,打开官网(https://dev.mysql.com/downloads/installer/)下载安装。

示例项目说明:

.NET 5.0 + 最基本的 EF Code First 模型。两个Entity,分别为Order和OrderItem。

数据库:


Order数据量500W

Order实体除了基本字段定义还定义了一个OrderItems

OrderItems数据量800W


OrderItem定义了一个Order virtual 属性


并在实体和表映射是定义了外键关联


正常系统中单表最大可能就千万级数据,数据再多便会考虑分表,所以最初设想是单个表准备1000W+的数据,但是没有考虑到我这个老年笔记本,所以实际操作时数据做了适当减少。

MySql记录

准备好测试数据后写了一些简单的SQL查询来做测试,一些稍微复杂点的查询耗时就十秒、二十秒。此时应该从数据库的优化入手,优化EF查询不能解决我们的问题。优化MySql查询和排序最简单有效的办法就是创建索引,根据业务需求合理的创建索引,保证索引的命中(最左原则),还要设置一个足够大的innodb-buffer-pool-size。


日志记录和诊断


项目中添加了两种方式查看EF生成的SQL和执行耗时。做一下简单说明实际开发中可自行选择。

Microsoft.Extensions.Logging

确保项目安装了Microsoft.Extensions.Logging包。

添加一个ILoggerFactory类型静态属性

public static readonly ILoggerFactory MyLoggerFactory = LoggerFactory.Create(builder => { builder.AddConsole(); });

EF Core 注册此实例

options.EnableSensitiveDataLogging()
                .UseLoggerFactory(MyLoggerFactory)
                .EnableDetailedErrors()

MiniProfile

安装MiniProfiler.AspNetCore.Mvc包

Startup的`ConfigureServices方法增加代码

services.AddMiniProfiler(options =>
            {
                // All of this is optional. You can simply call .AddMiniProfiler() for all defaults

                // (Optional) Path to use for profiler URLs, default is /mini-profiler-resources
                options.RouteBasePath = "/profiler";

                // (Optional) Control which SQL formatter to use, InlineFormatter is the default
                options.SqlFormatter = new StackExchange.Profiling.SqlFormatters.InlineFormatter();

                // (Optional) You can disable "Connection Open()", "Connection Close()" (and async variant) tracking.
                // (defaults to true, and connection opening/closing is tracked)
                options.TrackConnectionOpenClose = true;

                // (Optional) Use something other than the "light" color scheme.
                // (defaults to "light")
                options.ColorScheme = StackExchange.Profiling.ColorScheme.Auto;

                // The below are newer options, available in .NET Core 3.0 and above:

                // (Optional) You can disable MVC filter profiling
                // (defaults to true, and filters are profiled)
                options.EnableMvcFilterProfiling = true;
                // ...or only save filters that take over a certain millisecond duration (including their children)
                // (defaults to null, and all filters are profiled)
                // options.MvcFilterMinimumSaveMs = 1.0m;

                // (Optional) You can disable MVC view profiling
                // (defaults to true, and views are profiled)
                options.EnableMvcViewProfiling = true;
                // ...or only save views that take over a certain millisecond duration (including their children)
                // (defaults to null, and all views are profiled)
                // options.MvcViewMinimumSaveMs = 1.0m;

            }).AddEntityFramework();

Startup的Configure``方法增加``如下代码

app.UseMiniProfiler();

_ViewImports.cshtml文件中添加引用和对应taghelper

@using StackExchange.Profiling
@addTagHelper *, MiniProfiler.AspNetCore.Mvc

在视图文件中添加MiniProfiler

<mini-profiler />

.Net Core 5 提供了IQueryable的ToQueryString()方法可以直接获取Linq查询对应的SQL语句。

查询数据

先说明两个实例中没有出现的基本查询优化方案

1、大表避免整表返回(sql中的select *),简化查询实体仅返回业务需要的字段,返回多个字段时可以将Select查询映射到匿名类。

2、如果只是单纯的获取列表不需要更新从数据库中检索到的实体,应使用AsNoTracking方法设置非跟踪查询,无需设置更改跟踪信息(EF 在内部维护跟踪实例的字典),更快速地执行查询。

Find

示例中实现两个方法 GetByIdAsync和GetByIdFromSql,实现如下

启动项目看到如下输出:

EF的Find方法生成了一个简单的sql语句执行耗时19ms,反而通过FromSqlInterpolated调用自己写的SQL却生成一个看着怪异的sql语句,执行耗时3ms。MiniProfiler中查看耗时差不多

两个SQL耗时不应该有这么大的差距,把两个SQL复制到数据库中执行时发现两个SQL执行时间基本相同,说明调用EF方法EF到SQL的转换耗时也计算在内,因为EF的缓存机制再次调用时发现两个方法的耗时基本持平。

两种方式返回的Order中OrderItems数量为零,解决这个问题就涉及到EF加载相关数据的知识。

这里演示预先加载和延迟加载两种方式

预先加载

修改GetByIdAsync代码:

var order = await _dataDBContext.Orders.Include(a => a.OrderItems).FirstOrDefaultAsync(a => a.Id == id);

此时EF生成的代码就会关联查询OrderItem,EF生成SQL如下

查看打印的log会发现一个问题,我们修改EF代码为预先加载,SQL查询生成的SQL相同却会关联查询出OrderItems的数据。

再次修改代码

并修改GetByIdFromSql方法参数为1362(之前和GetByIdAsync参数一样为1360),运行

同样是Find查找,1362对应的OrderItems为空,1360对应的OrderItems的Count却为3,对应Sql查询的1362的OrderItems也为空。应该是EF的缓存机制造成的这种情况,有兴趣和精力的可以查看一下EF Core的源码。

延时加载

AddDbContext时增加UseLazyLoadingProxies方法调用

此时不管是EF的Find还是原始SQL都能查询出OrderItems的值。

查询结果集及外键关联数据

定义如下方法查询结果为某个用户订单及关联数据

运行代码,会遇到使用EF时经常遇到的一个错误

因为获取orders时已经建立一个连接,当我们循环orders获取OrderItems时(当前设置为延时加载)需要在建立连接从而引发这个异常。修改代码通过ToList来避免这个异常

此时可以正常获取OrderItems的数据,通过MiniProfiler查看生成的sql

这个EF方法生成了21(1条查询orders+20条延时查询orderitems)条sql。再次修改代码,改为预先加载的方式,查询Order的同时返回OrderItems数据。

EF生成的sql从21条变为1条。

拆分查询

EF Core 5.0 中引入拆分查询功能以避免“笛卡尔爆炸”问题,可以将指定 LINQ 查询拆分为多个 SQL 查询,仅在使用 Include 时可用。

单个EF查询调用AsSplitQuery方法启用拆分查询。也可以全局启用拆分查询,在设置应用程序数据库连接上下文时调用UseQuerySplittingBehavior开启全局拆分。

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(
            @"Server=(localdb)\mssqllocaldb;Database=EFQuerying;Trusted_Connection=True;ConnectRetryCount=0",
            o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
}

设置拆分查询为默认的查询方式后,可以再调用AsSingleQuery方法指定具体的EF查询为单个查询模式。

为了测试这个功能我又添加了一张Customer表,修改代码如下:

public async Task GetOrdersAsync(int customerId, int pageIndex, int pageSize)
        {

            var order = await _dataDBContext.Orders.Where(a => a.CustomerId == customerId)
                .OrderBy(a => a.CreatedTime)
                .Skip((pageIndex - 1) * pageSize).Take(pageSize)
                .Include(a => a.OrderItems).Include(a => a.Customer)
                .TagWith("--Get Orders").AsSplitQuery().FirstOrDefaultAsync();

            var orders = await _dataDBContext.Orders.Where(a => a.CustomerId == customerId)
                .OrderBy(a => a.CreatedTime)
                .Skip((pageIndex - 1) * pageSize).Take(pageSize)
                .Include(a => a.OrderItems).Include(a => a.Customer)
                .TagWith("--Get Orders").AsSplitQuery().ToListAsync();

            var count1 = 0;
            foreach (var _order in orders)
            {
                count1 += order.OrderItems.Count;
            }

            Console.WriteLine(#34;count1:{count1}");

        }

上面并不是一个能正常执行的代码,抛出异常MySql.Data.MySqlClient.MySqlException (0x80004005): There is already an open DataReader associated with this Connection which must be closed first。

Github的issues提到这个问题,拆分查询需要开启Sql Server的MARS(MultipleActiveResultSets=true)。但是MySql不持支MARS,目前我不知道如何在MySql下正常运行AsSplitQuery的代码。

拆分查询当前实现执行为每个查询的往返(类似延时加载), 这个将来会修改为单次往返中执行所有查询。

更新数据

EF Core 默认情况下,仅在单个批处理中执行最多42条语句,可以调整这些阈值实现可能更高的性能,但在修改之前应进行基准测试确保有更高的性能。

摘自官网的一个段示例说明

很遗憾EF目前还不支持批量更新和删除操作,官网也给出了优化方案,用原始SQL来执行:

context.Database.ExecuteSqlRaw("UPDATE [Employees] SET [Salary] = [Salary] + 1000");

B站活跃用户杨中科老师的一篇文章也有介绍:https://www.bilibili.com/read/cv8545714

但是复杂的更新业务写SQL同样是让人头疼的一件事,不想写一行SQL语句,又想实现批量更新和删除操作可以借助第三方库Zack.EFCore.Batch或Z.EntityFramework.Extensions.EFCore(https://entityframework-extensions.net)。

性能提升

DbContext 池

AddDbContextPool 启用可重用上下文实例的池,上下文池可以重复使用上下文实例,而不用每个请求创建新实例,从而提高大规模方案(如 web 服务器)的吞吐量。在请求上下文实例时,EF 首先检查池中是否有可用的实例。请求处理完成后,实例的任何状态都将被重置,并且实例本身会返回池中。

services.AddDbContextPool<BloggingContext>(options => options.UseSqlServer(connectionString));

poolSize 参数 AddDbContextPool 设置池保留的最大实例数 中128。一旦 poolSize 超出,就不会缓存新的上下文实例,EF 会回退到按需创建实例的非池行为。

上下文池的工作方式是跨请求重复使用同一上下文实例。上下文池适用于上下文配置(包括解析的服务)在请求之间固定的场景。对于需要作用域服务或需要更改配置的情况,请勿使用池。池的性能提升通常很小,仅在高度优化的方案中采用。

预编译查询

执行普通Linq查询的时会执行一次Compile,虽然EF对查询的Linq有缓存机制,但是编译的查询比自动缓存的 LINQ 查询效率更高。对于多次执行结构类似的查询可以通过预编译,仅编译查询一次并在每次执行时使用不同参数的方法来提高性能。

示例代码:

Func<DataDBContext, decimal, SingleQueryingEnumerable<int>> compiledProductReports =
            EF.CompileQuery<DataDBContext, decimal, SingleQueryingEnumerable<int>>(
            (ctx, total) => ctx.OrderItems.AsNoTracking().IgnoreAutoIncludes()
                .GroupBy(a => a.ProductId).Select(a => new
                {
                    ProductId = a.Key,
                    Quantity = a.Sum(b => b.Quantity),
                    Price = a.Sum(b => b.Price),
                }).Where(a => a.Price > total).Select(a => a.ProductId) as SingleQueryingEnumerable<int>
            );

        [Benchmark]
        public async Task ProductReports()
        {
            //var productIds = await _dataDBContext.OrderItems.IgnoreAutoIncludes().AsNoTracking()
            //    .GroupBy(a=>a.ProductId).Select(a => new {
            //        ProductId = a.Key,
            //        Quantity = a.Sum(b => b.Quantity),
            //        Price = a.Sum(b => b.Price),
            //    }).Where(a=>a.Price>100000).Select(a=>a.ProductId)
            //    .ToListAsync();

            var productIds = compiledProductReports(_dataDBContext, 100000).ToList();

        }

很遗憾这又不是一个好的代码,由于EF Core 5.0 增加了单个查询和拆分查询的概念,返回的类型为SingleQueryingEnumerable,遇到了Expression of type 'Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable1[System.Int32]' cannot be used for return type 'System.Linq.IQueryable1[System.Int32]' 这个错误,所以只能强转为SingleQueryingEnumerable。但是会产生如下警告

Any

Count和Any

当有个需求判断满足条件的数据是否存在通常会有如下写法

查看三种方式生成的SQL和执行耗时,Any是效率最高的一种。

补充

提升EF的性能还有很多办法,分为三大类:数据库性能(纯数据库优化)、网路传输(减少数据传输和连接次数)和EF运行时开销(跟踪和生成SQL语句)。还有很多优化技巧没能提及到如AsEnumerable方法改为

流式处理处理每次只获取一条数据,但是会增加数据连接。想进一步提醒程序的性能最简单的办法就是在加入缓存机制(Redis缓存等),缓存模式介绍(https://mp.weixin.qq.com/s/iUDA8L30-z_36XvYP8Dq1w),EF的拦截器也为我们提供了更多的解决方案(https://docs.microsoft.com/zh-cn/ef/core/logging-events-diagnostics/interceptors#example-advanced-command-interception-for-caching)。

Github地址:https://github.com/MayueCif/EFCore

转自:Stacking

链接:cnblogs.com/Stacking/p/ef-core-millions-data.html

相关推荐

【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库

如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...

Pure Storage推出统一数据管理云平台及新闪存阵列

PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...

对Java学习的10条建议(对java课程的建议)

不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...

SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!

官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...

JDK21有没有什么稳定、简单又强势的特性?

佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...

「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了

在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...

Java面试题及答案最全总结(2025版)

大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...

数据库日常运维工作内容(数据库日常运维 工作内容)

#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...

分布式之系统底层原理(上)(底层分布式技术)

作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...

oracle 死锁了怎么办?kill 进程 直接上干货

1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...

SpringBoot 各种分页查询方式详解(全网最全)

一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...

《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略

《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...

LoadRunner(loadrunner录制不到脚本)

一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...

Redis数据类型介绍(redis 数据类型)

介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...

RMAN备份监控及优化总结(rman备份原理)

今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...

取消回复欢迎 发表评论: