[英]Using flush() method on each 100 rows of 10 000 slows transaction
我有一個示例項目使用spring-boot
與spring-data-jpa
和postgres db
與一個表。
我試圖INSERT
10個000記錄在循環到表和測量執行時間-啟用或禁用flush()
從方法EntityManager
類對於每100個記錄。
預期的結果是啟用flush()
方法的執行時間遠少於禁用的執行時間,但實際上我得到了相反的結果。
UserService.java
package sample.data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
UserRepository userRepository;
public User save(User user) {
return userRepository.save(user);
}
}
UserRepository.java
package sample.data;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<User, Long> { }
Application.java
package sample;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.transaction.annotation.Transactional;
import sample.data.User;
import sample.data.UserService;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
@SpringBootApplication
@EnableJpaRepositories(considerNestedRepositories = true)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Autowired
private UserService userService;
@PersistenceContext
EntityManager entityManager;
@Bean
public CommandLineRunner addUsers() {
return new CommandLineRunner() {
@Transactional
public void run(String... args) throws Exception {
long incoming = System.currentTimeMillis();
for (int i = 1; i <= 10000; i++) {
userService.save(new User("name_" + i));
if (i % 100 == 0) {
entityManager.flush();
entityManager.clear();
}
}
entityManager.close();
System.out.println("Time: " + (System.currentTimeMillis() - incoming));
}
};
}
}
確保在持久性提供程序配置中啟用JDBC批處理。 如果您正在使用Hibernate,請將其添加到Spring屬性中:
spring.jpa.properties.hibernate.jdbc.batch_size=20 // or some other reasonable value
在沒有啟用批處理的情況下,我認為性能回歸是由於每100個實體清除持久性上下文的開銷,但我不確定(你必須測量)。
更新:
實際上,啟用JDBC批處理或禁用它不會影響這樣一個事實,即每隔一段時間使用flush()
就會比沒有它更快。 什么你使用手動控制flush()
是沖洗不如何做(通過批處理的語句或單一刀片),但是當沖洗到數據庫會做,而不是你的控制。
所以你要比較的是:
flush()
每100個對象:在flush()
將100個實例插入數據庫,然后執行10000/100 = 100次 。 flush()
:您只需在內存中的上下文中收集所有10000個對象,並在提交事務時執行10000次插入。 另一方面,JDBC批處理會影響刷新的發生方式,但仍然使用flush()
vs而不是flush()
發出相同數量的語句。
在循環中每隔一段時間刷新和清除的好處是避免由於緩存持有太多對象而OutOfMemoryError
。
寫一個微觀基准很難, Aleksey Shipilev在他的“JMH vs Caliper:reference thread”帖子中有很好的說明 。 您的案例不完全是微觀基准,但是:
低於10,000次重復將不會讓JVM預熱並在默認設置上JIT代碼。 在測量代碼性能之前預熱JVM。
System.nanoTime()
不是System.currentTimeMillis()
用於測量經過的時間。 如果以ms
為單位測量,則System.currentTimeMillis()
時鍾漂移會導致結果偏差。
您最有可能希望在數據庫端測量此值以查明瓶頸。 沒有瓶頸,很難理解什么是根本原因,例如您的數據庫可能位於大西洋的另一邊,網絡連接成本將掩蓋INSERT
語句成本。
您的基准是否足夠孤立? 如果數據庫由多個用戶和連接共享,那么除了基准測試之外,它的性能會有所不同。
找出當前設置中的瓶頸,假設如何驗證它,更改基准以匹配假設,然后再次測量以確認。 這是解決問題的唯一方法。
持久化實體最昂貴的部分是寫入數據庫。 相比之下,將實體保留在JPA中所花費的時間是微不足道的,因為它是純粹的內存操作。 與內存相比,它是IO。
寫入數據庫可能還會產生非常大的靜態開銷,這意味着寫入數據庫的次數可能會影響執行時間。 當您調用EntityManager#flush
,您會指示Hibernate將所有掛起的更改寫入數據庫。
所以你要做的是將執行與100個數據庫寫入比較,用一個數據庫寫入。 由於IO的開銷,前者將明顯變慢。
其他答案未提及的兩個方面。 除了刷新你需要清除Hibernate會話。 如果不清除它,它會增長,並會影響你的內存消耗,這可能會導致性能下降。
持久化實體時還有一件事要確保您的ID生成器使用hilosequence。 如果你的ID是1,2,3,4,5 ......每個插入都會有額外的往返以增加ID。
你能解釋為什么你相信:
預期的結果是啟用flush()方法的執行時間遠少於禁用的方法
在我看來,這是一個根本錯誤的假設。 沒有充分的理由相信,執行這個微不足道的操作10k次會更快,而不是沒有沖洗。
只要所有記錄都適合內存,我就會期望非中間刷新版本更快。 什么表明執行網絡IO訪問數據庫100次應該比最后一次執行1次更快?
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.