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

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

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

预先感谢您。

评论

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

#1 楼

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

如果不需要在数据库提供程序和不同的每个客户端表布局之间进行切换,并且主要读取数据,并且不需要能够在EF,SSRS,ADO.NET数据服务等中使用相同的模型-然后,如果您想要绝对的性能作为关键指标,那么做起来可能比看dapper更糟。在基于LINQ-to-SQL和EF的测试中,我们发现EF在原始读取性能方面明显较慢,这大概是由于抽象层(在存储模型等之间)和实现。 >在SO,我们对原始性能非常着迷,并且我们很高兴承受失去一些抽象的开发重心以提高速度。因此,我们查询数据库的主要工具是dapper。这甚至允许我们使用我们现有的LINQ-to-SQL模型,但很简单:堆速度更快。在性能测试中,它的性能基本上与手动编写所有ADO.NET代码(参数,数据读取器等)完全相同,但是没有冒错输入列名的风险。但是,它是基于SQL的(尽管很高兴使用SPROC(如果您选择了这种方法))。这样做的好处是不涉及其他处理,但是它是一个喜欢SQL的人使用的系统。我认为这不是一件坏事!例如,一个典型的查询可能是:

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

请注意,在此答案中,我并不是说EF不适合大批量工作。简而言之:我知道精致的服装就可以了。

评论


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

–本
2011年10月31日20:09

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

– niaher
2011年11月1日于10:43

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

–user2528
2011年11月1日22:39

@ acidzombie24 dapper支持事务,而dapper的贡献部分(不是nuget部署的一部分)正在获得insert等选项。只是为了完整性而提及。我很高兴小巧方便。

– Marc Gravell♦
2011年11月1日22:43

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

– Marc Gravell♦
17年12月28日在16:17

#2 楼

当涉及大规模应用程序中的整体数据访问策略和性能优化时,“我应该使用哪个ORM”这个问题确实针对一个巨大的冰山一角。大体上按重要性排序)将影响吞吐量,并且所有主要的ORM框架(有时以不同的方式)都处理了它们:数据库设计和维护

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

如果您没有使用适当的规范化技术,那么您的网站必定会失败。如果您没有主键,几乎每个查询的速度都会很慢。如果您无缘无故地使用众所周知的反模式(例如,将表用于键-值对(AKA实体-属性-值)),则会激增物理读写次数。 >如果您不利用数据库提供的功能,例如页面压缩,FILESTREAM存储(用于二进制数据),SPARSE列,hierarchyid用于层次结构等(所有SQL Server示例),那么您将不会在您可以看到的性能附近看到任何地方。

设计数据库并让自己确信它尽可能好之后,应该开始担心数据访问策略。


Eager vs.延迟加载

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

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

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


缓存

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

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

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


查询数量

关系数据库基于数据集。它们确实很擅长在短时间内生成大量数据,但在查询延迟方面却远远不及后者,因为每个命令都涉及一定量的开销。设计良好的应用程序应发挥DBMS的优势,并尝试减少查询数量并最大化每个查询中的数据量。您只需要一行。我的意思是,如果您同时需要全部CustomerAddressPhoneCreditCardOrder行以提供一个页面,那么您应该同时要求全部,不要分别执行每个查询。有时情况更糟,您会看到连续查询5次相同的Customer记录的代码,首先是获取Id,然后是Name,然后是EmailAddress,然后...效率低下。 br />即使您需要执行全部对完全不同的数据集进行操作的几个查询,将所有查询作为单个“脚本”发送到数据库并让其返回多个结果集通常更为有效。这是您所关心的开销,而不是数据总量。

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

一些框架-我在这里主要考虑的是NHibernate-对此非常聪明,允许您使用称为Future的东西来对整个查询进行批处理并尝试执行它们在可能的最后一刻全部完成。 AFAIK,如果您想使用任何Microsoft技术来做到这一点,您就自己掌握了;您必须将其构建到您的应用程序逻辑中。在覆盖索引的概念上遇到麻烦。他们认为,“好吧,Customer.Name列已建立索引,因此我每次对该名称进行的查找都应该很快。”除非它不能正常工作,否则Name索引将覆盖您要查找的特定列。在SQL Server中,这是通过INCLUDE语句中的CREATE INDEX完成的。

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

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


您执行此操作: />对于大多数现代ORM,这将指示它仅查询可能由索引覆盖的IdName列(但不会查询EmailLastActivityDate或您碰到的其他任何列)。

使用不合适的谓词也很容易完全吹走所有索引的好处。例如:类似地,另一个看起来非常简单的查询是:假设您在LIKE '%Doe%'上有一个索引,则该谓词有很大的机会使其完全无用。我们这里假设的程序员显然尝试创建一种动态查询(“如果指定了该参数,则仅过滤出生日期”),但这不是正确的方法。改为这样写:

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


...现在,数据库引擎知道如何对其进行参数化并执行索引查找。对查询表达式的一个微小的,看似微不足道的更改会极大地影响性能。去做和优化查询,有时却不是。因此,如果您只是编写普通的旧SQL,最终结果将令人沮丧地不一致,这对本来是显而易见的(无论如何,对于有经验的DBA)。基本上,所有这些都归结为这样一个事实,即您确实必须密切关注生成的SQL及其导致的执行计划,并且,如果您没有获得期望的结果,请不要害怕绕过偶尔对ORM层进行手工编码,然后对SQL进行编码。这适用于任何ORM,而不仅仅是EF。


事务和锁定

您是否需要显示最新的数据?也许-这取决于-但可能并非如此。可悲的是,Entity Framework并没有给您BirthDate,您只能在事务级别(而非表级别)使用nolock。实际上,没有一个ORM对此特别可靠。如果要进行脏读,则必须降到SQL级别并编写临时查询或存储过程。因此,归根结底,又是您在框架内完成此操作的难易程度。 3.5)真是令人敬畏,这使得突破“实体”抽象变得异常困难,但是现在您有了ExecuteStoreQuery和Translate,所以它还不错。与这些人交朋友,因为您会经常使用它们。在这方面,大多数ORM(包括实体框架)实际上往往比原始SQL更好,因为它们封装了工作单元模式,在EF中为SaveChanges。换句话说,只要您愿意,就可以“插入”,“更新”或“删除”实体到您的心脏内容中,以确保在您提交工作单元之前,实际上不会有任何更改被推送到数据库。 br />
请注意,UOW与长时间运行的事务不相似。 UOW仍使用ORM的乐观并发功能,并跟踪内存中的所有更改。在最后一次提交之前,不会发出任何DML语句。这样可以使交易时间尽可能短。如果您使用原始SQL来构建应用程序,则很难实现这种延迟的行为。

这对于EF特别意味着什么:使您的工作单元尽可能的粗略,直到您将其提交为止绝对需要。这样做,您最终将获得比随机使用单个ADO.NET命令要低得多的锁争用。


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

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


您可以看到,EF4(当前版本)的运行情况还不错,但是如果您最关心的是性能,那可能并不是最好的选择。 NHibernate在这一领域更加成熟,甚至Linq to SQL也提供了EF仍然没有的一些性能增强功能。对于非常特定的数据访问方案,原始ADO.NET通常会更快,但是,如果将所有内容放在一起,它实际上并没有提供从各种框架中获得的许多重要好处。 >
并且,为了完全确保我听起来像是破记录,如果您没有正确设计数据库,应用程序和数据访问策略,那么这一切都没有关系。上表中的所有项目均用于改善超出基准的性能;在大多数情况下,基线本身就是最需要改进的地方。

评论


真棒而全面的答案!

– Slauma
2011-10-30 18:10

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

–碎玻璃
2011年10月30日19:36

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

–拉迪斯拉夫·姆恩卡(Ladislav Mrnka)
2011年10月30日20:50

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

–尼克·克拉弗♦
2011年10月31日上午10:34

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

– DavidOlivánUbieto
2014年3月26日13:00



#3 楼

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


高流量网站中最大的性能改进是通过缓存(首先避免任何Web服务器处理或数据库查询),然后进行异步实现的。处理,以避免在执行数据库查询时出现线程阻塞。事实是,使用EF的开发人员生产力掩盖了复杂性,在许多情况下,复杂性导致EF的错误使用和糟糕的性能。您可以公开高级抽象接口以进行数据访问,并且在所有情况下都可以正常工作的想法是行不通的。即使使用ORM,您也必须知道抽象背后发生了什么以及如何正确使用它。

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


非常小心地使用SQL事件探查器修改数据访问,并检查LINQ查询(如果它们正确使用了Linq-to-entities而不是Linq)到对象
非常小心地使用高级EF优化功能,例如MergeOption.NoTracking

在某些情况下使用ESQL
经常执行的预编译查询
考虑利用EF缓存包装器的功能,以获得某些查询的“二级缓存”之类的功能
在某些情况下,使用SQL视图或自定义映射的SQL查询(需要手动维护EDMX文件)来处理需要改进性能的经常使用的投影或聚合
在某些查询无法提供足够性能时,请使用本机SQL和存储过程在Linq或ESQL中定义

编辑:谨慎使用查询-每个查询都进行到数据库的单独往返。 EFv4没有查询批处理,因为它无法针对每个执行的数据库命令使用多个结果集。 EFv4.5将为映射的存储过程支持多个结果集。

编辑:仔细处理数据修改。再次,EF完全缺少命令批处理。因此,在ADO.NET中,您可以使用包含多个插入,更新或删除的单个SqlCommand,但使用EF时,每个这样的命令将在与数据库的单独往返中执行。 。 EF有一种特殊方法(ObjectContext API中的GetByKey或DbContext API中的Find)首先查询缓存。如果您使用Linq-to-entities或ESQL,它将创建到数据库的往返,然后它将从缓存中返回现有实例。它并不总是双赢的解决方案,因为它产生了一个巨大的数据集。
您可以看到,这是很多额外的复杂性,这就是重点。 ORM使映射和实现更加简单,但是在处理性能时,它将变得更加复杂,并且您必须进行权衡。

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

评论


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

– niaher
2011年10月30日在17:07

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

– Slauma
2011年10月30日17:11

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

– Sam Saffron
2011年10月31日上午10:27

@Sam:是否有关于当前SO数据访问策略的新博客文章?会很有趣! Dapper在此期间是否得到延长?我的理解是,Dapper不是完整的ORM,也不支持关系-以及更新,插入,删除,事务,更改跟踪等问题。

– Slauma
2011年10月31日,11:16