关于程序优化那些事(持续更新)
前言
干程序员这么多年了,零零碎碎也接触过关于优化这方面的东西,类似于创建索引、计算前置、异步编排等等。但是一直都没有过一个系统的总结,最近公司里由于有些接口响应实在太慢,于是借着这个解决问题的机会,好好谈谈 关于程序优化那些事。
首先,项目给你需求,让优化接口,你也不知道到底是因为什么原因导致的响应慢,是sql的问题,还是代码嵌套多层循环的问题,还是前端调用了多个接口导致的性能问题,还是并发问题,或者是架构问题。
所以在这种情况之下,只能一个一个排查:
数据库层面:
-
优化索引
不单单局限于简单的索引,复合索引、覆盖索引等都可以去考虑,这个往往成本很低但是收效很大;
-
优化sql
-
劲量避免子查询语句
-
用小表驱动大表
-
where语句后面先接最容易筛选的条件
-
sql尽量简单,计算移步到应用程序中
-
排序和分组很消耗数据库性能,如果可以尽可能放在代码里
-
-
考虑是否优化表结构
-
适当的冗余字段其实可以避免一些关联查询,效果还是不错的
-
数据量大的时候(千万级),考虑是否进行水平拆分
-
当字段个数太多,可以垂直拆分出经常使用的字段
-
-
数据库是否返回了不必要的字段,比如仅仅需要10个字段,一次性返回了100个
-
查询中避免造成索引失效
-
其他待补充
代码层面:
-
考虑代码逻辑执行顺序对性能的影响
比如接口代码分三块,ABC顺序执行,有时候调整顺序能够收效显著。
-
考虑代码细节
-
是否在循环里执行了数据库查询语句,能不能放到循环外面一次查出来
-
多次的数据库插入语句能否合并,修改语句合并
-
不必要的循环语句
-
禁止超过两次的循环嵌套
-
不必要的实时计算(可以考虑计算前置)
-
是否依赖的下游方法响应慢,影响这边性能
-
-
考虑是否需要缓存中间件
-
会被并发访问的数据库资源,得加缓存,防止大量请求直接打到数据库
-
本地缓存+redis双重控制,设计缓存策略
-
缓存更新三大类:自动失效、定时更新、主动通知更新(变更极少,通过MQ通知更新)
-
当然一切是有利有弊的,当使用缓存时,得避免缓存雪崩、击穿和穿透等问题。另外,数据库缓存一致性也是个很值得去探讨的问题
-
-
使用异步编排
-
对于顺序执行的几块代码,如果是互相之间没有什么相关性,可以考虑异步的方式
-
对于遍历一个大list,可以考虑去将list切分,异步分别遍历
-
对主要性能影响不大的代码,例如注册成功发送短信,也可以异步
-
需求层面:
考虑是否需求合理,是不是必要的?有时候修改稍微修改产品设计可以显著减少代码复杂度
前端层面:
考虑是否请求了不必要的接口,有时候不必要整个页面接口刷新
总结
总之数据库资源是宝贵的,尽量让数据库只进行简单的查询插入修改删除操作,计算都移步到内存中。
如果以上方法都无法起效,可以考虑是不是机器性能问题,对于财大气粗的公司可以直接选择堆机器,
当然也可能是大的架构方面的问题,这个需要提出来共同商讨。
代码优化细节:
-
Map和List在声明的时候,设置容量
-
尽量指定类、方法的final修饰符
如果指定了一个类为final,则该类所有的方法都是final的。Java编译器会寻找机会内联所有的final方法,内联对于提升Java运行效率作用重大,
-
当进行大量字符串拼接的时候,用StringBuilder代替String
-
尽可能使用局部变量
调用方法时传递的参数和方法中使用的变量,都存储在栈中,速度较快,其他变量,如静态变量,实例变量都在堆中创建,速度较慢,而且栈随着方法结束就关闭了,不需要额外的垃圾回收
-
及时关闭流数据库连接,IO流等
-
尽量减少对变量的重复计算
明确一个概念,即使是只有一行代码的方法,调用也是有消耗的,包括创建栈帧,维护栈内存安全,调用结束弹栈等,都是消耗一定性能的,所以尽量减少重复不必要的调用,比如
for(int i = 0;i<list.size();i++){} // 替换成 int length = list.size(); for(int i = 0;i<length;i++){}
-
尽量采用懒加载,用的时候才创建对象
-
异常处理
使用异常首先会创建新的对象,Throwable接口的构造函数调用名为fillInStackTrace()的本地同步方法,fillInStackTrace()方法检查堆栈,收集调用跟踪信息。只要有异常被抛出,Java虚拟机就必须调整调用堆栈,因为在处理过程中创建了一个新的对象。异常只能用于错误处理,不应该用来控制程序流程。
-
当复制大量数据时,建议使用System.arraycopy()命令
-
当乘法和除法是2的整次幂的时候,建议采用移位操作
-
循环内不要重复创建对象
for (int i = 1; i <= count; i++) { Object obj = new Object(); } // 替换为 Object obj = null; for (int i = 0; i <= count; i++) { obj = new Object(); }
-
基于效率和类型检查的考虑,应该尽可能使用array,无法确定数组大小时才使用ArrayList
-
尽量使用HashMap,ArrayList,StringBuilder,除非考虑到线程安全才去考虑HashTable,Vector,StringBuffer。后三者使用同步机制而导致性能开销
-
尽量避免使用静态变量,静态变量常驻内存。
-
对于锁的合理使用
-
避免发生死锁
-
建议使用同步代码块的替代同步方法
-
-
常量声明为static final
-
不要创建不使用的对象和不需要引用的类
-
避免使用反射
-
使用数据库连接池和线程池
-
使用缓冲流
-
顺序插入和随机访问次数多用ArrayList,随机插入和元素删除多用LinkedList
-
if语句多个条件时,将容易判断的的条件放最左面
-
equal()时,常量放左边
-
数组禁止toString()
-
禁止对超过范围的long进行下转型
-
基本数据类型转换成String 最快是包装类.toString(),String.value() 次之,+"" 最次
-
最快遍历map的方法是使用Iterator
-
对多个资源的close() 应该分别close()
-
对于ThreadLocal使用前或者使用后一定要先remove
当前一般会使用线程池技术,在使用结束后线程不会销毁而是重新放入线程池,在下次使用时可能get上次set的数据,这一般很难发现
-
重写方法加@Override
-
清楚知道这个方法由父类继承而来
-
getObj() 和get0bj() 能立马确定是否继承成功
-
在抽象类或者接口中修改方法名,实现类中会报错
-
-
用Objects代替equals
-
多线程获取随机数时,用ThreadLocalRandom而非Random
-
待补充