一、前言
測試妹子反饋了一個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>
這個僅僅是插入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);
怎么會這樣呢?我幾乎是崩潰的,怎么還越來越慢了?!
四、數據庫優化
現在我就在考慮了,會不會不是程序問題導致的呢?會不會是數據庫性能導致的呢?聯想到最近公司剛從雲服務上撤了下來,改成自己搭建服務器和數據庫。數據庫並沒有經過什么優化參數設置。所以,我覺得我這個猜想還是有可行性的!
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條插入應該有的速度嘛!棒棒噠~
七、結語
走了這么多彎路,才醒悟,最被忽略的才是最重要的!
該文旨在介紹多種處理批量插入的方式,解決問題的思路不一定適用,畢竟最后發現完全走錯了路...