C#大型电商项目优化(二)——嫌弃EF与抛弃EF
上一篇博文中讲述了使用EF开发电商项目的代码基础篇,提到EF后,一语激起千层浪。不少园友纷纷表示:EF不适合增长速度飞快的互联网项目,EF只适合企业级应用等等。
也有部分高手提到了分布式,确实,性能优化从数据库出发,初期就加索引,然后垂直拆分,水平拆分,读写分离,甚至是分布式事务,阳春白雪,格局很高。然而笔者希望通过渐进的过程来优化这个项目,我们缩小格局,从细节查看不同方案的优劣。
之前提过,使用EF最主要的原因是项目时间紧迫,EF搭建速度快,熟悉的同事也多,使用方便。这个决策确实帮助我们挺过了初期的难关。在业务量增长的过程中,一些问题也逐渐暴露出来,我们开始针对问题做优化。
问题1:部分请求响应缓慢,影响用户体验。
使用EF做数据的增删改查,一些不规范代码也会拖慢程序效率,笔者在上一篇中已经提过。某些请求中可能包含多次数据查询与更新,如果这些细小的问题都以低效运行,那这个请求确实会很慢。然而在EF的框架下优化它,也未必能收到明显成效。以更新商品销量为例,我们上方案与代码:
方案1
先查出某一条数据,然后填入新算出的销量,再更新数据库,代码如下:
//先查出数据,再更新 var productSKU = unitOfWork.ProductSku.GetByID(dtos[0].Items[0].SKUId); productSKU.SalesCount = productSKU.SalesCount + dtos[0].Items[0].Quantity; unitOfWork.ProductSku.Update(productSKU); unitOfWork.Submit();
其响应时间如图:
方案2
采用IQuryable,之前提到过,这种查询方式不会真的将全部数据加载到内存,代码如下:
//2采用IQueryable查询更新 var productSKU1 = unitOfWork.ProductSku.Get(p => p.Id == dtos[0].Items[0].SKUId); foreach (var item in productSKU1) { item.SalesCount = item.SalesCount + dtos[0].Items[0].Quantity; unitOfWork.ProductSku.Update(item); } unitOfWork.Submit();
其响应时间如图:
方案3
直接使用SQL,简单粗暴,代码如下:
//3直接使用sql更新 string updateSql = @"update ProductSKU set SalesCount=SalesCount+" + dtos[0].Items[0].Quantity + " where Id='" + dtos[0].Items[0].SKUId.ToString() + "'"; unitOfWork.ProductSku.ExecuteSqlCommand(updateSql); unitOfWork.Submit();
其响应时间如图:
我们来分析下这三种方案:
方案1简单易懂,将数据查出来,更新后再塞回数据库,逻辑清晰,代码更清晰。可是将数据取出来,加载到内存,再对内存里的数据进行修改,最后生成SQL送回数据库执行,这套走下来,好像兜了一大圈,其实我们只想更新个销量。
方案2显得好了些,使用了我们在前一篇中讲到的理念,不将数据整个加载到内存,只到用时才加载,通过这种方式更新,其实生成的SQL和直接执行SQL差不太多了,但是我们可以发现:方案2和方案1时间差不多。在笔者预期中,方案2应该是比方案1好的,至于时间差不多的原因,应该是方案2用了一次循环,第二次循环时不符合条件,然后跳出循环,造成了些许时间浪费。如果是批量更新销量,方案2必然比方案1优秀很多。
方案3自然是简单粗暴,一条SQL搞定,毫不拖泥带水,其效果也是喜人的,几乎比方案1快1倍!
之前有园友提到索引的问题,其实我厂有专职DBA,不仅为数据量较大的表添加了聚集索引和非聚集索引,还写了定时任务去更新索引。所以索引这块我们不深究。
早起追逐开发效率阶段,我们可能将方案1优化为方案2,然后继续做新功能开发。但是现在我们有更多的选择,我们也有更多的时间去深究用户体验与系统效率了,那么自然的,我们开始嫌弃EF了。但是开发到这个程度,再去更换框架似乎不合适。而且大道至简,如果能用SQL搞定,那必是极好的。于是乎,我们开始对关键部分业务代码做重构,替换为原生SQL。这似乎是抛弃EF的开端。
问题2:部分数据量很大的表需要分表,EF难以维系
EF是ORM框架,映射数据库对象,然而同一数据库的一张表被拆分为两张以上,EF似乎没法映射了,两张表字段完全一致,难道再写个Model?而且分表是个动态的过程,也许一年分一次,也许一个月分一次,而且可能是定时任务去执行的,总不能一分表就改代码。自此,我们和EF的矛盾激化了。
园子里有关于数据库拆分的博客,我们所要改的只有数据访问层,最好不要动业务层。而且我们上文也提到:用原生SQL。那么我们就用原生SQL重写数据访问。
这里举个例子,比如我们拆分订单表,按年拆分,通常用户只会查看当年的订单,所以主表的查询次数肯定比其他分出的表要多,如果有用户要查往年的订单,我们再将查询范围扩大。按照这样的理念,我们开始重写数据访问层,并按照以下要点执行:
1.使用原生SQL
2.添加日期参数,如果日期超出主表的范围,则开始连接查询
3.查询中也可以添加其他条件,将参数作为对象填入SQL
4.抽象出一个生成SQL的公共方法,方便大家调用
拼装SQL是一件极其考验基本功的事,这个公共方法是我厂一位大师级的数据专家抽象出来的,大家也可以按照这个思路尝试下,一个简化版是很容易做出来的。
至此,项目中重要的业务功能已经和EF脱离关系了,我们也欣喜地收获了SQL带来的效率。而其他功能模块中,没有高并发场景或并不常用的应用,我们并没有做重构。
尽管本文标题是嫌弃,但如果是企业级应用,需要兼顾开发效率,且没有互联网模式下的业务量激增状况,笔者仍然推荐使用EF。