15天的性能优化工作,5方面的调优经验

前一段时间一直在做性能调优的工作,颇有收获。因此,简单的总结并分享下研究成果。性能调优很有趣但也是个无底洞,不可能在一篇文章全部阐述完。这里只是提供一个方向,以后碰到了知道可以从方向方面入手即可。具体如下:

 

 代码层面

  for循环中不要利用 + 号去拼接字符串

在循环次数比较多的for循环中,我们也不要利用 + 号去拼接字符串。具体例子如下:

具体解决方法如下:

  •   根据具体的业务场景,使用 StringBuffer(线程安全)或者 StringBuilder(非线程安全)
  •   使用数组

  设置容量参数提高系统性能

对于 StringBuffer(线程安全)或者 StringBuilder(非线程安全),都有相应的构造方法:

如果我们可以事先知道需要拼接的字符串长度,设置容量参数,防止 StringBuffer 在源码内部进行一系列复杂的内存复制操作,影响性能。

如上面的

StringBuilder stringBuilder = new StringBuilder(Integer.MAX_VALUE);

  for循环建议写法

for (int i = 0, int length = list.size(); i < length; i++)

方法的返回值

返回List:

解决方法:

返回Set:

Collections.EMPTY_SET

返回Map:

Collections.EMPTY_MAP

返回Boolean:

Boolean.TRUE

  不要再for循环中查询数据库

解决:

  •   根据业务场景,把for循环中的多次连接数据库查询,写到sql中去查询,既一次性查询出来
  •   根据业务场景,看是否可以利用缓存,提高查询效率

  去掉System.out.println

代码部署到生产环境前,去掉全部System.out.println

  四种数组复制方式的性能比较和抉择

数组copy有很多种方法,效率不一。我们先看下面具体实例:

运行结果:

多运行几次,我们得出数组复制效率:

System.arraycopy > clone > Arrays.copyOf > for

综上所述,当复制大量数据时,使用System.arraycopy()命令。

  实现高性能的字符串分割

实现字符串的分割的方法有很多种,常用的是 split ,StringTokenizer ,indexOf 和 substring 的配合,以及一些开源工具类,如:StringUtils。它们各有优缺。

运行结果:

从上面例子可以看出,字符分割的性能,由高到低的排序为:StringTokenizer > split ,StringUtils.split > indexOf 。有些书籍写着 indexOf 的性能是最高的,但是按照我的测试,index的性能是最差的。但是事物都有两面性,从上面的例子也可以看出,虽然 StringTokenizer 的性能高,但是代码量多,可读性差,而 split 代码相对就整洁多了。

  切勿把异常放置在循环体内

try-catch语句本身性能不高,如果再放到循环体中,无非是雪上加霜。因此在开发中,我们要极力避免。

例:

正确做法:

综上所述:不要再循环体内执行复制,耗时的操作。

  尽量缩小锁的范围

锁优化的思路和方法总结一下,有以下几种。

  • 减少锁持有时间(尽量缩小锁的范围)
  • 减小锁粒度
  • 锁分离
  • 锁粗化
  • 锁消除

我们应该确保我们只在必要的地方加锁,将锁从方法声明移到方法体中会延迟锁的加载,进而降低了锁竞争的可能性。先看下面的实例:

打印结果:

总结:因为,一个线程访问了 synchronized 同步代码块中的代码,另一个线程不可以访问该对象的任何同步代码块,但可以访问非同步代码块。所有缩小锁的范围可以在一定程度上提高代码性能。

  锁分离

  • 最常见的锁分离就是读写锁ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能。

还有就是网上一个高手写的一个例子:

优化后:

 文件导入导出注意使用缓存流

批量插入数据性能优化

1.1 问题一

直接批量保存3万多条数据。

1.2 问题二

批量保存时,利用UUID生成工具,给主键设置Id。找出Hibernate的先查询后更新的机制触发,造成不必要的查询损耗。

1.1 问题一解决方法

对于问题二,我们可以把所有数据,每500条进行一次批量保存操作,速度会比一次性批量保存好。具体如下:

1.2 问题二解决方法

对于问题三,由于Hibernate在进行插入时,会判断数据是进行插入还是进行更新。如果模型的主键不为空,查询数据后,再进行更新数据,否则,进行插入数据操作。因此,我们在进行插入操作时候,不要设置模型的主键,可以避免不必要查询消耗。

pcsTestcase.setId(UUIDUtils.generate());

  业务层面

  • 减少前端请求数
  • 过度复用方法带来的性能问题
  • 后端如果需要一次性加载数据,防止多次请求数据库

  数据库层面

SQL语句大小写规范

我们在写SQL的时候,通常会出现大小写混用的情况。如下:

select * FROM pm_testcase pt where pt.Name = ‘ay’

正确的做法是SQL语句全部大写或者全部小写。如下:

–全部小写

select * from pm_testcase pt where pt.name = ‘ay’

–全部大写

SELECT * FROM PM_TESTCASE PT WHERE PT.NAME = ‘ay’

PostgreSQL执行计划

PostgreSQL的执行计划,做为数据库性能调优的利器,有必要在开头简单的介绍下。

cost说明:

  •   第一个数字0.00表示启动cost,这是执行到返回第一行时需要的cost值。
  •   第二个数字4621.00表示执行整个SQL的cost

通过查看执行计划,我们就能够找到SQL中的哪部分比较慢,或者说花费时间多。然后重点分析哪部分的逻辑,比如减少循环查询,或者强制改变执行计划。

更多执行计划 Explain,可网上搜索。

建立索引避免全表扫描

首先,在数据库里有一张表 pm_testcase,里面有150万条数据。

如下SQL,我们利用执行计划,对创建时间(created_time)进行排序,输出执行计划结果。

cost=说明:

第一个数字4103259.72表示启动cost,这是执行到返回第一行时需要的cost值。

第二个数字4107084.44表示执行整个SQL的cost。

该语句总共耗时 4107084.44

这里我们创建 created_time 索引,对相同语句执行 程序清单 2-1 的SQL,得到的执行计划结果为:

很明显,执行整个SQL的 cost 由 4107084.44 减少到 384739.28

因此,为了避免全表扫描,建议在考虑在 where 及 order by 涉及的列上建立索引。

防止索引失效

我们应尽量避免在 where 子句中使用 != 或 <> 操作符,否则引擎将放弃使用索引而进行全表扫描。

如下例子,我们在 pm_testcase 的 code 上添加了索引:

通过上面的例子可以看出,!= 操作符使得索引失效。

避免建立太多的索引

索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 和 update 的效率,因为 insert 或 update 时有可能会重建索引,所以视具体情况而定。一个表的索引数最好不要超过7个,若太多则应考虑一些不常使用到的列上建的索引是否有必要.

关于查询效率的几点建议

  •   尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。
  •   尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为变长字段存储空间小,对于查询来说,在一个相对较小的字段内搜索效率显然要高些。
  •   最好不要给数据库留NULL,尽可能的使用 NOT NULL填充数据库。备注、描述、评论之类的可以设置为 NULL。其他的,最好不要使用NULL。
  •   任何地方都不要使用 select from t ,用具体的字段列表代替 ,不要返回用不到的任何字段。
  •   应尽量避免在 where 子句中使用 or 来连接条件,可以考虑使用 union 代替
  •   in 和 not in 也要慎用。对于连续的数值,能用 between 就不要用 in,exists 代替 in
  •   尽量避免在 where 子句中对字段进行表达式操作和函数操作

在Join表的时候字段使用相同类型,并将其索引

如果你的应用程序有很多JOIN查询,你应该确认两个表中Join的字段是被建过索引的。这样,SQL内部会启动为你优化Join的SQL语句的机制。而且,这些被用来Join的字段,应该是相同的类型的。例如:如果你要把 DECIMAL 字段和一个 INT 字段 Join 在一起,SQL 就无法使用它们的索引。对于那些STRING 类型,还需要有相同的字符集才行。(两个表的字符集有可能不一样)程序员站

优化子查询

子查询很灵活可以极大的节省查询的步骤,但是子查询的执行效率不高。执行子查询时数据库需要为内部嵌套的语句查询的结果建立一个临时表,然后再使用临时表中的数据进行查询。查询完成后再删除这个临时表,所以子查询的速度会慢一点。

我们可以使用join语句来替换掉子查询,来提高效率。join语句不需要建立临时表,所以其查询速度会优于子查询。大部分的不是很复杂的子查询都可以替换成join语句。

  服务器层面

服务器的调优,就得根据客户提供的真实环境的配置。如服务器是几核几个CPU等等。服务器的硬件指标确定下来后,根据指标调整Tomcat,JDK,数据库,Apatch等配置参数。让整个环境达到最优的效果。这块工作一般不是开发人员进行的。但是我们要了解清楚一些配置参数

Tomcat && JDK

  •   tomcat 配置
  •   JDK垃圾回收机制
  •   垃圾回收机制算法选择
  •   JVM内存模型

Postgresql数据库配置

Linux服务器

输出系统日志最后10行 dmesg | tail

该命令会输出系统日志的最后10行。这些日志可以帮助排查性能问题。

top命令

top命令是进行性能分析最常使用的命令,也是最重要的命令。每个参数代表什么意思,都必须非常清楚。

top命令包含了前面好几个命令的检查的内容。比如系统负载情况(uptime)、系统内存使用情况(free)、系统CPU使用情况(vmstat)等。因此通过这个命令,可以相对全面的查看系统负载的来源。同时,top命令支持排序,可以按照不同的列排序,方便查找出诸如内存占用最多的进程、CPU占用率最高的进程等。

但是,top命令相对于前面一些命令,输出是一个瞬间值,如果不持续盯着,可能会错过一些线索。这时可能需要暂停top命令刷新,来记录和比对数据。

第一行:

解释:

第二行和第三行,当有多个CPU时,这些内容可能会超过两行。内容如下:

最后两行为内存信息。内容如下:

进程信息区统计信息区域的下方显示了各个进程的详细信息。首先来认识一下各列的含义。

查询登录当前系统的用户信息:w命令

可查询登录当前系统的用户信息,以及这些用户目前正在做什么操作

iostat

运行时,如果出现下面的提示信息

执行下 sudo apt-get install sysstat 即可。

Iostat提供三个报告:CPU利用率、设备利用率和网络文件系统利用率,使用-c,-d和-h参数可以分别独立显示这三个报告。

内存分析命令:free m

free: 查看系统内存的使用情况,-m参数表示按照兆字节展示。

最后两列分别表示用于IO缓存的内存数,和用于文件系统页缓存的内存数。需要注意的是,第二行-/+ buffers/cache,看上去缓存占用了大量内存空间。这是Linux系统的内存使用策略,尽可能的利用内存,如果应用程序需要内存,这部分内存会立即被回收并分配给应用程序。因此,这部分内存一般也被当成是可用内存。如果可用内存非常少,系统可能会动用交换区(如果配置了的话),这样会增加IO开销(可以在iostat命令中提现),降低系统性能。

查看CPU的占用情况 mpstat

显示每个CPU的占用情况,如果有一个CPU占用率特别高,那么有可能是一个单线程应用程序引起的。

  前端

由于我不是做前端的工作,所以没有太多的经验总结。不过代码方面性能优化有些是可以运用到前端。同事也可以减少前端的请求链接等等手段去优化。这里由于本人不了解,不做过多的阐述。

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注