Java 开发过程影响系统的坑

简介

现场CPU/内存飙高,服务器卡死宕机,你是否慌得一逼?如果是,下面的要点或许能帮你提前跳过一些坑。

填坑列表

数据库连接资源未释放

  1. 影响指数: 5颗星
  2. 问题描述:
# DataSource.getConnection()/Statement/PreparedStatement/ResultSet使用后未close。

  public Object getUser(String id) throws SQLException {
    Connection connection=dataSource.getConnection();
    PreparedStatement statement=connection.prepareStatement("select *from user where code = ?");
    statement.setString(1,id);
    ResultSet resultSet=statement.executeQuery();
    if(resultSet.next()){
      ...
      resultSet.getObject(1);
      ...
    }else{
      return null;
    }
  }
  1. 血案描述:
    • 常见于系统运行一段时间后,突然卡死,卡在某个Sql的执行上,一般重启系统后能够继续正常运行一段时间。
    • 通过线程dump定位,一般是卡在获取数据库连接。A系统连接数太多没释放,B系统也连了同一个数据库,A系统已卡死,由于占用大量数据库连接资源,导致B系统也跟着卡死。
    • 如果用的Druid线程池,有提供对应的监控界面,通过查看对应数据源的打开和关闭的物理连接数是否一致就可盘点是否是连接资源未释放。
  2. 分析说明:
    • connection.close 自动关闭 Statement.close 自动导致 ResultSet 对象无效(注意只是 ResultSet 对象无效,ResultSet 所占用的资源可能还没有释放)。所以还是应该显式执行connection、Statement、ResultSet的close方法。
    • 特别是在使用connection pool的时候,connection.close 并不会导致物理连接的关闭,不执行ResultSet的close可能会导致更多的资源泄露。
  3. 解决说明:
public Object getUser(String id) throws SQLException {
    Connection connection = null;
    PreparedStatement statement = null;
    ResultSet resultSet = null;
    try {
      connection = dataSource.getConnection();
      statement = connection.prepareStatement("select *from user where code = ?");
      statement.setString(1, id);
      resultSet = statement.executeQuery();
      if (resultSet.next()) {
      ......
        resultSet.getObject(1);
      ......
      } else {
        return null;
      }
    } finally {
      if (resultSet != null) {
        resultSet.close(); //关闭 ResultSet
      }
      if (statement != null) {
        statement.close(); //关闭 PreparedStatement
      }
      if (connection != null) {
        connection.close(); //关闭 Connection
      }
    }
  }

大事务写库

  1. 影响指数: 5颗星
  2. 问题描述:
    一次性修改或者保存上万条数据,随着数据量的加大,执行时间越来越长。
  3. 血案描述:
    • 客户反馈该操作很慢,要等很久;甚至导致其它用户操作其它功能模块时出现卡顿。
    • 涉及该表写操作的其它功能卡死。
    • 现场工程反馈,数据库服务器的内存占用升高。
  4. 分析说明:
    • 一个事务内执行大数据的insert和update后的commit会占用大量的数据库服务器资源。
    • 常见于批量保存、批量修改XX数据;一次性审批N张单据数据;批量生成XX数据之类的功能。
  5. 解决说明:
    • 拆分大事务为几个小事务,一次性只提交处理合适的数据行数,比如500行执行一次commit操作。

表未建索引

  1. 影响指数: 3颗星
  2. 问题描述:
    新创建的表未指定索引,或者旧表索引没建对。
  3. 血案描述:
    • 用到该表的业务功能块,一开始数据量不大时(1000以内),操作很流畅。一段时间后,随着数量的增加(但数据量还未超过10W),操作出现卡顿,和该表无关的其它功能模块操作正常。
  4. 分析说明:
    由于其它功能模块操作正常,就当前功能块有卡顿,因此首先排除应用系统整体出问题,通过查看日志定位具体的服务调用链路,跟踪过程中的执行语句,
    发现对应表除了一个主键无其它索引。
  5. 解决说明:
    • Oracle 执行语句可以通过PLSQL中按F5调出分析计划窗口,通过定位语句中TABLE ACCESSFULL的表找到未建索引或索引未建对或者索引未生效的地方,
      再针对性进行加索引或者调整执行语句。

in 语句参数超过1000

  1. 影响指数: 5颗星
  2. 问题描述:
ORA-01795: maximum number of expressions in a list is 1000
ORA-01795:列表中的最大表达式数为 1000
  1. 血案描述:
    • 客户一开始上线时,数据量较小,使用正常,一段时间后,数据量超过1000,在执行某些涉及查询语句功能时报错,导致无法继续往下走流程,
      一般现场临时通过加过滤条件缩减数据可以勉强使用。
    • 常见于某些资料数据的查询方法,尤其用于排除某些数据的条件限制。
  2. 分析说明:
    由于in(含not in)条件的列表的最大上限为1000,实际编写查询语句过程没有考虑到,因此导致以上报错。
  3. 解决说明:
 如果是JDBC,将in语句列表按一定数量分组,比如500个为一组,拼出  and (field in (1,...,500) or field in (501,...,1000) or ...)。

一次性查出所有数据

  1. 影响指数: 5颗星
  2. 问题描述:
    应用日志常见java.lang.OutOfMemoryError: PermGen space。
  3. 血案描述:
    • 随着项目现场数据量的增加,涉及到该表查询的功能块,越用越慢,应用服务器的内存占用越来越高(内存快满时CPU也跟着飙高),如果并发操作的客户端用户再多一些,很容易就导致应用服务器崩溃。
    • 常见于“查看所有XX数据”、“全部加载”、“导入”、“导出全部数据”等功能。
  4. 分析说明:
    取全表数据时,要分析下未来该表的数据量有多少,同时考虑并发访问取该表的量有多少,是否真的适合做全表查询,如无法评判请请教他人。
  5. 解决说明:
    • 使用分页查询,限制一次最多只取N行数据,比如100,如果是加工类作业请先查询一个分页再加工然后再接着遍历下一个分页。
    • 如果实在需要全量数据(且数据量不大,是并发大导致内存溢出),请走内存缓存(Redis、MemoryCache)。

某数据接口查询统一调用分页查询方法

  1. 影响指数: 2颗星
  2. 问题描述:
    给前端或者给第三方的数据查询方法(get/list/query)统一通过包装转换,直接调用后端实现的分页查询query方法来实现,客户反应接口的QPS不达标,查询性能低或者界面数据加载慢。
  3. 血案描述:
    • 服务接口中的get/list方法,性能差,QPS不达标。
    • 如果某张表的数据量很庞大,比如千万级别,执行select count语句时要耗费很长时间,甚至导致数据库连接查询的阻塞。
  4. 分析说明:
    • 分页查询会执行两条SQL语句,一条为select count...另一条为实际的数据查询,get和list方法根本无需执行select count语句。
    • 一般的分页查询会关联fetch其它表数据,get和list方法大部分情况下只会取出查询表自身的全部或者部分字段,直接调用分页查询query方法实现的方式很影响查询性能。
    • 有人说:"我这是为了代码复用!"。如果你的项目接受这样的查询性能的话那请忽略本点,个中取舍,具体项目具体分析。
  5. 解决说明:
    • get/list方法单独实现,不走分页查询方法,尽量按需求方需要的查询字段最小集去实现。

输入输出流未关闭

  1. 影响指数: 5颗星
  2. 问题描述:
    • 应用部署到linux中,运行一段时间后报to many open files的错误。
    • 对应文件读写操作相关的功能块卡住。
    # 问题代码示例1:
    InputStream src = getClass().getResourceAsStream("/dsRouter.json");
    ...//此处为业务代码,输入流src对象使用完后并未关闭。
    
    # 问题代码示例2:
     FileOutputStream fos = new FileOutputStream(serverFile);
     fos.write(datas);
     fos.flush();
    ...没有调用close()方法
    
    
  3. 血案描述:
    • 用户无法进行下一步操作,比如导入文件、导出文件、获取某某文件中的配置信息。
  4. 分析说明:
    • 如果是linux系统通过执行命令"lsof |wc -l",查看当前系统打开的文件数量,可以快速定位到哪个文件的输入/输出流资源未释放导致。
    • 如果确定文件输入和输出流有关闭,那么另一个可能原因是接口的并发访问量过大。
  5. 解决说明:
    • 检查代码中文件读写位置的输入和输出流是否都关闭了,尤其是异常情况下是否有考虑到。
    • 如果是并发访问太大,请控制并发量,比如超过一定请求数则拒绝请求、或者队列等待等方式;或者如果服务器性能允许的情况下,适当加大服务器允许打开的文件数量。

代码中的死循环

  1. 影响指数: 5颗星
  2. 问题描述:
# 常见于后台加工类作业,死循环加工数据。
 while (true) {
  List result = getDataProcessDao().listExecuteRecords(tenantId, null);//如果数据量很大,list所有数据还有内存溢出的风险
  for (JobDataProcess record : result) {
   ...... //倘若这里再发起一个新线程进行异步加工,甚至导致开发电脑宕机/蓝屏。
  }
 }
  1. 血案描述:
    • 应用新部署上线,数据量不大,访问的用户量也不多,单核的cpu直接飙到100%;数据库日志显示某SQL查询语句每秒内执行下次数异常多,数据库服务器的资源占用也跟着加大。
    • 如果开发本地开启了这个加工作业,调试过程能感觉到卡顿,同时IDE控制台产生了大量的SQL查询语句;
      如果加工作业内部单挑数据加工走异步线程的形式,还有可能导致电脑宕机/蓝屏等。
  2. 分析说明:
    • while(true)长期占用CPU资源,大部分情况下加工表中并没有数据,频繁不间断的select影响应用服务器和数据库服务器的性能。
  3. 解决说明:
    • 当没查出数据或者一次while逻辑后,可以使用sleep休眠一段时间,释放CPU资源。
     while (true) {
      List result = getDataProcessDao().listExecuteRecords(tenantId, null);//如果数据量很大,list所有数据还有内存溢出的风险
      for (JobDataProcess record : result) {
       ...... //倘若这里再发起一个新线程进行异步加工,甚至导致开发电脑宕机/蓝屏。
      }
    
       try {
              Thread.sleep(500); //休眠一段时间,时间可以开放出配置来
           } catch (InterruptedException e) {
           }
     }
    

客户端超时(504 Gateway Time-out)

  1. 影响指数: 5颗星
  2. 问题描述:
    客户端界面报504 Gateway Time-out。
  3. 血案描述:
    • 用户操作时界面报错提示超时异常,一段时间后发现要处理的数据已经走到下一个状态了或者处理完成了,用户表示很诧异。
    • 用户操作时界面报错提示超时异常,用户以为是特殊情况,另外重复做了提交/保存等动作,导致出现重复的数据等;然后客户跑来问工程“为什么界面报错了还会有数据生成?为什么出现单号不同内容一样的重复数据?”
  4. 分析说明:
    • 实际情况是客户端连接超时了,服务端代码还在运行,因此容易出现上述的场景。
    • 多见于批量操作(批量审批、删除、保存)、导入、导出操作等。
    • 又见于部分执行时间耗时较久的功能块,比如执行时间超过30秒或者1分钟之类的特殊时长。
  5. 解决说明:
    • 治标不治本:超时错误常见于用了反向代理服务器比如nginx之后,由于nginx的超时时间没配或者配置不合理导致,加大对应的超时配置时间即可。
    • 优化代码功能,加快执行时间,或者使用异步执行的方式。

读写大数据文件(Excel、Csv)

  1. 影响指数: 5颗星
  2. 问题描述:
    读写大数据文件时,服务器的内存和CPU同时飙高。
  3. 血案描述:
    • 进行大数据导入和导出文件时,服务器开始卡死甚至出现宕机的情况。
    • 同时CPU和内存双重飙高。
  4. 分析说明:
    通过使用jvisualvm监控工具查看CPU实时动态,发现在堆内存骤然上升后,发生JVM垃圾回收的时候CPU陡然上升,稍后会稍微下降,一会儿又进行垃圾回收,导致CPU再次飙高。基本锁定在堆内存高使用量且频繁GC的原因导致。通过查应用日志或者问询客户做了哪些操作,基本锁定在大文件导入和大数据导出文件两个功能上面。
  5. 解决说明:
    • 大文件读写优化,抛弃面向于字节传输的传统 IO 方式,使用FileChannel进行大数据文件的读写:
    # CSV 文件读写
    FileChannel fileChannel = new RandomAccessFile(new File("db.data"), "rw").getChannel();
    // 写
    byte[] data = new byte[4096];
    long position = 1024L;
    //指定 position 写入 4kb 的数据
    fileChannel.write(ByteBuffer.wrap(data), position);
    //从当前文件指针的位置写入 4kb 的数据
    fileChannel.write(ByteBuffer.wrap(data));
    
    // 读
    ByteBuffer buffer = ByteBuffer.allocate(4096);
    long position = 1024L;
    //指定 position 读取 4kb 的数据
    fileChannel.read(buffer,position);
    //从当前文件指针的位置读取 4kb 的数据
    fileChannel.read(buffer);
    
    • Excel大数据文件的读写(参考连接>>)
    # 写Excel文件
    SXSSFWorkbook wb = null;
    try {
          wb = new SXSSFWorkbook(1000); //参数表示内存中保留的写行数,超过这个范围的会自动刷到硬盘中。
          ...
    
       //这种写法会在大括号中的代码执行完毕后,自动调用类实例的close()方法。
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();) {
          wb.write(outputStream);
          outputStream.flush();
        }
    }finally{
       if(wb!=null){
          wb.close();
       }
    }
    
    # 读Excel文件
    //由于比较繁琐,此处不提供代码示例,参照上方提供的参考连接。
    

你可能感兴趣的:(Java 开发过程影响系统的坑)