实体框架是否适合高流量网站?


176

对于可能具有1000次点击/秒的公共网站,Entity Framework 4是否是一个好的解决方案?

以我的理解,EF对于大多数小型网站或Intranet网站都是可行的解决方案,但对于像流行的社区网站这样的应用来说,EF并不容易扩展(我知道SO正在使用LINQ to SQL,但是..我想提供更多示例/证明。 ..)

现在,我正处于选择纯ADO.NET方法或EF4的十字路口。您是否认为使用EF提高开发人员的生产力值得失去ADO.NET的性能和细粒度访问(使用存储过程)?高流量网站可能会遇到任何严重问题,是否使用EF?

先感谢您。


1
您不了解扩展。扩展意味着当您增加10倍容量时获得10倍吞吐量。EF为什么会阻止这种情况的发生?它为任何数据库工作负载增加了恒定的因素开销。
usr

Answers:


152

这取决于您需要多少抽象。一切都是妥协;例如,EF和NHibernate引入了极大的灵活性,可以在有趣的模型和外来的模型中表示数据-但结果却确实增加了开销。明显的开销。

如果没有需要能够数据库提供者之间切换,并且不同的每个客户端表格布局,如果你的数据主要是阅读,如果你并不需要能够使用EF相同的模型,SSRS ,ADO.NET数据服务等-然后,如果您想要绝对的性能作为关键指标,那么做起来比做Dapper要差得多。在基于LINQ-to-SQL和EF的测试中,我们发现EF 在原始读取性能方面明显较慢,这大概是由于抽象层(在存储模型等之间)和实现所致。

在SO,我们对原始性能非常着迷,并且我们很高兴承受失去一些抽象的开发重心以提高速度。因此,我们查询数据库的主要工具是dapper。这甚至允许我们使用我们现有的LINQ-to-SQL模型,但很简单:堆速度更快。在性能测试中,它的性能基本上与手动编写所有ADO.NET代码(参数,数据读取器等)完全相同,但是没有冒错输入列名的风险。但是,它是基于SQL的(尽管很高兴使用SPROC(如果您选择了这种方法))。这样做的好处是涉及其他处理,但是它一个喜欢SQL的人使用的系统。我认为:这不是一件坏事!

例如,一个典型的查询可能是:

int customerId = ...
var orders = connection.Query<Order>(
    "select * from Orders where CustomerId = @customerId ",
    new { customerId }).ToList();

这很方便,安全注射等-但没有大量的数据读取器黏性物质。请注意,尽管它可以同时处理水平和垂直分区以加载复杂的结构,但它不支持延迟加载(但是:我们是非常明确的加载的忠实拥护者-惊喜较少)。

请注意,在此答案中我并不是说EF 适合大批量工作。简而言之:我知道小巧的人可以做到。


25
dapper的+1。无需为读取模型使用复杂的ORM。现在,我们采用的方法是将ORM用于我们的域模型(在其中花哨的ORM东西实际上是有用的)和dapper用于我们的读取模型。这使得超快速的应用。

2
@Marc,谢谢您的出色回答-我终于可以自信地做出决定了!稍后一定会更详细地研究dapper。真的很像它只是一个文件:)

3
我写了自己的ORM。它慢。我看着小巧玲珑,很喜欢。现在,我使用dapper进行所有读取,并使用自己的ORM进行插入(支持FK,事务和所有好东西)。它是我编写过的最简单,最易读的代码。

2
@ acidzombie24 dapper支持事务,而dapper的贡献部分(不是nuget部署的一部分)正在获得insert等选项。只是为了完整性而提及。我很高兴小巧玲珑。
马克·格雷韦尔

1
@anyname我从未做过任何主题的视频课程;有一些视频,但不是我的。我倾向于做书面文字的人
Marc Gravell

217

当涉及大规模应用程序中的整体数据访问策略和性能优化时,“我应该使用哪个ORM”这个问题确实针对一个巨大的冰山一角。

下面所有的东西(大致按重要性排序)会影响到产量,所有这些都是由大多数主要的ORM框架在那里的处理(有时以不同的方式):

  1. 数据库设计与维护

    很大程度上,这是决定数据驱动的应用程序或网站的吞吐量的最重要的决定因素,并且经常被程序员完全忽略。

    如果您没有使用适当的规范化技术,那么您的网站必定会失败。如果您没有主键,几乎每个查询的速度都会很慢。如果您无缘无故地使用众所周知的反模式(例如,将表用于键-值对(AKA实体-属性-值)),则会激增物理读取和写入的次数。

    如果您不利用数据库提供的功能,例如页面压缩,FILESTREAM存储(用于二进制数据),SPARSE列,hierarchyid用于层次结构等(所有SQL Server示例),那么您将无法在数据库附近找到任何内容。您可能会看到的性能。

    在设计了数据库并说服自己至少在目前为止它可能达到的最佳性能之后,您应该开始担心数据访问策略。

  2. 渴望与懒惰加载

    大多数ORM使用一种称为延迟加载关系的技术,这意味着默认情况下,它将一次加载一个实体(表行),并且每次需要加载一个或多个相关(外部)时都要往返数据库。键)行。

    这不是一件好事或坏事,而是取决于数据实际要执行的操作以及您预先知道多少。有时,延迟加载绝对是正确的选择。例如,NHibernate可以决定根本不查询任何内容,而仅生成特定ID 的代理。如果您只需要ID本身,为什么还要索取更多呢?另一方面,如果您尝试在3级层次结构中打印每个元素的树,则延迟加载将成为O(N²)操作,这对性能非常不利。

    使用“纯SQL”(即原始ADO.NET查询/存储过程)的一个有趣的好处是,它基本上迫使您考虑显示任何给定屏幕或页面所需的数据是什么。ORM和延迟加载功能并不能阻止您执行此操作,但是它们确实为您提供了……好吧,懒惰,并意外地激增了您执行的查询数量。因此,您需要了解ORM的急切加载功能,并始终警惕要为任何给定页面请求发送到服务器的查询数量。

  3. 快取

    所有主要的ORM都维护一个一级缓存,也就是“身份缓存”,这意味着如果您通过其ID两次请求同一实体,则不需要第二次往返,并且(如果您正确地设计了数据库, )使您能够使用开放式并发。

    在L2S和EF中,L1缓存非常不透明,您必须信任它的正常运行。NHibernate对此更为明确(Get/ Loadvs. Query/ QueryOver)。不过,只要您尝试通过ID进行查询,此处就可以了。许多人忘记了L1高速缓存,并通过其ID(即查找字段)以外的内容反复重复查找同一实体。如果需要执行此操作,则应保存ID或整个实体,以备将来查找。

    还有一个2级缓存(“查询缓存”)。NHibernate具有此内置功能。Linq to SQL和Entity Framework具有已编译的查询,可以通过编译查询表达式本身来帮助大大降低应用服务器的负载,但它不缓存数据。微软似乎认为这是应用程序问题,而不是数据访问问题,这是L2S和EF的主要弱点。不用说,这也是“原始” SQL的弱点。为了使用NHibernate以外的任何ORM都能获得真正好的性能,您需要实现自己的缓存外观。

    EF4还有一个L2缓存“扩展”,这是可以的,但实际上并不是批发替代应用程序级缓存。

  4. 查询数量

    关系数据库基于数据。它们确实很擅长在短时间内生成大量数据,但是在查询延迟方面却远远不及后者,因为每个命令都涉及一定的开销。设计良好的应用程序应发挥DBMS的优势,并尝试最小化查询数量并最大化每个查询中的数据量。

    现在,我并不是说只需要一行时就查询整个数据库。我想说的是,如果你需要CustomerAddressPhoneCreditCard,和Order排在同一时间,以满足单页,那么你应该他们都在同一时间,不另行执行每个查询。有时情况更糟糕的是,您将看到代码Customer连续查询5次相同的记录,首先要获取Id,然后是Name,然后是EmailAddress,然后...这简直是效率低下。

    即使您需要执行全部对完全不同的数据集运行的几个查询,将所有查询作为单个“脚本”发送到数据库并让其返回多个结果集通常更为有效。这是您所关心的开销,而不是数据总量。

    这听起来像是常识,但通常常常很容易失去对应用程序各个部分中正在执行的所有查询的跟踪。您的成员资格提供者查询用户/角色表,标题操作查询购物车,菜单操作查询站点地图表,侧边栏操作查询特色产品列表,然后您的页面可以分为几个独立的自治区域,分别查询“订单历史记录”,“最近浏览过的”,“类别”和“库存”表,在不知不觉中,您将执行20个查询,甚至无法开始为该页面提供服务。它只会完全破坏性能。

    一些框架-我在这里主要考虑的是NHibernate-对此非常聪明,可以让您使用称为Future的东西来对整个查询进行批处理,并尝试在可能的最后一刻立即执行所有查询。AFAIK,如果您想使用任何Microsoft技术来做到这一点,您就自己决定了;您必须将其构建到应用程序逻辑中。

  5. 索引,谓词和投影

    我与之交谈的开发人员中至少有50%,甚至某些DBA似乎都难以理解覆盖索引的概念。他们认为,“好吧,该Customer.Name列已建立索引,因此我对名称所做的每次查找都应该很快。” 除非Name索引无法覆盖您要查找的特定列,否则它将无法正常工作。在SQL Server中,这是通过语句完成INCLUDECREATE INDEX

    如果您天真地SELECT *在任何地方使用-除非您明确指定使用投影,否则几乎每个ORM都会这样做-DBMS可能会完全选择忽略索引,因为它们包含未覆盖的列。例如,投影意味着不这样做:

    from c in db.Customers where c.Name == "John Doe" select c
    

    您可以这样做:

    from c in db.Customers where c.Name == "John Doe"
    select new { c.Id, c.Name }
    

    这将对于大多数现代的ORM,指示它只是去查询IdName这大概是由索引覆盖的列(而不是EmailLastActivityDate或任何其他你碰巧在那里坚持列)。

    使用不适当的谓词也很容易完全吹走所有索引的好处。例如:

    from c in db.Customers where c.Name.Contains("Doe")
    

    ...看起来几乎与我们之前的查询相同,但实际上将导致全表扫描或索引扫描,因为它转换为LIKE '%Doe%'。同样,另一个看起来很简单的查询是:

    from c in db.Customers where (maxDate == null) || (c.BirthDate >= maxDate)
    

    假设您在上有一个索引BirthDate,则该谓词有很大的机会使其完全无用。我们这里的假设程序员显然尝试创建一种动态查询(“如果指定了该参数,则仅过滤出生日期”),但这不是正确的方法。改为这样写:

    from c in db.Customers where c.BirthDate >= (maxDate ?? DateTime.MinValue)
    

    ...现在,数据库引擎知道如何对其进行参数化并执行索引查找。对查询表达式的一个微小的,看似微不足道的更改会极大地影响性能。

    不幸的是,通常LINQ使得编写这样的错误查询变得非常容易,因为有时提供程序能够猜测您试图执行的操作并优化查询,而有时却不能。因此,最终您会得到令人沮丧的不一致结果,如果您只是编写普通的旧SQL,则结果将是显而易见的(无论如何,对于有经验的DBA)。

    基本上,所有这些都归结为以下事实:您必须密切关注生成的SQL及其所导致的执行计划,并且,如果您没有获得预期的结果,请不要害怕绕过偶尔对ORM层进行手工编码,然后对SQL进行编码。这适用于任何 ORM,而不仅仅是EF。

  6. 交易和锁定

    您是否需要显示最新的数据(毫秒)?也许-这取决于-但可能不是。可悲的是,实体框架没有给您nolock,您只能READ UNCOMMITTED事务级别(而不是表级别)使用。实际上,没有一个ORM对此特别可靠。如果要进行脏读,则必须降到SQL级别并编写临时查询或存储过程。因此,归根结底,就是您在框架内进行此操作的难易程度。

    实体框架在这方面已经走了很长的路要走-EF(在.NET 3.5中)的版本1真是太糟糕了,这使得突破“实体”抽象变得异常困难,但是现在您有了ExecuteStoreQueryTranslate,所以它真的还不错 与这些人交朋友,因为您会经常使用它们。

    还有写锁定和死锁的问题,以及在数据库中保持锁定的时间尽可能短的一般做法。在这方面,大多数ORM(包括Entity Framework)实际上往往比原始SQL 更好,因为它们封装了工作单元模式,在EF中为SaveChanges。换句话说,只要您愿意,就可以“插入”,“更新”或“删除”实体到您的心脏内容中,以确保在您提交工作单元之前,实际上不会有任何更改被推送到数据库中。

    请注意,UOW 与长时间运行的事务相似。UOW仍然使用ORM的乐观并发功能,并跟踪内存中的所有更改。在最终提交之前,不会发出任何DML语句。这样可以使交易时间尽可能短。如果使用原始SQL构建应用程序,则很难实现这种延迟行为。

    这对于EF特别意味着什么:使您的工作单元尽可能地粗糙,并且在绝对需要之前不要提交它们。这样做,您将得到比随机使用单个ADO.NET命令低得多的锁争用。

结论:

EF对于高流量/高性能应用程序来说完全合适,就像其他所有框架对于高流量/高性能应用程序一样。重要的是如何使用它。这是最流行的框架及其在性能方面提供的功能的快速比较(图例:N =不支持,P =部分,Y =是/受支持):

                                | L2S | EF1 | EF4 | NH3 | ADO
                                +-----+-----+-----+-----+-----
Lazy Loading (entities)         |  N  |  N  |  N  |  Y  |  N
Lazy Loading (relationships)    |  Y  |  Y  |  Y  |  Y  |  N
Eager Loading (global)          |  N  |  N  |  N  |  Y  |  N
Eager Loading (per-session)     |  Y  |  N  |  N  |  Y  |  N
Eager Loading (per-query)       |  N  |  Y  |  Y  |  Y  |  Y
Level 1 (Identity) Cache        |  Y  |  Y  |  Y  |  Y  |  N
Level 2 (Query) Cache           |  N  |  N  |  P  |  Y  |  N
Compiled Queries                |  Y  |  P  |  Y  |  N  | N/A
Multi-Queries                   |  N  |  N  |  N  |  Y  |  Y
Multiple Result Sets            |  Y  |  N  |  P  |  Y  |  Y
Futures                         |  N  |  N  |  N  |  Y  |  N
Explicit Locking (per-table)    |  N  |  N  |  N  |  P  |  Y
Transaction Isolation Level     |  Y  |  Y  |  Y  |  Y  |  Y
Ad-Hoc Queries                  |  Y  |  P  |  Y  |  Y  |  Y
Stored Procedures               |  Y  |  P  |  Y  |  Y  |  Y
Unit of Work                    |  Y  |  Y  |  Y  |  Y  |  N

如您所见,EF4(当前版本)的表现还不错,但是如果您最关心的是性能,那可能并不是最好的选择。NHibernate在这一领域更加成熟,甚至Linq to SQL也提供了EF仍然没有的一些性能增强功能。对于非常特定的数据访问方案,原始ADO.NET通常会更快,但是,如果将所有部分放在一起,它实际上并没有提供从各种框架中获得的许多重要好处。

而且,只是为了完全确保我听起来像是破记录,如果您没有正确设计数据库,应用程序和数据访问策略,那么这一切都不重要。上表中的所有项目均用于提高性能,使其超出基准。在大多数情况下,最需要改善的是基线本身。


38
真棒而全面的答案!

2
+1(如果可以的话,请提供更多信息)-这是我一段时间以来看到的最好的答案之一,而且我学到了一两件事-感谢分享!
2011年

1
即使我不同意上述所有内容,这也是一个很好的答案。比较ORM的表并不总是正确的。什么是实体延迟加载?您是说延迟加载列吗?L2S支持该功能。您为什么认为NH不支持编译查询?我认为可以预先编译命名的HQL查询。EF4不支持多个结果集。
Ladislav Mrnka,2011年

11
我不得不强烈反对无条件的“ EF对于高流量/高性能应用程序来说是完全好的”的说法,我们已经反复看到并非如此。当然,也许我们不同意“高性能”的含义,但是例如在500ms内优化网页并在框架内莫名其妙地花掉400ms以上(实际上只有10ms 达到SQL)在某些情况下并不理想,对于我们的开发团队来说,这是完全不可接受的。
尼克·克拉弗

1
关于EF期货的简单说明。它们不是由微软EF团队正式提供的,但是可以通过定义IQueryable <>的Future <>扩展的第三方项目来实现。例如EntityFramework。由LoreSoft扩展,在NuGet中可用。我在生产应用程序中进行的个人测试表明,使用Future在一个批次中打包数十个非相关查询(所有查询可以并行执行,没有一个要求上一个查询的结果)时,性能提高了10倍。同样,仅读取大量记录,而以后不更新时,AsNoTracking()也会大大提高性能。
DavidOlivánUbieto 2014年

38

编辑:基于@Aaronaught的一个很好的答案,我添加了一些针对EF的性能指标。这些新点以编辑为前缀。


高流量网站中性能的最大改善是通过缓存(首先避免任何Web服务器处理或数据库查询),然后进行异步处理以避免执行数据库查询时的线程阻塞来实现的。

您的问题没有防弹的答案,因为它始终取决于应用程序的要求和查询的复杂性。事实是,使用EF的开发人员生产力掩盖了复杂性,在许多情况下,复杂性导致EF的错误使用和糟糕的性能。您可以公开高级抽象接口以进行数据访问并且在所有情况下都可以正常工作的想法是行不通的。即使使用ORM,您也必须知道抽象背后发生了什么以及如何正确使用它。

如果您以前没有使用EF的经验,则在处理性能时会遇到很多挑战。与ADO.NET相比,使用EF时会犯更多的错误。另外,在EF中还有很多其他处理,因此EF总是比本地ADO.NET慢得多-这可以通过简单的概念证明应用程序来衡量。

如果要从EF获得最佳性能,则很可能必须:

  • 如果SQL探查器正确使用Linq-to-entities而不是Linq-to-objects,请非常仔细地使用SQL事件探查器修改数据访问并查看LINQ查询。
  • 非常小心地使用高级EF优化功能,例如 MergeOption.NoTracking
  • 在某些情况下使用ESQL
  • 预编译经常执行的查询
  • 考虑利用EF Caching包装器来获取“二级缓存”之类的功能,以进行某些查询
  • 在某些情况下,使用SQL视图或自定义映射的SQL查询(需要手动维护EDMX文件)来处理需要改进性能的常用投影或聚合
  • 对在Linq或ESQL中定义时无法提供足够性能的某些查询使用本机SQL和存储过程
  • 编辑:谨慎使用查询-每个查询都进行到数据库的单独往返。EFv4没有查询批处理,因为它无法针对每个执行的数据库命令使用多个结果集。EFv4.5将支持映射存储过程的多个结果集。
  • 编辑:认真处理数据修改。EF再次完全缺少命令批处理。因此,在ADO.NET中,您可以使用SqlCommand包含多个插入,更新或删除的单个命令,但是使用EF时,每个这样的命令将在数据库的单独往返中执行。
  • 编辑:仔细使用身份映射/身份缓存。EF有一种特殊的方法(GetByKey在ObjectContext API或FindDbContext API中)首先查询缓存。如果使用Linq-to-entities或ESQL,它将创建到数据库的往返,然后从缓存中返回现有实例。
  • 编辑:谨慎使用渴望加载。它并不总是双赢的解决方案,因为它产生了一个庞大的数据集。如您所见,这是很多额外的复杂性,这就是重点。ORM使映射和实现更加简单,但是在处理性能时,它将变得更加复杂,并且您必须进行权衡。

我不确定SO是否仍在使用L2S。他们开发了名为Dapper的新开源ORM ,我认为这一开发的主要目的是提高性能。


拉迪斯拉夫,这是一个非常有用的答案。这是我第一次听说Dapper(并因此发现了Massive的PetaPoco),这似乎是一个有趣的想法。

1
因此,SO现在似乎混合使用了LINQ to SQL和Dapper:samsaffron.com/archive/2011/03/30/…Quote“我们正在针对特定问题使用新的ORM [Dapper]:将参数化SQL映射到业务对象我们不会将其用作完整的ORM,它不会处理关系和其他问题,这使我们可以继续使用LINQ-2-SQL(在性能不重要的情况下),并移植所有内联SQL以使用我们的映射器,因为它更快,更灵活。”

5
@Slauma好,这是几个月前的一条声明,通常所有关于SO的新工作都在Dapper中完成,例如,我今天添加的新表甚至不在dbml文件中。
山姆·萨弗隆

1
@Sam:是否有关于当前SO数据访问策略的新博客文章?会有趣!在此期间,Dapper是否得到了扩展?我的理解是Dapper不是完整的ORM,也不支持关系-以及更新,插入,删除,事务,更改跟踪等问题
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.