TypechoJoeTheme

香草物语

统计
登录
用户名
密码
/
注册
用户名
邮箱
输入密码
确认密码

常见的后端性能优化方法

Laughing博主
2024-06-30
/
0 评论
/
205 阅读
/
1985 个字
/
百度已收录
06/30
本文最后更新于2024年06月30日,已超过80天没有更新。如果文章内容或图片资源失效,请留言反馈,我会及时处理,谢谢!

软件的响应速度和稳定性直接影响到用户的满意度和留存率。如果一个应用加载缓慢或频繁卡顿,用户可能会选择卸载并转向竞争对手的产品。良好的性能是保证用户界面流畅、操作响应快速的基础,有助于提升用户粘性和正面评价。

本文不是教条式的指导,比如优化索引、重构代码等等这种形而上学的东西,而是重在动手实践,根据个人在日常开发中遇到的性能问题,通过具体的手段进行优化,比如加索引,到底索引加在哪,或者重构代码,应该在什么位置重构,代码应该怎么写。

一、通过索引优化性能

在数据库中加索引,能够优化查询、更新、删除的性能,但是索引并不是越多越好,这个还是需要根据实际情况的,举个例子,我们如果有如下的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 joininner 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)
赞赏
感谢您的支持,我会继续努力哒!
版权属于:

香草物语

本文链接:

https://www.xiangcaowuyu.net/java/common-backend-performance-optimization-methods.html(转载时请注明本文出处及文章链接)

评论 (0)