1、背景:
在做業務系統時,經常會碰到主子表模型,子表的數據量比較大,如果采用for循環進行insert操作,效率會很慢,MyBatis提供一個批量操作功能foreach,批量插入操作效率會大大提高。
<insert id="insertBatch" parameterType="java.util.List"> <![CDATA[insert into bd_user (id, dept_id, user_code, user_name, birthday, usable) values ]]> <foreach collection="list" item="item" index="index" separator=","> <![CDATA[(#{item.id},#{item.deptId},#{item.userCode},#{item.userName},#{item.birthday},#{item.usable})]]> </foreach> </insert>
隨之而來,我們會有一個疑問,這個數據量有沒有極限呢,它會受哪些條件影響?帶着這樣的思考我們進行實驗,看看結果如何。
2、測試過程
1)數據庫使用MySQL8.0.19,默認配置。數據使用虛擬機安裝,虛擬機配置為2核4G內存。
2)數據庫表結構
CREATE TABLE `bd_user` ( `id` bigint(0) NOT NULL, `dept_id` bigint(0) NULL DEFAULT NULL COMMENT '部門ID', `user_code` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用戶編碼', `user_name` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用戶名稱', `birthday` date NULL DEFAULT NULL COMMENT '生日', `usable` tinyint(1) NULL DEFAULT NULL COMMENT '是否可用', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '人員' ROW_FORMAT = DYNAMIC;
3)測試代碼
@SpringBootTest(classes = MybatisApplication.class) public class UserTest { @Autowired private IUserOperateService userOperateService; @Autowired private SnowFlake snowFlake; @Test public void addUser() { int start = 0; int len = 10000; List<User> list = new ArrayList<>(len); for (int j = 0; j < len; j++) { User model = new User(); model.setId(snowFlake.nextId()); model.setDeptId((long) (1 + j % 4)); model.setUserCode(StringUtils.leftPad((++start) + "", 10, "0")); model.setUserName("測試數據" + model.getUserCode()); model.setUsable(Boolean.TRUE); model.setBirthday(new Date()); list.add(model); } long startTime = System.currentTimeMillis(); userOperateService.save(list); // for (User user : list) { // userOperateService.save(user); // } System.out.println("耗時:" + (System.currentTimeMillis() - startTime) + "毫秒"); } }
4)實驗結果如下(每次操作后數據都會被清空)
| 記錄數 | for方式耗時(毫秒) | foreach方式耗時(毫秒) | ||||
| 第一次 | 第二次 | 第三次 | 第一次 | 第二次 | 第三次 | |
| 1000 | 4909 | 4923 | 4327 | 890 | 860 | 879 |
| 5000 | 18196 | 18316 | 18633 | 1350 | 1200 | 1333 |
| 10000 | - | - | - | 1782 | 1476 | 1398 |
| 100000 | - | - | - | 6567 | 4780 | 5288 |
| 500000 | 23691 | 22573 | 22128 | |||
數據達到100W條記錄時,出現如下錯誤:
com.mysql.cj.jdbc.exceptions.PacketTooBigException: Packet for query is too large (99,899,527 > 67,108,864). You can change this value on the server by setting the 'max_allowed_packet' variable.
3、關於MySQL的max_allowed_packet參數
1)參數說明
- max_allowed_packet為數據包消息緩存區最大大小,單位字節,默認值67108864(64M),最大值1073741824(1G),最小值1024(1K),參數值須為1024的倍數,非倍數將四舍五入到最接近的倍數。
- 數據包消息緩存區初始大小為net_buffer_length個字節
- 每條SQL語句和它的參數都會產生一個數據包緩存區,跟事務無關。
- 我嘗試調整該參數的大小,它並不能提高性能,它的作用在於能夠處理大參數,如大BLOB或長字符串就可能調整該參數,還有in后面的記錄數也受制於該參數。
2)查看和設置max_allowed_packet參數
show variables like 'net_buffer_length'; show variables like 'max_allowed_packet'; // 查看參數 set global max_allowed_packet=536870912; // 重新打開數據庫連接參數生效,數據庫服務重啟后參數恢復為默認,想持久化可以在my.cnf中設置該參數
官網介紹:https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_max_allowed_packet
4、回到剛才的報錯
剛才我們測試100W條數據報錯,如果我們把100W數據拆成2個50W條數據進行保存,則不會報錯,耗時大約為插入50條數據的的2倍。
