🚀性能直接提升154倍?那些你可能知道但是不知道能提升多少性能的优化技巧

364 阅读6分钟

简介

image-20230804133604357

盘点那些工作中你可能知道,但是你不知道性能会提升多少的优化手段和技巧。

一、内存操作优化

测试平台

Java:zulu_jdk java_version 20.0.2

CPU:Apple M2 Pro (10+16)

Memory:32GB

OS:MacOS Ventura 13.3.1

Stream流使用优化

有时候我们可能需要从一个list中过滤出一部分或者一条数据,进行拼装数据,就像下边这样。

 public static void main(String[] args) {
      // 先查出用户列表
      List<User> userList = findUserList();
      // 查出用户的其它信息
      List<User> userAvatar = findUserAvatar();
 ​
      long start = System.currentTimeMillis();
      // 拼装数据
      for (User user : userList) {
          Long userId = user.getUserId();
          // 根据用户id筛选出对应的用户头像
          String avatar = userAvatar.stream().filter(t -> t.getUserId().equals(userId)).findFirst().orElse(new User(null, null, null)).getUserAvatar();
          user.setUserAvatar(avatar);
      }
 ​
      System.out.println("耗时:" + (System.currentTimeMillis() - start) + "ms");
  }

如果直接在每次循环内进行stream.filter()的话,运行结果如下:

输出结果:耗时60ms

stream.filter()看似是内存操作,但是扛不住次数多呀,这种场景是非常常见的,而且我们很有可能会拼装不止一个属性,可能用户头像、关注数量,关注者数量,等信息都需要进行拼装,那么一次stream.filter()60ms,那2次就是120ms,拼装的多了,性能损耗就很大了。

怎么优化呢,方案如下:

 public static void highPerformanceNormal(List<User> userList, List<User> userAvatar) {
      // 将数据转换成map,利用红黑树实现高性能查询
      Map<Long, List<User>> userAvatarMap = userAvatar.stream().collect(Collectors.groupingBy(User::getUserId));
      // 拼装数据
      for (User user : userList) {
          Long userId = user.getUserId();
          // 根据用户id筛选出对应的用户头像
          // opt与性能优化无关,只是防止空指针
          Optional.ofNullable(userAvatarMap.getOrDefault(userId, null)).ifPresent(
                  avatarList -> user.setUserAvatar(avatarList.get(0).getUserAvatar())
          );
      }
  }

优化完成后的运行结果如下:性能直接提升10倍

性能对比

普通封装耗时(stream.filter()):55ms

高性能封装耗时(map.get()):5ms

性能提升约:🚀90%


copyPropertis优化

常用与对象的属性拷贝,原理是利用反射进行赋值,性能低下。代码如下:

 // 将用户转换成userDTO,返回给前端
 List<UserDTO> userDtoList = new ArrayList<>(userList.size());
 long start = System.currentTimeMillis();
 for (User user : userList) {
    UserDTO userDTO = new UserDTO();
    // 拷贝属性
    BeanUtils.copyProperties(user, userDTO);
    userDtoList.add(userDTO);
 }
 System.out.println("普通封装耗时:" + (System.currentTimeMillis() - start) + "ms");

输出结果:117ms

优化思路:使用MapStruct或者手动setter(),其实是一样的。为了方便展示和理解,就手动调用setter代替MapStruct

 /**
   * 高性能封装
   */
  public static void highPerformanceNormal(List<User> userList, List<UserDTO> userDtoList) {
      for (User user : userList) {
          UserDTO userDTO = new UserDTO();
          userDTO.setUserAvatar(user.getUserAvatar());
          userDTO.setUserId(user.getUserId());
          userDTO.setUserName(user.getUserName());
          userDtoList.add(userDTO);
      }
  }

性能对比:

普通封装(BeanUtil.copyProperties()):耗时:154ms

高性能封装(MapStruct、手动setter):耗时:1ms

性能提升:100% or 🚀154倍


反射优化

假如我们需要把一个Object对象转换成Map,通常要使用到反射,例如:

 public static void main(String[] args) throws Exception {
      // 查询用户,5000条
      List<StreamOptimization.User> userList = StreamOptimization.findUserList();
      List<Map<String, Object>> resultMapList = new ArrayList<>();
 ​
      long start = System.currentTimeMillis();
      for (StreamOptimization.User user : userList) {
          // 使用hutool的工具简化
          resultMapList.add(BeanUtil.beanToMap(user));
      }
      System.out.println("反射赋值耗时:" + (System.currentTimeMillis() - start) + "ms");
  }

输出结果:反射耗时:63ms

如果我们把需要转成Map的对象字段提前缓存一下,在进行转换呢?

 private static final Map<Field, Method> USER_FIELD_METHOD_MAP;
 ​
  // 提前将字段和对应的get方法缓存好
  static {
      USER_FIELD_METHOD_MAP = new LinkedHashMap<>();
      Field[] fields = StreamOptimization.User.class.getDeclaredFields();
      for (Field field : fields) {
          String fieldName = field.getName();
          String methodName = "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
          try {
              Method method = StreamOptimization.User.class.getMethod(methodName);
              USER_FIELD_METHOD_MAP.put(field, method);
          } catch (NoSuchMethodException e) {
              e.printStackTrace();
          }
      }
  }
 ​
 /**
   * 高性能封装
   */
  public static void highPerformanceNormal(List<StreamOptimization.User> userList, List<Map<String, Object>> resultMapList) throws Exception {
      for (StreamOptimization.User user : userList) {
          Map<String, Object> resultMap = new LinkedHashMap<>();
         // 直接读取缓存好的map,不需要再次反射获取,直接调用即可
          for (Map.Entry<Field, Method> entry : USER_FIELD_METHOD_MAP.entrySet()) {
              resultMap.put(entry.getKey().getName(), entry.getValue().invoke(user));
          }
          resultMapList.add(resultMap);
      }
  }

性能对比:

普通封装(BeanUtil.beanToMap()):耗时:76ms

高性能封装(MapStruct、手动setter):耗时:10ms

性能提升:🚀7.6倍


多流程操作优化

假如我们要在一个查询里进行3个操作,查询商品列表、查询商品审核信息、查询商品销量信息。操作的顺序是,先查询列表,剩下两个顺序都可以。

假如查询商品列表要100ms,查询审核信息要88ms,查询销量信息要105ms

传统查询方式如下:

 package cn.yufire.code.optimization;
 ​
 import java.util.concurrent.TimeUnit;
 ​
 /**
  * @author Yufire
  * @date 2023/8/4 11:55
  * -
  */
 public class MultiOperationOptimization {
 ​
     public static void main(String[] args) {
         long start = System.currentTimeMillis();
         try {
             queryGoodsList();
             queryGoodsCheckInfo();
             queryGoodsSaleInfo();
 ​
             // 组装数据...
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.println("总耗时:" + (System.currentTimeMillis() - start) + "ms");
     }
 ​
     public static void queryGoodsList() throws InterruptedException {
         TimeUnit.MILLISECONDS.sleep(100);
         System.out.println("商品列表查询完成");
     }
 ​
     public static void queryGoodsCheckInfo() throws InterruptedException {
         TimeUnit.MILLISECONDS.sleep(88);
         System.out.println("商品审核详情查询完成");
     }
 ​
     public static void queryGoodsSaleInfo() throws InterruptedException {
         TimeUnit.MILLISECONDS.sleep(105);
         System.out.println("商品销量详情查询完成");
     }
 }

结果:

商品列表查询完成 商品审核详情查询完成 商品销量详情查询完成 总耗时:305ms

如果使用线程池或者CompletableFuture进行优化呢?

两者效率都一致,为了方便使用线程池进行演示。

 public static void main(String[] args) throws Exception {
      long start = System.currentTimeMillis();
      // 建议自定义线程池,这里只是为了方便演示
      ExecutorService executorService = Executors.newFixedThreadPool(2);
 ​
      // 查询商品
      queryGoodsList();
      // 查询审核详情
      Future<?> goodsCheckFuture = executorService.submit(MultiOperationOptimization::queryGoodsCheckInfo);
      // 查询销量详情
      Future<?> goodsSaleFuture = executorService.submit(MultiOperationOptimization::queryGoodsSaleInfo);
 ​
      goodsCheckFuture.get();
      goodsSaleFuture.get();
      // 组装数据..
      // 返回前端
      System.out.println("总耗时:" + (System.currentTimeMillis() - start) + "ms");
  }

性能对比:

普通串行执行:耗时:305ms

线程池并行执行:耗时:217ms

性能提升约:🚀40%


二、SQL优化

场景一

Excel数据导出,需要重复调用某个查询接口,直到没有数据。

流程大概如下:

 long total = goodsService.queryTotal();
 // 一次查询5000条,计算一共几页
 int totalPage = total / 5000
 // 循环
 for(int i = 1;i <= totalPage; i++){
    // 查询数据,i是第几页,5000是一次查询5000条
    List<Goods> list = goodsService.queryList(i,5000);
    //excel写入操作...
    // excel.write()
 }

生成的sql可能是长这样的

 select field1,field2,fieldn... from goods
 where 条件...
 limit 5000 # 第一次分页
 limit 5000,5000 # 第二次分页
 limit 10000,5000 # 第三次分页
 limit pageIndex * queryTotal,5000 # 第n次分页

可以看到,随着页数的增加,分页深度依次递增queryTotal(一次查询多少条),而且你去EXPLAIN这个sql,会发现根本不会走索引。

那怎么样才能让分页走索引呢?

  1. 可以将sql进行改造,添加条件和排序
 select field1,field2,fieldn... from goods
 # 注意offsetId做好非空判断,<if test="offsetId!=null"> ....
 where 条件... and id > #{offsetId}
 order by id
  1. 将导出的for循环改造,添加offsetId参数,查询的时候传入。
 long lastId = 0;
 for(int i = 1;i <= totalPage; i++){
    // 查询数据,lastId是最后一条数据的id,i是第几页,5000是一次查询5000条
    List<Goods> list = goodsService.queryList(lastId,i,5000);
    // 获取最后一个id
    lastId = list.get(list.size()-1).getId();
    //excel写入操作...
    // excel.write()
 }
  1. 此时你会发现EXPLAIN这条sql的时候,它会走range级别的索引,效率提升。并且越往后索引过滤的数据越多,效率越高!
  2. 导出的效率会明显变高

剩下技巧,等待更新中...

原创作者:Yufire