软件的响应速度和稳定性直接影响到用户的满意度和留存率。如果一个应用加载缓慢或频繁卡顿,用户可能会选择卸载并转向竞争对手的产品。良好的性能是保证用户界面流畅、操作响应快速的基础,有助于提升用户粘性和正面评价。
本文不是教条式的指导,比如优化索引、重构代码等等这种形而上学的东西,而是重在动手实践,根据个人在日常开发中遇到的性能问题,通过具体的手段进行优化,比如加索引,到底索引加在哪,或者重构代码,应该在什么位置重构,代码应该怎么写。
一、通过索引优化性能
在数据库中加索引,能够优化查询、更新、删除的性能,但是索引并不是越多越好,这个还是需要根据实际情况的,举个例子,我们如果有如下的Sql
select email,phone from user where name = '张三'
上面的sql很简单,查询姓名是张三的手机号、邮箱,因为我们需要通过name
进行过滤,所以我们可以给name
添加一个索引,避免触发全标扫描。
二、留意隐式转换导致的性能问题
隐式转换是指在进行查询、比较、计算或数据连接等操作时,如果涉及到的操作数具有不同的数据类型,数据库管理系统(DBMS)会自动将其中一个或多个操作数的数据类型转换为兼容的类型,以便这些操作可以顺利完成。这种转换过程是自动发生的,无需用户在查询语句中显式指定转换类型。
这是一个在开发中不太容易犯错,但是一点出现问题极不容易发现的点。
比如我们有一个表sys_dict_data
其中有一个排序字段dict_sort
,因为是排序字段,我们定义成int
类型。如果我们代码里面写了下面一段Sql
select dict_value, dict_label
from sys_dict_data
where dict_type = 'car_usage_nature'
and dict_sort >= '3'
这个Sql是完全正确的,但是有一个问题,我们在where条件里面写的是dict_sort >= '3'
,也就是字符串'3'
,这样就导致了一个隐式转换。所以我们应该正确写dict_sort >= 3
三、避免大事务
在Spring Boot开发中,我们经常使用@Transactional
进行事务注解,因为注解是基于切面的,所以在方法开始就会开启事务,如果我们方法内部逻辑比较复杂,相对的事务就会比较长。
举个不太恰当的栗子,我们有个需求是从数据库A表查询数据,查出来之后,对数据进行加工,加工之后,将数据插入到B表。数据查询方法比如是selectMethod()
、数据计算方法是computeMethod()
、数据插入方法是insertMethod()
,如果我们有以下代码
@Transactonal(rollbackFor = Exception.class)
public void baseMethod(){
selectMethod();
computeMethod();
insertMethod();
}
在selectMethod()
方法中,因为我们只是查询数据,所以是不需要事务的,比如上面的方法,我们实际上就把事务给放大了。为了减小事务,我们可以把selectMethod()
、computeMethod()
放到事务外面。
public void baseMethod(){
selectMethod();
computeMethod();
}
@Transactonal(rollbackFor = Exception.class)
public void insertMethod(){
}
当然,因为事务是基于切面的,我们需要注意需要在外面调用insertMethod()
避免事务失效。
四、减少数据库查询数据
这个如果是平时开发可能遇到的比较少,但是基于敏捷开发平台的遇到的会相对多点。
比如我们现在的敏捷开发平台,平时创建表单时,会基于模型,但是因为表单涉及增删改查,所以模型上会有所有的表、字段。有时候与别的模块对接,为了省事,直接使用模型取数,但是对接过程中,我们只使用主表数据,子表取数就白白的消耗掉了性能。这种情况其实很好解决,我们在取数时,做到按需取数即可。
五、减少重复取数
这种情况,我觉得一般出现在取系统参数的过程中。在系统不断的迭代过程中,我们不断增加新的参数,如果增加一个参数就取一遍数据,也会导致大量的sql。针对这种情况,我们可以提取一个公共方法,将要获取的参数都放里面,尽量一次性取出来,并对参数进行缓存。避免重复从数据库取数。
六、使用Redis缓存
针对全局参数、一些配置参数等,因为改动很少,我们可以借助Redis对数据进行缓存。在获取参数时,先从Redis获取,如果获取到直接返回,如果获取不到,再从数据库获取,并将获取到的数据缓存到Redis中,在参数保存时,清掉Redis缓存的数据。
七、合理使用order by
order by
是一个成本很高的操作,如果非必要尽量不要使用order by
,如果需要使用order by
,尽可能在order by
子句使用索引字段,减少排序的成本。
八、减少使用临时表
这里说的临时表,指的不仅仅是全局临时表也包括实表临时表(也就是一个表当临时表用),临时表如果使用不当,比如频繁地创建和销毁临时表,或者临时表设计不合理导致索引缺失,可能降低整体的数据库性能。另外,现在数据库种类很多,不同数据库对临时表的语法不尽相同,会增加程序移植的复杂度。
涉及使用临时表的地方,可以考虑使用left join
或inner join
代替。
九、关于exists关键字
在我们的开发思维中,我们一般认为在大数据量时,exists
的性能要比in
的性能高,但是在国产神通数据库中,exists
的性能却是特别的差(当然in
也不高)。针对这种,我们可能需要针对特定数据库对sql进行优化,像神通数据库,我们可以改成left join
并判断主键是否是null
。
十、关于or关键字
当一个字段只有某几个值时,我们可能会这样查询
select * from table where filed = '1' or filed = '2'
针对这种,我们可以尝试使用union all
select * from table where filed = '1'
union all
select * from table where filed = '2'
十一、串行改并行
当函数串行时,整个计算过程耗时是所有函数耗时的和,改成并行后,理论上,耗时是最慢的一个函数的耗时。
比如A调用B\C\D三个方法,其中B方法耗时1s,C方法耗时1.5s,D方法耗时2s,如果串行的话,不考虑A自身耗时,那么调用B\C\D的耗时为4.5s = 1s + 1.5s + 2s
,如果我们改成并行,那么理论上耗时为 2s
,也就是耗时最长的D方法的耗时。
当然,如果涉及到多线程之间的同步,我们可以借助CountDownLatch
等并发工具类,实现线程之间的同步。下面是一个简单的示例。
@Slf4j
public class CountDownLatchCase1 {
public static void main(String[] args) throws InterruptedException {
// 创建 CountDownLatch 对象,需要等待 3 个线程完成任务
CountDownLatch latch = new CountDownLatch(3);
// 创建 3 个线程
Worker worker1 = new Worker(latch, "worker1");
Worker worker2 = new Worker(latch, "worker2");
Worker worker3 = new Worker(latch, "worker3");
// 启动 3 个线程
worker1.start();
worker2.start();
worker3.start();
// 等待 3 个线程完成任务
latch.await();
// 所有线程完成任务后,执行下面的代码
log.info("All workers have finished their jobs!");
}
}
class Worker extends Thread {
private final CountDownLatch latch;
public String name;
public Worker(CountDownLatch latch, String name) {
this.latch = latch;
this.name = name;
}
@Override
public void run() {
try {
// 模拟任务耗时
TimeUnit.MILLISECONDS.sleep(1000);
log.info("{} has finished the job!", name);
} catch (InterruptedException e) {
log.error(e.getMessage(), e);
} finally {
// 一定要保证每个线程执行完毕或者异常后调用countDown()方法
// 如果不调用会导致其他线程一直等待, 无法继续执行
// 建议放在finally代码块中, 防止异常情况下未调用countDown()方法
latch.countDown();
}
}
}
十二、分库分表
当数据库中表的数据量过大时,可以考虑分库分表。比如每个月的数据量都比较大,我们考虑按月或者按年进行分表,每个月或者每年一张表。当然分库分表会增加后续查询的复杂度。
十三、分页
其实原本是不打算写这条的,因为分页是一个程序必备的东西,后来想想还是加上了,其实不管自己写Sql也好,各个ORM框架也都提供了分页的组件。
评论 (0)