首页 > 代码库 > 从细节出发 纯后端性能优化实战小结

从细节出发 纯后端性能优化实战小结

最近2个月没做什么新项目 完全是对于旧的系统进行性能优化 避免超时 死锁 数据处理能力不够等常见的性能问题

这里不从架构方面出发 毕竟动大手脚成本比较高 那么我们以实例为前提 从细节开始 


优化角度

一.业务逻辑优化

二.DB优化

三.数据处理优化

四.锁与性能

五.细节


 业务逻辑优化

这一条不具有普遍性 不同的业务不同的场景 如果归纳起来 就是在不影响业务的前提下进行流程精简

1. 废弃冗余逻辑 

常见于各种基于数据库的检查 很多同学在维护别人代码的时候 没有深入理解别人的逻辑 也许别人在取数据的时候已经在查询条件中已经过滤了相关逻辑 而后来维护的同学又来了一次check

当然如果处于数据安全的角度 double check无可厚非,但是如果连锁都没有的double check 其实不做也罢。

毕竟 省一次dbcall 可能效果胜于你做的N多优化

2. 合并业务请求

出发点和上述一致 节省dbcall 但是存在一个矛盾的点 如果业务包在事务里 这条需要慎重考虑 事务的设计原则里 当然能小则小


DB优化

这个其实是比较核心的点

1. 索引优化

这个点比较泛泛 但是做好的人不多 一个专攻于索引优化的人也可以在运维方面独当一面了

我们拿实例来看一个索引优化例子

首先利其器 选中你需要的调试信息

技术分享

随便拿个典型sql来作示例 相关值为虚假值 仅供参考

SELECT DISTINCT TOP 1000 a.CustomerID FROM TravelTicket(nolock) aWHERE  a.TicketChargeDate < GETDATE()AND a.AvailableAmount > 999AND a.[Status] <>999AND a.IsInCome <>999AND a.IsInCome <>998 AND NOT EXISTS (SELECT TOP 1 1 FROM TicketCharge(NOLOCK) b 	WHERE b.chargetype = 999		AND b.IsSuccessful = 999		AND b.IsDeleted != 999		AND b.FeeMonth = ‘999‘		AND b.CustomerID = a.CustomerID)

在完全没有任何索引的前提下我们查询一遍看下效果

 SQL Server Execution Times:   CPU time = 0 ms,  elapsed time = 0 ms.SQL Server parse and compile time:    CPU time = 0 ms, elapsed time = 0 ms.SQL Server parse and compile time:    CPU time = 32 ms, elapsed time = 37 ms.(1000 row(s) affected)Table ‘TravelTicket‘. Scan count 1, logical reads 9069, physical reads 0, read-ahead reads 4248, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.Table ‘TicketCharge‘. Scan count 1, logical reads 262, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.(1 row(s) affected) SQL Server Execution Times:   CPU time = 109 ms,  elapsed time = 591 ms.SQL Server parse and compile time:    CPU time = 0 ms, elapsed time = 0 ms. SQL Server Execution Times:   CPU time = 0 ms,  elapsed time = 0 ms.

  执行计划如图

技术分享

如图中所示 存在2个聚集索引扫描 我们优化的目标是将扫描(scan)变为查找(seek)

技术分享

  在有聚集索引的表格上,数据是直接存放在索引的最底层的,所以要扫描整个表格里的数据,就要把整个聚集索引扫描一遍。在这里,聚集索引扫描就相当于一个表扫描。所要用的时间和资源与表扫描没有什么差别。并不是说这里有了“Index”这个字样,就说明执行计划比表扫描的有多大进步。当然反过来讲,如果看到“Table Scan”的字样,就说明这个表格上没有聚集索引。换句话说 上面那段sql存在2个表扫描。

  首先我们改变sql查找方式从not exists换为left join

SELECT TOP 1000 TravelTicket.CustomerIDFROM   TravelTicket(NOLOCK)        LEFT JOIN TicketCharge(NOLOCK)            ON  TravelTicket.CustomerID = TicketCharge.CustomerID                    AND AvailableAmount > 999            AND [Status] != 999            AND TicketCharge.chargetype = 999            AND TicketCharge.IsSuccessful = 999            AND IsDeleted != 999            AND travelTicket.IsInCome <> 999            AND TravelTicket.IsInCome <> 998            AND TicketCharge.FeeMonth =  ‘999‘WHERE  TicketChargeDate < GETDATE()        AND TicketCharge.IsSuccessful IS NULLGROUP BY        TravelTicket.CustomerID

  再运行一次看看

 SQL Server Execution Times:   CPU time = 0 ms,  elapsed time = 0 ms.SQL Server parse and compile time:    CPU time = 0 ms, elapsed time = 0 ms.(1000 row(s) affected)Table ‘Worktable‘. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.Table ‘Workfile‘. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.Table ‘TravelTicket‘. Scan count 1, logical reads 604, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.Table ‘TicketCharge‘. Scan count 1, logical reads 262, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.(1 row(s) affected) SQL Server Execution Times:   CPU time = 31 ms,  elapsed time = 151 ms.SQL Server parse and compile time:    CPU time = 0 ms, elapsed time = 0 ms. SQL Server Execution Times:   CPU time = 0 ms,  elapsed time = 0 ms.

  travelticket表的逻辑读降低到了10分之一以内。执行计划类似

  技术分享

  那么下面我们再进行索引优化 先来针对travelticket表。

  首先去掉不等于逻辑 当你的sql语句存在不等于逻辑的时候 索引将不会被使用。同样的情况也存在于“like”,is null或者is not null

SELECT top 1000 a.CustomerIDFROM   TravelTicket(NOLOCK) a        LEFT JOIN TicketCharge(NOLOCK)            ON  a.CustomerID = TicketCharge.CustomerID       AND a.AvailableAmount > 999	    AND (a.[Status] = 991 OR a.[Status] = 992 OR a.[Status] = 993 )	    AND (a.IsInCome = 991 OR a.IsInCome =993 OR a.IsInCome =994 OR a.IsInCome =995)	    AND TicketCharge.IsDeleted != 999            AND TicketCharge.chargetype = 999            AND TicketCharge.IsSuccessful =999            AND TicketCharge.FeeMonth =  ‘999‘WHERE  TicketChargeDate < GETDATE()       			AND TicketCharge.IsSuccessful IS NULLGROUP BY a.CustomerID

  这里有个细节我将distinct 换成了group by ,在数据量小的时候 这2者其实没什么区别,但是当数据量变得庞大后后者的效率是优于前者的。

  我们来继续添加索引

  技术分享

  技术分享

  再运行一次 看看

 SQL Server Execution Times:   CPU time = 0 ms,  elapsed time = 0 ms.SQL Server parse and compile time:    CPU time = 0 ms, elapsed time = 0 ms.(1000 row(s) affected)Table ‘Worktable‘. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.Table ‘Workfile‘. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.Table ‘TravelTicket‘. Scan count 1, logical reads 60, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.Table ‘TicketCharge‘. Scan count 1, logical reads 262, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.(1 row(s) affected) SQL Server Execution Times:   CPU time = 15 ms,  elapsed time = 188 ms.SQL Server parse and compile time:    CPU time = 0 ms, elapsed time = 0 ms. SQL Server Execution Times:   CPU time = 0 ms,  elapsed time = 0 ms.

  travelticket表的逻辑读再次降低到了10分之一以内。我们看看执行计划有哪些变化。

技术分享

  聚集索引扫描变成为索引查找,KO!

  技术分享


除了上述人为的添加索引 还有一种取巧的办法

  打开sql server profiler

  技术分享

  按照自己的需求新建一个跟踪脚本

  技术分享

  写个脚本循环跑这个语句然后保存跟踪脚本。打开推荐sql优化器

  技术分享

  导入刚才的跟踪脚本,并且选择索引优化

  技术分享

  执行分析

  技术分享

  技术分享


数据处理优化

  对于db优化在程序员能力已经到瓶颈的前提下,可以着手从应用程序上的细节出发,例如并行。

  所谓并行也就是多线程针对同任务分区分块协同处理。多线程的技术大家都很了解,这里突出以下线程同步的问题。例如我有1000个人我分10组任务执行,如何正确的保证当前10组任务正确的完成互相不冲突并且等到所有任务完成后才开启下一轮1000.

  这里介绍一个比较通用的方法,首先申明一个信号量队列List<ManualResetEvent>();

  取出1000条后对于1000条进行添加顺序标识表示并且模余分组。标识从1开始递增就可以,模余分组方法如下

  testInfos.GroupBy(i => i.index % workTaskCount).Select(g => g.ToList()).ToList();    

  其中index为刚才添加的顺序标识, workTaskCount为分组的任务数。

//循环处理批次任务foreach (var testGroup in testGroups){    var mre = new ManualResetEvent(false);    manualEvents.Add(mre);    var testMethodParam = new TestMethodParam    {        mrEvent = mre,        testGroup = TestGroup,        testParam = testParam    };    //线程池处理计划任务    ThreadPool.QueueUserWorkItem(DoTestMethod, testMethodParam);}if (manualEvents.Count != 0){    //等待所有信号量完成 切记这里最大值为64    WaitHandle.WaitAll(manualEvents.ToArray(), 30 * 60 * 1000);}

  DoTestMethod就是旧的任务处理逻辑,testMethodParam负责涵盖你旧逻辑中所需要的参数并且包含一个完成信号量。

  这里需要牢记的是WaitHandle等待的信号量最大值为64.

  如果你需要的任务分组数超过64那么这里推荐在DoTestMethod方法中不适用信号量,而是使用原子操作的标识,例如Interlocked.Increment(taskCount)。当taskCount累加到1000(你设计的当前批次值)就结束一轮。不过比起WaitHandle性能上要慢一些。


死锁问题

  锁超时的问题大多是因为表锁产生。解决表锁的问题说难也不难,不过需要牺牲性能。mssql针对主键的更新不会产生表锁而是产生行锁。针对这个问题那么死锁的问题初步解决起来就简单了。

  这是比较通用的处理方法

DECLARE @step INT DECLARE @id CHAR(12)create table #tmpTest  --创建临时表(	rec_index INT ,	id CHAR(12));INSERT INTO #tmpTest  (rec_index,ID) SELECT ROW_NUMBER() OVER(ORDER BY ID) AS rec_index,ID FROM TestTable(nolock) WHERE BID = @bIDSET @rowcount=0SET @step=1SELECT @rowcount=COUNT(*) FROM #tmpTest AS ttWHILE(@step<=@rowcount)BEGIN	SELECT @id=Id	FROM #tmpTest  AS tt	WHERE @step=rec_index	UPDATE TestTable    SET		UpdateUser = @UpdateUser,		UpdateTime = @UpdateTime,	WHERE  ID =@id	SET @step=@step+1END

 上篇先到此 优化无止境 下篇等空下来再写了

 

从细节出发 纯后端性能优化实战小结