SpringBoot 高效批量插入萬(wàn)級(jí)數(shù)據(jù),哪種方式最強(qiáng)?
上方藍(lán)色“ JavaPub ”,選擇“設(shè)為星標(biāo)”
回復(fù)“ 資料 ”獲取整理好的 面試資料
原文:
blog.csdn.net/weixin_44030143/article/details/130825037
準(zhǔn)備工作
1、Maven項(xiàng)目中pom.xml文件引入的相關(guān)依賴如下
<dependencies>
<!-- SpringBoot Web模塊依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis-Plus 依賴 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
<!-- 數(shù)據(jù)庫(kù)連接驅(qū)動(dòng) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 使用注解,簡(jiǎn)化代碼-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
2、application.yml配置屬性文件內(nèi)容(重點(diǎn):開啟批處理模式)
server:
端口號(hào)
port: 8080
# MySQL連接配置信息(以下僅簡(jiǎn)單配置,更多設(shè)置可自行查看)
spring:
datasource:
連接地址(解決UTF-8中文亂碼問(wèn)題 + 時(shí)區(qū)校正)
(rewriteBatchedStatements=true 開啟批處理模式)
url: jdbc:mysql://127.0.0.1:3306/bjpowernode?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
用戶名
username: root
密碼
password: xxx
連接驅(qū)動(dòng)名稱
driver-class-name: com.mysql.cj.jdbc.Driver
3、Entity實(shí)體類(測(cè)試)
/**
* Student 測(cè)試實(shí)體類
*
* @Data注解:引入Lombok依賴,可省略Setter、Getter方法
*/
@Data
@TableName(value = "student")
public class Student {
/** 主鍵 type:自增 */
@TableId(type = IdType.AUTO)
private int id;
/** 名字 */
private String name;
/** 年齡 */
private int age;
/** 地址 */
private String addr;
/** 地址號(hào) @TableField:與表字段映射 */
@TableField(value = "addr_num")
private String addrNum;
public Student(String name, int age, String addr, String addrNum) {
this.name = name;
this.age = age;
this.addr = addr;
this.addrNum = addrNum;
}
}
4、數(shù)據(jù)庫(kù)student表結(jié)構(gòu)(注意:無(wú)索引)
測(cè)試工作
1、for循環(huán)插入(單條)(總耗時(shí):177秒)
總結(jié):測(cè)試平均時(shí)間約是177秒,實(shí)在是不忍直視(捂臉),因?yàn)槔胒or循環(huán)進(jìn)行單條插入時(shí),每次都是在獲取連接(Connection)、釋放連接和資源關(guān)閉等操作上,(如果數(shù)據(jù)量大的情況下)極其消耗資源,導(dǎo)致時(shí)間長(zhǎng)。
@GetMapping("/for")
public void forSingle(){
// 開始時(shí)間
long startTime = System.currentTimeMillis();
for (int i = 0; i < 50000; i++){
Student student = new Student("李毅" + i,24,"張家界市" + i,i + "號(hào)");
studentMapper.insert(student);
}
// 結(jié)束時(shí)間
long endTime = System.currentTimeMillis();
System.out.println("插入數(shù)據(jù)消耗時(shí)間:" + (endTime - startTime));
}
測(cè)試時(shí)間:
2、拼接SQL語(yǔ)句(總耗時(shí):2.9秒)
簡(jiǎn)明:拼接格式:insert into student(xxxx) value(xxxx),(xxxx),(xxxxx)…
總結(jié):拼接結(jié)果就是將所有的數(shù)據(jù)集成在一條SQL語(yǔ)句的value值上,其由于提交到服務(wù)器上的insert語(yǔ)句少了,網(wǎng)絡(luò)負(fù)載少了,性能也就提上去。
但是當(dāng)數(shù)據(jù)量上去后,可能會(huì)出現(xiàn)內(nèi)存溢出、解析SQL語(yǔ)句耗時(shí)等情況,但與第一點(diǎn)相比,提高了極大的性能。
/**
* 拼接sql形式
*/
@GetMapping("/sql")
public void sql(){
ArrayList<Student> arrayList = new ArrayList<>();
long startTime = System.currentTimeMillis();
for (int i = 0; i < 50000; i++){
Student student = new Student("李毅" + i,24,"張家界市" + i,i + "號(hào)");
arrayList.add(student);
}
studentMapper.insertSplice(arrayList);
long endTime = System.currentTimeMillis();
System.out.println("插入數(shù)據(jù)消耗時(shí)間:" + (endTime - startTime));
}
mapper
public interface StudentMapper extends BaseMapper<Student> {
@Insert("<script>" +
"insert into student (name, age, addr, addr_num) values " +
"<foreach collection='studentList' item='item' separator=','> " +
"(#{item.name}, #{item.age},#{item.addr}, #{item.addrNum}) " +
"</foreach> " +
"</script>")
int insertSplice(@Param("studentList") List<Student> studentList);
}
測(cè)試結(jié)果
3、批量插入saveBatch(總耗時(shí):2.7秒)
簡(jiǎn)明:使用MyBatis-Plus實(shí)現(xiàn)IService接口中批處理saveBatch()方法,對(duì)底層源碼進(jìn)行查看時(shí),可發(fā)現(xiàn)其實(shí)是for循環(huán)插入,但是與第一點(diǎn)相比,為什么性能上提高了呢?
因?yàn)槔梅制幚恚╞atchSize = 1000) + 分批提交事務(wù)的操作,從而提高性能,并非在Connection上消耗性能。(目前個(gè)人覺得較優(yōu)化方案)
/**
* mybatis-plus的批處理模式
*/
@GetMapping("/saveBatch1")
public void saveBatch1(){
ArrayList<Student> arrayList = new ArrayList<>();
long startTime = System.currentTimeMillis();
// 模擬數(shù)據(jù)
for (int i = 0; i < 50000; i++){
Student student = new Student("李毅" + i,24,"張家界市" + i,i + "號(hào)");
arrayList.add(student);
}
// 批量插入
studentService.saveBatch(arrayList);
long endTime = System.currentTimeMillis();
System.out.println("插入數(shù)據(jù)消耗時(shí)間:" + (endTime - startTime));
}
重點(diǎn)注意:MySQLJDBC驅(qū)動(dòng)默認(rèn)情況下忽略saveBatch()方法中的executeBatch()語(yǔ)句,將需要批量處理的一組SQL語(yǔ)句進(jìn)行拆散,執(zhí)行時(shí)一條一條給MySQL數(shù)據(jù)庫(kù),造成實(shí)際上是分片插入,即與單條插入方式相比,有提高,但是性能未能得到實(shí)質(zhì)性的提高。
測(cè)試:數(shù)據(jù)庫(kù)連接URL地址缺少 rewriteBatchedStatements = true 參數(shù)情況
# MySQL連接配置信息
spring:
datasource:
連接地址(未開啟批處理模式)
url: jdbc:mysql://127.0.0.1:3306/bjpowernode?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
用戶名
username: root
密碼
password: xxx
連接驅(qū)動(dòng)名稱
driver-class-name: com.mysql.cj.jdbc.Driver
測(cè)試結(jié)果:10541 約等于 10.5秒(未開啟批處理模式)
4、循環(huán)插入 + 開啟批處理模式(總耗時(shí):1.7秒)(重點(diǎn):一次性提交)
簡(jiǎn)明:開啟批處理,關(guān)閉自動(dòng)提交事務(wù),共用同一個(gè)SqlSession之后,for循環(huán)單條插入的性能得到實(shí)質(zhì)性的提高;由于同一個(gè)SqlSession省去對(duì)資源相關(guān)操作的耗能、減少對(duì)事務(wù)處理的時(shí)間等,從而極大程度上提高執(zhí)行效率。(目前個(gè)人覺得較優(yōu)化方案)
/**
* 共用同一個(gè)SqlSession
*/
@GetMapping("/forSaveBatch")
public void forSaveBatch(){
// 開啟批量處理模式 BATCH 、關(guān)閉自動(dòng)提交事務(wù) false
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH,false);
// 反射獲取,獲取Mapper
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
long startTime = System.currentTimeMillis();
for (int i = 0 ; i < 50000 ; i++){
Student student = new Student("李毅" + i,24,"張家界市" + i,i + "號(hào)");
studentMapper.insert(student);
}
// 一次性提交事務(wù)
sqlSession.commit();
// 關(guān)閉資源
sqlSession.close();
long endTime = System.currentTimeMillis();
System.out.println("總耗時(shí): " + (endTime - startTime));
}
5、ThreadPoolTaskExecutor(總耗時(shí):1.7秒)
?目前個(gè)人覺得較優(yōu)化方案
?
@Autowired
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Autowired
private PlatformTransactionManager transactionManager;
@GetMapping("/batchInsert2")
public void batchInsert2() {
ArrayList<Student> arrayList = new ArrayList<>();
long startTime = System.currentTimeMillis();
// 模擬數(shù)據(jù)
for (int i = 0; i < 50000; i++){
Student student = new Student("李毅" + i,24,"張家界市" + i,i + "號(hào)");
arrayList.add(student);
}
int count = arrayList.size();
int pageSize = 1000; // 每批次插入的數(shù)據(jù)量
int threadNum = count / pageSize + 1; // 線程數(shù)
CountDownLatch countDownLatch = new CountDownLatch(threadNum);
for (int i = 0; i < threadNum; i++) {
int startIndex = i * pageSize;
int endIndex = Math.min(count, (i + 1) * pageSize);
List<Student> subList = arrayList.subList(startIndex, endIndex);
threadPoolTaskExecutor.execute(() -> {
DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(transactionDefinition);
try {
studentMapper.insertSplice(subList);
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
} finally {
countDownLatch.countDown();
}
});
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
首先定義了一個(gè)線程池(ThreadPoolTaskExecutor),用于管理線程的生命周期和執(zhí)行任務(wù)。然后,我們將要插入的數(shù)據(jù)列表按照指定的批次大小分割成多個(gè)子列表,并開啟多個(gè)線程來(lái)執(zhí)行插入操作。
首先通過(guò) TransactionManager 獲取事務(wù)管理器,并使用 TransactionDefinition 定義事務(wù)屬性。然后,在每個(gè)線程中,我們通過(guò) transactionManager.getTransaction() 方法獲取事務(wù)狀態(tài),并在插入操作中使用該狀態(tài)來(lái)管理事務(wù)。
在插入操作完成后,我們?cè)俑鶕?jù)操作結(jié)果調(diào)用 transactionManager.commit() 或 transactionManager.rollback() 方法來(lái)提交或回滾事務(wù)。在每個(gè)線程執(zhí)行完畢后,都會(huì)調(diào)用 CountDownLatch 的 countDown() 方法,以便主線程等待所有線程都執(zhí)行完畢后再返回。
推薦閱讀
- 一代碼“黑”掉任意網(wǎng)站
- 爆火情侶竟不是真人!新版Midjourney效果炸裂,網(wǎng)友:太可怕了
- 最“賺錢”編程語(yǔ)言出爐,驚到我了.....
- 最新 23 屆計(jì)算機(jī)大廠校招薪資匯總!
你好,我是 JavaPub,多年開發(fā)老司機(jī),區(qū)塊鏈從業(yè)者、自媒體創(chuàng)作者、站長(zhǎng)。喜歡自由、開放。選擇計(jì)算機(jī)這個(gè)行業(yè),就是因?yàn)闊釔邸R宦愤^(guò)來(lái),給我最深的感受就是一定要不斷學(xué)習(xí)并關(guān)注前沿。只要你能堅(jiān)持下來(lái),多思考、少抱怨、勤動(dòng)手,就很容易實(shí)現(xiàn)彎道超車!所以,不要問(wèn)我現(xiàn)在干什么是否來(lái)得及。如果你看好一個(gè)事情,一定是堅(jiān)持了才能看到希望,而不是看到希望才去堅(jiān)持。相信我,只要堅(jiān)持下來(lái),你一定比現(xiàn)在更好!如果你還沒(méi)什么方向,可以先關(guān)注我,這里會(huì)經(jīng)常分享一些前沿資訊,幫你積累彎道超車的資本。
