mybatisPlus バッチ挿入パフォーマンスの最適化

背景: Internet of Things プラットフォームの背景、センサーの取得頻度が 1000Hz に達し、100 以上のテーブルが分割されましたが、mysql はまだ揚げられていません。現在、1 つのテーブルのデータ量は約 1,000 で、非同期バッチ挿入のために Kafka からデータを取得しています. 挿入されるデータの量は、毎回 1,500 です. テスト中に問題はありません. その結果、 Kafka サーバーがオンラインになった直後にハングアップする. サーバーには数十ギガバイトのデータが蓄積されており、運用環境のログを調べたところ、最後の単一のバッチ挿入時間が 10 秒以上、または 20 秒以上に固定されていることがわかりました数秒、そして Kafka はコンシューマーをコンシューマー グループから直接追い出しました...そのため、Kafka メッセージは継続されました 消費がなければ、総重量により Kafka データが蓄積され、ハングアップしました...

このような状況では、採用されたソリューションはサブデータベースとサブテーブルにすぎず、単一のテーブルのデータ量を減らし、データベースへの圧力を軽減し、バッチ挿入の効率を改善し、消費者の消費速度を向上させます。
この記事では、主にバッチ挿入の効率を向上させる方法に焦点を当てています。


mybatisplus のバッチ挿入メソッド: saveBatch() を使用しました。オンラインで見たことがありますが、 rewriteBatchedStatements=trueパラメータを jdbc URL パスに追加すると、mysql の最下層で実際のバッチ挿入モードを有効にできます。

ドライバー バージョン 5.1.13 以降が高性能のバッチ挿入を実現できることを確認します。デフォルトでは、MySQL JDBC ドライバーは executeBatch() ステートメントを無視し、バッチで実行されると予想される一連の SQL ステートメントを分割し、それらを 1 つずつ MySQL データベースに送信します。パフォーマンスが低下します。rewriteBatchedStatements パラメーターが true に設定されている場合にのみ、ドライバーはバッチで SQL を実行します。また、このオプションは INSERT/UPDATE/DELETE に対して有効です。

ただ、以前追加したことがあり、現在データテーブルのインデックスが作成されていないため、1000 to w のデータ量で 1500 バッチ挿入しても 20 秒を消費できないため、矛盾を saveBatch メソッドに渡します。使用バージョン: V3.4.3.4
ソースコードを表示:

   public boolean saveBatch(Collection<T> entityList, int batchSize) {
    
    
        String sqlStatement = this.getSqlStatement(SqlMethod.INSERT_ONE);
        return this.executeBatch(entityList, batchSize, (sqlSession, entity) -> {
    
    
            sqlSession.insert(sqlStatement, entity);
        });
    }
protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
    
    
        return SqlHelper.executeBatch(this.entityClass, this.log, list, batchSize, consumer);
    }
    public static <E> boolean executeBatch(Class<?> entityClass, Log log, Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
    
    
        Assert.isFalse(batchSize < 1, "batchSize must not be less than one", new Object[0]);
        return !CollectionUtils.isEmpty(list) && executeBatch(entityClass, log, (sqlSession) -> {
    
    
            int size = list.size();
            int i = 1;

            for(Iterator var6 = list.iterator(); var6.hasNext(); ++i) {
    
    
                E element = var6.next();
                consumer.accept(sqlSession, element);
                if (i % batchSize == 0 || i == size) {
    
    
                    sqlSession.flushStatements();
                }
            }

        });
    }

最後に、executeBatch() メソッドに到達しました。これは明らかに 1 つずつループに挿入され、sqlSession.flushStatements() を介して単一の挿入の挿入ステートメントをバッチで送信することがわかります。これは同じ sqlSession です。比較されます コレクション ループ挿入をトラバースするための特定のパフォーマンスの向上がありますが、これは SQL レベルでの実際のバッチ挿入ではありません。

関連ドキュメントを調べたところ、mybatisPlus が sql インジェクターを提供し、ビジネスの実際の開発ニーズを満たすようにメソッドをカスタマイズできることがわかりました。
SQL インジェクションの公式の例 オルガン ネットワーク
sql インジェクター
mybtisPlus のコア パッケージで提供されるデフォルトの注入可能なメソッドは次のとおりです。
ここに画像の説明を挿入
拡張パッケージの下で、mybatisPlus はスケーラブルな注入可能なメソッドも提供します。
ここに画像の説明を挿入
AlwaysUpdateSomeColumnById: Id に従って各フィールドを更新します。 null フィールドを無視せず、mybatis-plus の updateById がデフォルトでエンティティの null 値フィールドを自動的に無視して更新しないという問題を解決; InsertBatchSomeColumn: 実際のバッチ挿入、単一の SQL 挿入ステートメントによるバッチ挿入を
実現;
Upsert: update または insert 。一意の制約に従って update または delete を実行するかどうかを判断します。これは、重複キー更新での挿入のサポートを提供することと同じです。

mybatisPlus がすでに InsertBatchSomeColumn のメソッドを提供していることがわかります。このメソッドを sql インジェクターに追加するだけです。

    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
    
    
        KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
        SqlMethod sqlMethod = SqlMethod.INSERT_ONE;
        List<TableFieldInfo> fieldList = tableInfo.getFieldList();
        String insertSqlColumn = tableInfo.getKeyInsertSqlColumn(true, false) + this.filterTableFieldInfo(fieldList, this.predicate, TableFieldInfo::getInsertSqlColumn, "");
        //------------------------------------拼接批量插入语句----------------------------------------
        String columnScript = "(" + insertSqlColumn.substring(0, insertSqlColumn.length() - 1) + ")";
        String insertSqlProperty = tableInfo.getKeyInsertSqlProperty(true, "et.", false) + this.filterTableFieldInfo(fieldList, this.predicate, (i) -> {
    
    
            return i.getInsertSqlProperty("et.");
        }, "");
        insertSqlProperty = "(" + insertSqlProperty.substring(0, insertSqlProperty.length() - 1) + ")";
        String valuesScript = SqlScriptUtils.convertForeach(insertSqlProperty, "list", (String)null, "et", ",");
        //------------------------------------------------------------------------------------------
        String keyProperty = null;
        String keyColumn = null;
        if (tableInfo.havePK()) {
    
    
            if (tableInfo.getIdType() == IdType.AUTO) {
    
    
                keyGenerator = Jdbc3KeyGenerator.INSTANCE;
                keyProperty = tableInfo.getKeyProperty();
                keyColumn = tableInfo.getKeyColumn();
            } else if (null != tableInfo.getKeySequence()) {
    
    
                keyGenerator = TableInfoHelper.genKeyGenerator(this.getMethod(sqlMethod), tableInfo, this.builderAssistant);
                keyProperty = tableInfo.getKeyProperty();
                keyColumn = tableInfo.getKeyColumn();
            }
        }

        String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript);
        SqlSource sqlSource = this.languageDriver.createSqlSource(this.configuration, sql, modelClass);
        return this.addInsertMappedStatement(mapperClass, modelClass, this.getMethod(sqlMethod), sqlSource, (KeyGenerator)keyGenerator, keyProperty, keyColumn);
    }

次に、実際のバッチ挿入が SQL インジェクターによって実現されます

デフォルトの SQL インジェクター

public class DefaultSqlInjector extends AbstractSqlInjector {
    
    
    public DefaultSqlInjector() {
    
    
    }

    public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
    
    
        if (tableInfo.havePK()) {
    
    
            return (List)Stream.of(new Insert(), new Delete(), new DeleteByMap(), new DeleteById(), new DeleteBatchByIds(), new Update(), new UpdateById(), new SelectById(), new SelectBatchByIds(), new SelectByMap(), new SelectCount(), new SelectMaps(), new SelectMapsPage(), new SelectObjs(), new SelectList(), new SelectPage()).collect(Collectors.toList());
        } else {
    
    
            this.logger.warn(String.format("%s ,Not found @TableId annotation, Cannot use Mybatis-Plus 'xxById' Method.", tableInfo.getEntityType()));
            return (List)Stream.of(new Insert(), new Delete(), new DeleteByMap(), new Update(), new SelectByMap(), new SelectCount(), new SelectMaps(), new SelectMapsPage(), new SelectObjs(), new SelectList(), new SelectPage()).collect(Collectors.toList());
        }
    }
}

DefaultSqlInjector カスタム SQL インジェクターを継承

/**
 * @author zhmsky
 * @date 2022/8/15 15:13
 */
public class MySqlInjector extends DefaultSqlInjector {
    
    

    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
    
    
        List<AbstractMethod> methodList = super.getMethodList(mapperClass);
        //更新时自动填充的字段,不用插入值
        methodList.add(new InsertBatchSomeColumn(i -> i.getFieldFill() != FieldFill.UPDATE));
        return methodList;
    }
}

カスタム SQL インジェクターを Mybatis コンテナーに挿入する

/**
 * @author zhmsky
 * @date 2022/8/15 15:15
 */
@Configuration
public class MybatisPlusConfig {
    
    

    @Bean
    public MySqlInjector sqlInjector() {
    
    
        return new MySqlInjector();
    }
}

BaseMapper を継承してカスタム メソッドを追加する

/**
 * @author zhmsky
 * @date 2022/8/15 15:17
 */
public interface CommonMapper<T> extends BaseMapper<T> {
    
    
    /**
     * 真正的批量插入
     * @param entityList
     * @return
     */
    int insertBatchSomeColumn(List<T> entityList);
}

対応するマッパー レイヤー インターフェイスは、上記のカスタム マッパーを継承します。

 * @author zhmsky
 * @since 2021-12-01
 */
@Mapper
public interface UserMapper extends CommonMapper<User> {
    
    

}

最後に、UserMapper の insertBatchSomeColumn() メソッドを直接呼び出して、実際のバッチ挿入を実現します。

    @Test
    void contextLoads() {
    
    

        for (int i = 0; i < 5; i++) {
    
    
            User user = new User();
            user.setAge(10);
            user.setUsername("zhmsky");
            user.setEmail("[email protected]");
            userList.add(user);
        }
        long l = System.currentTimeMillis();
        userMapper.insertBatchSomeColumn(userList);
        long l1 = System.currentTimeMillis();
        System.out.println("-------------------:"+(l1-l));
        userList.clear();
    }

ログ出力情報を確認し、実行された sql ステートメントを観察して、
ここに画像の説明を挿入
これが sql レベルでの実際のバッチ挿入であることを確認します。
mybatisPlus が公式に提供している insertBatchSomeColumn メソッドは、バッチ挿入、つまり一度に何個直接挿入されるかをサポートしていません。 mysql であるため、saveBatch に似たバッチ バッチ挿入メソッドも実装する必要があります。

一括挿入を追加

元の saveBatch メソッドを模倣します。

 * @author zhmsky
 * @since 2021-12-01
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    
    

    @Override
    @Transactional(rollbackFor = {
    
    Exception.class})
    public boolean saveBatch(Collection<User> entityList, int batchSize) {
    
    
        try {
    
    
            int size = entityList.size();
            int idxLimit = Math.min(batchSize, size);
            int i = 1;
            //保存单批提交的数据集合
            List<User> oneBatchList = new ArrayList<>();
            for (Iterator<User> var7 = entityList.iterator(); var7.hasNext(); ++i) {
    
    
                User element = var7.next();
                oneBatchList.add(element);
                if (i == idxLimit) {
    
    
                    baseMapper.insertBatchSomeColumn(oneBatchList);
                    //每次提交后需要清空集合数据
                    oneBatchList.clear();
                    idxLimit = Math.min(idxLimit + batchSize, size);
                }
            }
        } catch (Exception e) {
    
    
            log.error("saveBatch fail", e);
            return false;
        }
        return true;
    }
}

テスト:

    @Test
    void contextLoads() {
    
    

        for (int i = 0; i < 20; i++) {
    
    
            User user = new User();
            user.setAge(10);
            user.setUsername("zhmsky");
            user.setEmail("[email protected]");
            userList.add(user);
        }
        long l = System.currentTimeMillis();
        userService.saveBatch(userList,10);
        long l1 = System.currentTimeMillis();
        System.out.println("-------------------:"+(l1-l));
        userList.clear();
    }

出力結果:
ここに画像の説明を挿入
バッチ挿入が完了し、作業が終了しました。

次に重要なテスト パフォーマンス

ここに画像の説明を挿入
現在のデータ テーブルのデータ量は 100w を超えています。これに基づいて、元の saveBatch (偽のバッチ挿入) と insertBatchSomeColumn (実際のバッチ挿入) がパフォーマンスの比較に使用されます----(jdbc all enable rewriteBatchedStatements ) :

元の偽の一括挿入:

  @Test
    void insert(){
    
    
        for (int i = 0; i < 50000; i++) {
    
    
            User user = new User();
            user.setAge(10);
            user.setUsername("zhmsky");
            user.setEmail("[email protected]");
            userList.add(user);
        }
        long l = System.currentTimeMillis();
        userService.saveBatch(userList,1000);
        long l1 = System.currentTimeMillis();
        System.out.println("原来的saveBatch方法耗时:"+(l1-l));
    }

ここに画像の説明を挿入
カスタムinsertBatchSomeColumn:

    @Test
    void contextLoads() {
    
    

        for (int i = 0; i < 50000; i++) {
    
    
            User user = new User();
            user.setAge(10);
            user.setUsername("zhmsky");
            user.setEmail("[email protected]");
            userList.add(user);
        }
        long l = System.currentTimeMillis();
        userService.saveBatch(userList,1000);
        long l1 = System.currentTimeMillis();
        System.out.println("自定义的insertBatchSomeColumn方法耗时:"+(l1-l));
        userList.clear();
    }

ここに画像の説明を挿入
バッチで 50,000 個のデータを挿入すると、カスタムの実際のバッチ挿入時間は約 3 秒短縮され、insertBatchSomeColum を使用してバッチで 1500 個のデータを挿入すると 650 ミリ秒かかります。これはすでにかなり高速です
ここに画像の説明を挿入

おすすめ

転載: blog.csdn.net/weixin_42194695/article/details/126349842