前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >mybatis-plus 性能优化:【大数据量mybatis序列化和反序列化慢的问题】

mybatis-plus 性能优化:【大数据量mybatis序列化和反序列化慢的问题】

作者头像
danielxiao
发布2022-08-16 17:43:53
5K1
发布2022-08-16 17:43:53
举报
文章被收录于专栏:小菜鸡的笔记

成果

400M数据,30w条,从80秒干到8秒

技术栈

springboot+mybatis plus +postgresql

抛转引玉

本人在开发多个项目中,都遇到过同样的问题,有些 数据量(超过20w条)条数多的接口,接口特别慢,列举两个我碰到过的问题。

  1. 用spring data mongo 读mongo数据库25w条的时候,query响应只需要100毫秒,但是接口却需要30秒左右(50个字段左右,400m数据)。
  2. 用mybatis plus 查询数据库的时候,query只需要6秒,但是接口响应却需要90秒左右(60个字段左右,400m数据),

这两个问题出现的原因都是类似的,数据库的框架在对象序列化的过程中,花费了大量的时间,以下展示一下具体的解题思路

错误的方式请不要模仿

代码语言:javascript
复制
当你看到这里的时候,请大胆质疑,为什么系统中会设计这样的接口,一个接口需要返回这么多条的数据?
答案:这种大数据量的接口服务,很多情况都是未了兼容老的业务需求(数据同步、数据订阅等)
如果你是在设计新的系统,请认真思考,请选择最正确的技术路线去解决问题,例如消息队列、流计算、cdc等

解题思路

  • 方案1:用stream的方式读数据库。
  • 方案2:用并行读,分段读(多线程)。
  • 方案3:修改mybatis-plus的源码。

### 方案1的问题(数据结构限制了发挥空间)

代码语言:javascript
复制
/**
 * 这个是我们的统一接口返回的结构,所以用流数,没有本质上解决问题,还是要等所有结果响应完毕后,才能返回给客户端
 * @param <T>
 */
public class ResultDTO<T> implements Serializable {
  
    private boolean success;
  
    private T data;
 
    private String message;

    private String code;
}

方案2的问题(直接贴源码)

  1. 没有找到本质问题所在,mybatis是游标读,并行的可能性很低
  2. 开发难度大,无端增加无用的开发工作
代码语言:javascript
复制
  private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
      throws SQLException {
        DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
        ResultSet resultSet = rsw.getResultSet();
        skipRows(resultSet, rowBounds);

        //这里 !resultSet.isClosed() && resultSet.next() 就是mybatis-plus的游标读,一个一个读
      while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
        ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
        Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
        storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
      }
    }

方案3的结果(直接上结果,400M数据,30w条)

方式

mybatis-plus自带的序列化功能

自己手写序列化方式

硬编码hardCode

数据耗时

80秒

8秒

1秒

缺点

大量反射操作,数据量大的时候很慢

自定义字段类型处理可能失败

硬编码,会经常改代码

优点

简单,准确

很快很快

推荐

推荐

推荐

不是很推荐

原理:充分利用mybatis-plus的typeHandle+拒绝反射
代码语言:javascript
复制
    //这个是处理mybatis-plus的序列化的主要类
   package org.apache.ibatis.executor.resultset.DefaultResultSetHandler;

   //关键1:将typeHandler的集合做一个缓存
    private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
        throws SQLException {
        DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
        ResultSet resultSet = rsw.getResultSet();
        skipRows(resultSet, rowBounds);
    
        //这里举个例子,当类为Student类的时候,先把typeHandler缓存,这里不用反射的方式,所以快了很多
        if (resultMap.getType().equals(Student.class)){
        Map<String, Field> logicPropertyField=new HashMap<>();
        Map<String, TypeHandler<?>>logicPropertyTypeHandler=new HashMap<>();


        List<Field> allPropertyField=new ArrayList<>();
        Class<?> temp=resultMap.getType();
        while(temp!=null){
            //获取对象的所有字段
            allPropertyField.addAll(Arrays.asList(temp.getDeclaredFields()));
            //处理对象的继承关系
            temp=temp.getSuperclass();
            temp=null;
        }

        //字段映射关系  数据库返回字段 to 逻辑字段
        List<ResultMapping> resultMappings=resultMap.getResultMappings();
        if(resultMappings!=null&&!resultMappings.isEmpty()){
        for(ResultMapping mappingRule:resultMappings){
        Field fieldTarget=allPropertyField.stream().filter(field->field.getName().equals(mappingRule.getProperty())).findFirst().orElse(null);
        if(fieldTarget!=null){
        //先做string验证
        fieldTarget.setAccessible(true);
            logicPropertyField.put(mappingRule.getColumn(),fieldTarget);
            logicPropertyTypeHandler.put(mappingRule.getColumn(),mappingRule.getTypeHandler());
        }
        }
        }
        /*以下片段是result的demo用法,后续变动时,可以参考如下逻辑*/
        //ResultMapping demo=new ResultMapping();
        //demo.getProperty() //逻辑字段
        //demo.getColumn() //数据库返回的字段
        //demo.getJavaType() //逻辑字段类型
        //demo.getJdbcType() //数据库字段类型,可能为null
        //demo.getTypeHandler() // 该字段的typeHandler

        while(shouldProcessMoreRows(resultContext,rowBounds)&&!resultSet.isClosed()&&resultSet.next()){
            ResultMap discriminatedResultMap=resolveDiscriminatedResultMap(resultSet,resultMap,null);
            Object rowValue=getRowValue(rsw,discriminatedResultMap,null,logicPropertyField,logicPropertyTypeHandler);
            storeObject(resultHandler,resultContext,rowValue,parentMapping,resultSet);
        }
        
        }   
        else{
            while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
            ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
            //这里是原生的序列化方式
            Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
            storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
            }
        }
        
    }


    // 展示一下用typeHandler处理的序列化逻辑
    // GET VALUE FROM ROW FOR SIMPLE RESULT MAP
    //
    private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix,Map<String,Field> logicPropertyField,Map<String,TypeHandler<?>> logicPropertyTypeHandler) throws SQLException {
        Object rowValue=null;
        try{
            rowValue=resultMap.getType().newInstance();
            ResultSet temp=rsw.getResultSet();
            //这里遍历可以有优化空间(遍历算法还可以提升)
            for (String columnName : logicPropertyField.keySet()) {
                Field field=logicPropertyField.get(columnName);
                if (logicPropertyTypeHandler.containsKey(columnName)){
                    field.set(rowValue,logicPropertyTypeHandler.get(columnName).getResult(temp,(columnName)));
                }else{
                    field.set(rowValue,temp.getObject(columnName));
                }
            }
        }catch (Exception e){
            //这里自定义你自己的异常逻辑
        }
        return rowValue;
    }


    //下面这个是硬编码的方式,虽然最快,但是改动起来会很痛苦
    private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
    final ResultLoaderMap lazyLoader = new ResultLoaderMap();
            Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
            if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
             if (resultMap.getType().equals(Student.class)){
                //这个是硬编码的方式,通过getter setter直接赋值  
                ResultSet temp=rsw.getResultSet();
                Student dbItem=new Student();
                dbItem.setName(temp.getString("name"));
                rowValue=dbItem;
              }else{
                final MetaObject metaObject = configuration.newMetaObject(rowValue);
                boolean foundValues = this.useConstructorMappings;
                if (shouldApplyAutomaticMappings(resultMap, false)) {
                foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
                }
                foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
                foundValues = lazyLoader.size() > 0 || foundValues;
                rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
             }
        }
        return rowValue;
    }

有没有更好的方式

这里提供一个破题思路

  1. 充分利用mybatis-plus的上下文,如果不满足,自己写一个上下文,结合IPage分页查询使用
  2. 分页操作是才query操作前的,所以可以先得知这次查询会返回多少条数据,根据返回数据的条数动态去选择序列化方式
  3. 如果结果条数大于5000,用typeHandler缓存的方式序列化对象,否则用mybatis-plus自带的反射机制进行序列化(动态选择)
  4. 看代码思路
代码语言:javascript
复制
 //这个是mybatis-plus的分页插件源码
  public boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) thr
         ows SQLException {
    IPage<?> page = (IPage)ParameterUtils.findPage(parameter).orElse(null);
    if (page != null && page.getSize() >= -1L && page.searchCount()) {
      MappedStatement countMs = this.buildCountMappedStatement(ms, page.countId());
      BoundSql countSql;
      if (countMs != null) {
        countSql = countMs.getBoundSql(parameter);
      } else {
        countMs = this.buildAutoCountMappedStatement(ms);
        String countSqlStr = this.autoCountSql(page, boundSql.getSql());
        MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);
        countSql = new BoundSql(countMs.getConfiguration(), countSqlStr, mpBoundSql.parameterMappings(), parameter);
        PluginUtils.setAdditionalParameter(countSql, mpBoundSql.additionalParameters());
      }

      CacheKey cacheKey = executor.createCacheKey(countMs, parameter, rowBounds, countSql);
      List<Object> result = executor.query(countMs, parameter, rowBounds, resultHandler, cacheKey, countSql);
      long total = 0L;
      if (CollectionUtils.isNotEmpty(result)) {
        Object o = result.get(0);
        if (o != null) {
          total = Long.parseLong(o.toString());
        }
      }
      //这里已经获取到数据总数了,在这里根据你的需求把序列化方式放在上下文中即可
      page.setTotal(total);
      
      //往下才会执行真正的query 语句,所以这种方式更优雅
      return this.continuePage(page);
    } else {
      return true;
    }
  }
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-08-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 成果
  • 技术栈
  • 抛转引玉
  • 错误的方式请不要模仿
  • 解题思路
    • 方案2的问题(直接贴源码)
      • 方案3的结果(直接上结果,400M数据,30w条)
        • 原理:充分利用mybatis-plus的typeHandle+拒绝反射
    • 有没有更好的方式
    相关产品与服务
    文件存储
    文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档