記錄一次批量插入的優化歷程


一、前言

    測試妹子反饋了一個bug,說后台報了個服務器異常——保存一個數量比較大的值時,比如 9999,一直在轉圈圈,直到最后報了一個服務器異常。我接過了這個bug,經過仔細查看代碼后發現,代碼卡在了一個批量插入的SQL語句上,就是比如前端保存 9999 的時候,后端的業務邏輯要進行 9999 次的批量插入。

二、方案一

    最開始的SQL語句是這樣的,傳入一個List,由MyBatis 處理這個 List 拼接成一個SQL語句並執行,看着也沒有什么大問題呀!

        INSERT INTO yy_marketing_coupon
        (
        uuid,
        no,
        name,
        type,
        money,
        status,
        instruction,
        astrict,
        total_number,
        remain_number,
        send_mode,
        get_mode,
        use_mode,
        user_rank_lower,
        send_start_time,
        send_end_time,
        use_start_time,
        use_end_time,
        use_expire_time,
        discount,
        user_mobiles,
        create_time,
        creater,
        update_time,
        updater,
        appid,
        use_car_type,
        highest_money,
        term_type,
        coupon_template_uuid,
        gift_uuid,
        city_uuids,
        city_names
        )
        VALUES
        <foreach collection="list" item="item" index="index" separator=",">
            (
            #{item.uuid},
            (select FN_CREATE_COUPON_NO(1)),
            #{item.name},
            #{item.type},
            #{item.money},
            #{item.status},
            #{item.instruction},
            #{item.astrict},
            #{item.totalNumber},
            #{item.remainNumber},
            #{item.sendMode},
            #{item.getMode},
            #{item.useMode},
            #{item.userRankLower},
            #{item.sendStartTime},
            #{item.sendEndTime},
            #{item.useStartTime},
            #{item.useEndTime},
            #{item.useExpireTime},
            #{item.discount},
            #{item.userMobiles},
            #{item.createTime},
            #{item.creater},
            #{item.updateTime},
            #{item.updater},
            #{item.appid},
            #{item.useCarType},
            #{item.highestMoney},
            #{item.termType},
            #{item.couponTemplateUuid},
            #{item.giftUuid},
            #{item.cityUuids},
            #{item.cityNames}
            )
        </foreach>
View Code

    這個僅僅是插入1000條數據的耗時量,快兩分鍾了,這怎么得了?

三、方案二

    經過我們公司的架構師介紹說,要不用 Spring 的 jdbcTemplate 的 batchUpdate() 方法來執行批量插入吧!聽過會走二級緩存?

1、applicationContext.xml

    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>

2、數據庫連接配置 url 中需要加上允許執行批量插入:rewriteBatchedStatements=true

3、jdbcTemplate 的批量插入代碼如下:

String sql = "INSERT INTO " +
        " yy_marketing_coupon(uuid,no,name,type,money,status,instruction,astrict,total_number," +
        "remain_number,send_mode,get_mode,use_mode,user_rank_lower,send_start_time,send_end_time," +
        "use_start_time,use_end_time,use_expire_time,discount,user_mobiles,create_time,creater," +
        "update_time,updater,appid,use_car_type,highest_money,term_type,coupon_template_uuid,gift_uuid," +
        "city_uuids,city_names) " +
        " values (?,(select FN_CREATE_COUPON_NO(1)),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
List<Object[]> batchArgs = new LinkedList<>();
int size = marketingCouponListDo.size();
for (int i = 0; i < size; i++) {
    MarketingCouponDto dto = marketingCouponListDo.get(i);
    Object[] objects = {
            dto.getUuid(),
            dto.getName(),
            dto.getType(),
            dto.getMoney(),
            dto.getStatus(),
            dto.getInstruction(),
            dto.getAstrict(),
            dto.getTotalNumber(),
            dto.getRemainNumber(),
            dto.getSendMode(),
            dto.getGetMode(),
            dto.getUseMode(),
            dto.getUserRankLower(),
            dto.getSendStartTime(),
            dto.getSendEndTime(),
            dto.getUseStartTime(),
            dto.getUseEndTime(),
            dto.getUseExpireTime(),
            dto.getDiscount(),
            dto.getUserMobiles(),
            dto.getCreateTime(),
            dto.getCreater(),
            dto.getUpdateTime(),
            dto.getUpdater(),
            dto.getAppid(),
            dto.getUseCarType(),
            dto.getHighestMoney(),
            dto.getTermType(),
            dto.getCouponTemplateUuid(),
            dto.getGiftUuid(),
            dto.getCityUuids(),
            dto.getCityNames()
    };
    batchArgs.add(objects);
}
jdbcTemplate.batchUpdate(sql, batchArgs);
View Code

    

    怎么會這樣呢?我幾乎是崩潰的,怎么還越來越慢了?!

四、數據庫優化

    現在我就在考慮了,會不會不是程序問題導致的呢?會不會是數據庫性能導致的呢?聯想到最近公司剛從雲服務上撤了下來,改成自己搭建服務器和數據庫。數據庫並沒有經過什么優化參數設置。所以,我覺得我這個猜想還是有可行性的!

1、>  vim /etc/my.cnf

2、數據庫參數做了如下優化設置:

#緩存innodb表的索引,數據,插入數據時的緩沖,操作系統內存的70%-80%最佳
innodb_buffer_pool_size = 4096M
#配置成cpu的線程數
innodb_thread_concurrency = 24
#查詢緩存大小,必須設置成1024的整數倍
query_cache_size = 128M
#為一次插入多條新記錄的INSERT命令分配的緩存區長度(默認設置是8M)。
bulk_insert_buffer_size = 256M
#上傳的數據包大小(默認是4M)
max_allowed_packet=16M
#join語句使用的緩存大小,適當增大到1M左右,內存充足的話可以增加到2MB
join_buffer_size = 2M
#數據進行排序的時候用到的緩存,一般設置成2-4M
sort_buffer_size = 4M
#隨機讀緩存區大小,最大2G
read_rnd_buffer_size = 32M

3、重啟數據庫 > service mysqld restart

    好,再來試一下,結果發現並沒有什么卵用,插入數據庫還是一樣的龜速!那么,到底問題出在哪里呢?!

五、方案三

    架構師又介紹了我一種 Spring+Mybatis 的 sqlSessionTemplate 來批量插入數據,聞言效率更高!

1、applicationContext.xml

<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
    <constructor-arg index="0" ref="sqlSessionFactory" />
</bean>

2、依賴注入

@Autowired
private SqlSessionTemplate sqlSessionTemplate;

3、sqlSessionTemplate 的批量插入代碼如下:

SqlSession session = sqlSessionTemplate.getSqlSessionFactory().openSession(ExecutorType.BATCH, false);
MarketingCouponMapper mapper = session.getMapper(MarketingCouponMapper.class);
int size = marketingCouponListDo.size();
try {
    for (int i = 0; i < size; i++) {
        MarketingCouponDto marketingCouponDto = marketingCouponListDo.get(i);
        mapper.add(marketingCouponDto);
        if (i % 1000 == 0 || i == size - 1) {
            //手動每1000個一個提交,提交后無法回滾
            session.commit();
            //清理緩存,防止溢出
            session.clearCache();
        }
    }
}catch (Exception e){
    session.rollback();
}finally {
    session.close();
}

    測試后發現速度依然沒有什么有效的提升,我要炸了!到底問題出在哪里呢?我想我是不是方向走錯了?是不是根本不是程序的效率問題?

六、解決問題

    最后,我發現,那條簡單的插入語句有個不起眼的地方,(select FN_CREATE_COUPON_NO(1)) — 調用執行過程,我試着把這個調用換成了一個字符串 '111111' 插入,一下子執行速度就提升上來了,我的天,終於找到這個罪魁禍首了!接着怎么優化呢?仔細看看這個存儲過程的邏輯,發現也沒做什么大的業務,那何不把它提出來寫在程序中呢?存儲過程的業務代碼我就不貼了。

    

    這才是1000條插入應該有的速度嘛!棒棒噠~

七、結語

    走了這么多彎路,才醒悟,最被忽略的才是最重要的!

    該文旨在介紹多種處理批量插入的方式,解決問題的思路不一定適用,畢竟最后發現完全走錯了路...


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM