优化可以分为三部分:第一是数据库优化、第二是代码优化、第三是算法优化。
因为我们平时对数据库操作最多的是查询操作,所以数据库的优化重点就体现在如何优化我们的查询操作。
主要有几点需要考虑:
循环操作改为批量操作
循环查询可以改为批量查询,再封装为map使用。循环插入可以改为批量保存。
需要什么查什么
尽量不要无脑SELECT *
,无脑查询全部字段会给网络和内存带来额外的压力。这是不必要的。
创建索引提高查询速度
使用索引也有几点需要注意:
优化特定的查询
优化COUNT()查询,统计行数时尽量使用COUNT(*),mysql对COUNT(*)做了优化,它会忽略所有的列而直接统计所有的行数。
优化联接查询:优化联接查询有几个注意点
优化LIMIT和OFFSET子句
在偏移量非常大的时候,例如,可 能是LIMIT 1000,20这样的查询,这时MySQL需要查询10020条记录然 后只返回最后20条,前面10 000条记录都将被抛弃,这样的代价非常高。可以通过延迟联接进行优化,充分利用索引的优势。
比如:SELECT id, name, age FROM ORDER BY age LIMIT 10000, 10;
可以通过延迟联接进行优化:
SELECT id, name, age FROM user
INNER JOIN (
SELECT id FROM user
ORDER BY age LIMIT 10000, 10
) AS temp USING(id);
这种方式在查询字段较多且字段长度较长时有明显的性能提升。
推荐阅读: 《高性能MySql(第4版)》
作为开发人员,对Mysql优化可以先看第7章(创建高性能的索引)和第八章(查询性能的优化)。
不要太过信任自己的代码,多推敲一下,往往问题就出现在我们深信不疑的地方。
下面列举一下平时使用的时候不注意,却可以锦上添花的地方。
Spring 的 BeanUtils.copyProperties
方法,是一个很好用的属性拷贝工具方法。当需要对两个对象的属性进行拷贝时,使用一行代码就能完成,十分简洁。BeanUtils.copyProperties(Object source, Object target)
。
但是,代码简洁的前提是以牺牲性能和安全为代价的。
不推荐的原因有二:
在类的每个对象实例中,每个成员变量都有一份副本,而成员静态常量只有一份实例。
反例:
public class Test {
String mac = "xxxls" + "abcdefg";
}
正例:
public class Test {
private static final String MAC_PREFIX = "xxxls";
String mac = MAC_PREFIX + "abcdefg";
}
JVM 支持基本类型与对应包装类的自动转换,被称为自动装箱和拆箱。装箱和拆箱都是需 要 CPU 和内存资源的,所以应尽量避免使用自动装箱和拆箱。
反例:
Integer num = 0;
for(int i = 0; i < 10; i++) {
num = num + 1; //相当于num = Integer.valueOf(num.intValue() + value);
}
正例:
int num = 0;
for(int i = 0; i < 10; i++) {
num = num + 1;
}
直接使用基本类型的包装类进行运算会造成大量的拆箱装箱代码,消耗资源,然而这种消耗完全是不必要的。而且,在超过包装类对象在内存中的缓存范围后,每次装箱都会创建对象。
在循环中直接用包装类进行计算性能损耗尤为严重。
在函数内,基本类型的参数和临时变量都保存在栈(Stack)中,访问速度较快,随着方法出栈,对应栈中的变量就清除;对象类型的参数和临时变量的引用都保存在栈(Stack)中,内容都保存在堆(Heap) 中,访问速度较慢,而且创建的对象需要经过gc进行回收。
避免不必要的装箱、拆箱和空指针判断。尽量使用基本数据类型进行计算。返回值也最好用基本类型。在 JDK 类库的方法中,很多方法返回值都采用了基本数据类型。比如: Collection.isEmpty()
和 Map.size()
。
反例:
public static double sum(Double v1, Double v2) {
v1 = Objects.isNull(v1) ? 0.0D : v1;
v2 = Objects.isNull(v2) ? 0.0D : v2;
return v1 + v2;
}
正例:
public static double sum(double v1, double v2) {
return v1 + v2;
}
反例:
List<Integer> list = ...;
for( int i = 0; i < list.size(); i++) {
//每次循环都会调用list.size()方法
...
}
正例:
List<Integer> list = ...;
int size = list.size();
for( int i = 0; i < size; i++) {
...
}
用移位操作可以极大地提高性能。对于乘除 2^n(n 为正整数)的正整数计算,可以用移位操作来代替。对于计算频次非常高的计算可以用位运算封住函数使用,使用频率不高的话性能差别不大。
int num1 = a * 4; //等价于 int num1 = a << 2;
int num2 = a / 4; //等价于 int num2 = a >> 2;
使用!取反会多一次计算,如果没有必要则优化掉。
if-else 语句,每个 if 条件语句都要加装计算,直到 if 条件语句为 true 为止。switch 语句进行了跳转优化,Java 中采用 tableswitch
或 lookupswitch
指令实现,对于多 常量选择分支处理效率更高。
经过试验证明:在每个分支出现概率相同的情况下, 低于 5 个分支时 if-else 语句效率更高,高于 5 个分支时 switch 语句效率更高。
因为String是final类,每一次拼接都会在堆内存中产生一个新对象。可以使用StringBuilder
拼接字符串。
正例:
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 100; i++) {
builder.append(i).append(" ");
}
使用""+
进行字符串转化,使用方便但是效率低,建议使用 String.valueOf
.
反例:
int i = 12345;
String s = "" + i;
正例:
int i = 12345;
String s = String.valueOf(i);
Pattern.compile()
方法的性能开销很大。如果多次用到同一个正则表达式进行匹配,可以先构建Pattern对象,重复利用。
compile()
方法中有一个循环,所以把正则编译放到循环里面执行,实际的时间复杂度要比预估的高一个数量级。
// Convert all chars into code points
for (int x = 0; x < patternLength; x += Character.charCount(c)) {
c = normalizedPattern.codePointAt(x);
if (isSupplementary(c)) {
hasSupplementary = true;
}
temp[count++] = c;
}
正例:
String regex = "\\w+";
Pattern PATTERN = Pattern.compile(regex);
String[] ss = {"aaa", "aba", "cba"};
for (String s : ss) {
boolean isMatched = PATTERN.matcher(s).matches();
}
类似的方法还有:
// 循环中调用多次
String source = "*********";
boolean isMatched = source.matches("regex");
String target = source.replaceAll("regex","result");
String[] targets = source.split("regex");
这些方法都使用了Java的正则匹配,如果在循环中调用,都会进行重复编译对应的正则表达式。正确的做法是把正则表达式的编译放到循环的外面,一次编译多次使用。
正确使用:
final Pattern PATTERN = Pattern.compile("regex");
//以下代码执行多次
String source = "*********";
boolean isMatched = PATTERN.matcher(source).matches();
String target = PATTERN.matcher(source).replaceAll("regex","result");
String[] targets = PATTERN.matcher(source).split(source);
Java 集合初始化时都会指定一个默认大小,当默认大小不再满足数据需求时就会扩 容,每次扩容的时间复杂度有可能是 O(n)。所以,尽量指定预知的集合大小,就能避免或减少集合的扩容次数。
推荐使用Google提供的类库中创建集合的工具类。lists、Sets、Maps
。
对于Map和Set初始化集合大小的注意事项。因为Map设置了装填因子,当元素大于(装填因子 x 容量)时就会进行扩容。所以初始化Map和Set的大小时new HashMap(int size)
是错误的操作。
Maps工具类中提供了Maps.newHashMapWithExpectedSize()
初始化Map。其中实现了计算容积的方法,使得已知元素个数的情况下,创建的Map刚好不会进行扩容。
static int capacity(int expectedSize) {
if (expectedSize < 3) {
CollectPreconditions.checkNonnegative(expectedSize, "expectedSize");
return expectedSize + 1;
} else {
return expectedSize < 1073741824 ? (int)((float)expectedSize / 0.75F + 1.0F) : Integer.MAX_VALUE;
}
}
返回空集合时,尽量使用Collections工具类中的空集合对象Collections中专门定义了空集合的静态常量,比如
Collections.emptyMap();
Collections.emptyList();
Collections.emptySet();
因为new出来的集合会占用一定内存空间。比如 new ArrayList()
会默认开辟大小为10的对象地址空间,new HashMap()
会创建初始大小为16的数组空间然而,返回空对象时这段空间就没有任何使用的意义,浪费了。
1.8之后是延迟加载,插入第一个元素时才创建空间,直接创建空集合性能影响不大。但是本来就有静态类可以用,又何必创建空集合的对象呢?
JDK 提供的方法可以一步指定集合的容量,避免多次扩容浪费时间和空间。同时, 这些方法的底层也是调用本地方法 System.arraycopy ()
方法实现,直接从内存上进行复制,进行数据的批量拷贝效率更高。比如List中的addAll()
方法。但是Map中的putAll()
方法还是循环调用的put()
方法,要注意区分。
比如用到了List的顺序性,参数类型传List;用到了Set的唯一性,则将Set作为参数。如果只是用到了集合存储数据的特性,则直接用Collection即可。好处是拓展性强。
值得一提的是,我们的项目中引入了MabatisPlus,MabatisPlus的接口中用到了大量的泛型、Collection、Object定义参数,这就是其拓展性很强的原因。我们用MabatisPlus写的CRUD方法其实就是对MabatisPlus接口的延伸,一样可以用类似的方法,让我们的代码有更好的兼容性。
比如:集合用Collection类型作为参数,这样Set和List都能调用这个查询、数字用Number作为参数,这样整形和浮点型都能调用,方法的复用性就提升了很多。
反例:
public void sout(List<Integer> list) {
for(Integer i: list) {
//遍历...
}
}
public void sout(Set<Integer> set) {
for(Integer i: set) {
//遍历...
}
}
正例:
public void sout(Collection<?> coll) {
for(Object i: coll) {
//遍历...
}
}
Service继承了BaseMapper后,虽然可以直接在别的注入了这个Service 的地方使用Mabatis进行查询,但是这种写法耦合度太高。
比如:我在ServiceA里写了一个查询ServiceB数据的方法 f()
,此时ServiceC刚好有相同的查询逻辑,如果ServiceC知道ServiceA里有 f()
,则需注入ServiceA进行调用,此时的调用链是ServiceC -> ServiceA -> ServiceB。另外,如果ServiceC不知知道ServiceA里有f(),那么ServiceC则需要重新写一个相同逻辑的查询接口在ServiceB中或者ServiceC中。
正确的方法应该是把 f()
写在ServiceB中,这样ServiceA和ServiceB都能复用f(),而且写在ServiceB中 f()
能更容易被发现。
在我们的代码中用到了大量的字符串拼接,定义的字符串却是直接写在代码中。这样做会导致复用性降低,且每一次调用相同的代码都会创建相同字符串对象进行拼接。可以新增一个接口保存这些使用频率非常高的字符串常量,进行使用。
算法优化一般来讲是最难处理的。因为算法的设计一般在程序设计阶段就已经确立了,当出现了性能问题要从算法上进行优化时一般来讲都会对之前的架构会有影响。这种情况下,优化的代价基本上就是重构代码。
一般来讲,算法优化的思路一般就两个:时间复杂度和空间复杂度。从这两个方向综合考虑优化,这往往需要丰富的经验和知识作支撑。