source

봄의 jdbc 템플릿인 이유.batchUpdate() 너무 느리다고요?

factcode 2023. 10. 26. 21:52
반응형

봄의 jdbc 템플릿인 이유.batchUpdate() 너무 느리다고요?

배치 삽입을 더 빠르게 할 수 있는 방법을 찾고 있습니다.

jdbcTemplate.update(String sql)로 여러 배치를 삽입하려고 했는데, 여기서 sql은 StringBuilder에 의해 빌드되었으며 다음과 같습니다.

INSERT INTO TABLE(x, y, i) VALUES(1,2,3), (1,2,3), ... , (1,2,3)

배치 크기는 정확히 1000이었습니다.거의 100개의 배치를 삽입했습니다.StopWatch를 사용하여 시간을 확인한 결과 삽입 시간:

min[38ms], avg[50ms], max[190ms] per batch

기쁘기도 했지만 코드를 더 잘 만들고 싶었습니다.

그 후 저는 jdbcTemplate를 사용하려고 했습니다.batchUpdate다음과 같은 방식으로 업데이트:

    jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
        @Override
        public void setValues(PreparedStatement ps, int i) throws SQLException {
                       // ...
        }
        @Override
        public int getBatchSize() {
            return 1000;
        }
    });

sql이 생긴 곳.

INSERT INTO TABLE(x, y, i) VALUES(1,2,3);

그리고 실망했습니다! jdbcTemplate는 1000라인 배치의 모든 삽입을 분리된 방식으로 실행했습니다.mysql_log를 살펴보니 수천 개의 삽입물이 있었습니다.StopWatch를 사용하여 시간을 확인한 결과 삽입 시간:

배치당 min[900ms], avg[1100ms], max[2000ms]

그럼 누가 왜 jdbcTemplate가 이 방법으로 분리 삽입을 하는지 설명해 줄 수 있나요?메서드의 이름이 batchUpdate인 이유는 무엇입니까?아니면 제가 이 방법을 잘못 사용하고 있는 것이 아닐까요?

JDBC 연결 URL에 있는 이러한 매개 변수들은 일괄 처리된 문장의 속도에 큰 차이를 만들 수 있습니다. 제 경험으로는, 이 매개 변수들은 일의 속도를 빠르게 합니다.

?ServerPrepStmts=false를 사용하여 배치된 문 다시 쓰기=true

참조: JDBC 배치 삽입 성능

통화에서 argTypes 배열을 설정하는 것이 크게 개선되었습니다.

Spring 4.1.4 및 Oracle 12c의 경우 35개의 필드로 5000개의 행을 삽입하는 경우:

jdbcTemplate.batchUpdate(insert, parameters); // Take 7 seconds

jdbcTemplate.batchUpdate(insert, parameters, argTypes); // Take 0.08 seconds!!!

argTypers 매개변수는 다음과 같은 방식으로 각 필드를 설정하는 int 배열입니다.

int[] argTypes = new int[35];
argTypes[0] = Types.VARCHAR;
argTypes[1] = Types.VARCHAR;
argTypes[2] = Types.VARCHAR;
argTypes[3] = Types.DECIMAL;
argTypes[4] = Types.TIMESTAMP;
.....

org\springframework\jdbc\core\JdbcTemplate를 디버깅했습니다.java를 통해 대부분의 시간이 각 분야의 성격을 알기 위해 소비되었고, 이것은 각 기록마다 만들어졌다는 것을 발견했습니다.

도움이 되길 바랍니다!

Spring JDBC 템플릿에 대해서도 동일한 문제에 직면했습니다.아마도 Spring Batch에서는 모든 삽입자나 청크에 대해 문장이 실행되고 커밋되어 작업 속도가 느려졌을 것입니다.

저는 jdbcTemplate를 교체하였습니다.원래 JDBC 배치 삽입 코드가 있는 batchUpdate() 코드를 사용하여 Major 성능 향상을 찾았습니다.

DataSource ds = jdbcTemplate.getDataSource();
Connection connection = ds.getConnection();
connection.setAutoCommit(false);
String sql = "insert into employee (name, city, phone) values (?, ?, ?)";
PreparedStatement ps = connection.prepareStatement(sql);
final int batchSize = 1000;
int count = 0;

for (Employee employee: employees) {

    ps.setString(1, employee.getName());
    ps.setString(2, employee.getCity());
    ps.setString(3, employee.getPhone());
    ps.addBatch();

    ++count;

    if(count % batchSize == 0 || count == employees.size()) {
        ps.executeBatch();
        ps.clearBatch(); 
    }
}

connection.commit();
ps.close();

링크도 JDBC 일괄 삽입 성능을 확인

거래를 이용하기만 하면 됩니다.@Transactional on 메서드를 추가합니다.

여러 데이터 소스 @Transactional("dsTxManager")을 사용하는 경우 올바른 TX 관리자를 선언해야 합니다.나는 60000개의 레코드를 삽입하는 경우가 있습니다.15초 정도 걸립니다.다른 조정은 없습니다.

@Transactional("myDataSourceTxManager")
public void save(...) {
...
    jdbcTemplate.batchUpdate(query, new BatchPreparedStatementSetter() {

            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                ...

            }

            @Override
            public int getBatchSize() {
                if(data == null){
                    return 0;
                }
                return data.size();
            }
        });
    }

sql 삽입을 다음으로 변경합니다.INSERT INTO TABLE(x, y, i) VALUES(1,2,3)를 생성합니다 프레임워크는 사용자에게 루프를 만들어 줍니다.예를 들어,

public void insertBatch(final List<Customer> customers){

  String sql = "INSERT INTO CUSTOMER " +
    "(CUST_ID, NAME, AGE) VALUES (?, ?, ?)";

  getJdbcTemplate().batchUpdate(sql, new BatchPreparedStatementSetter() {

    @Override
    public void setValues(PreparedStatement ps, int i) throws SQLException {
        Customer customer = customers.get(i);
        ps.setLong(1, customer.getCustId());
        ps.setString(2, customer.getName());
        ps.setInt(3, customer.getAge() );
    }

    @Override
    public int getBatchSize() {
        return customers.size();
    }
  });
}

이런 거 있으면.봄은 다음과 같은 역할을 합니다.

for(int i = 0; i < getBatchSize(); i++){
   execute the prepared statement with the parameters for the current iteration
}

프레임워크는 먼저 쿼리에서 준비된 상태를 만듭니다.sqlvariable) 그런 다음 setValues 메서드를 호출하고 문을 실행합니다.그것은 당신이 지정한 만큼 반복됩니다.getBatchSize()방법.따라서 삽입문을 작성하는 올바른 방법은 하나의 값 절만 사용하는 것입니다.http://docs.spring.io/spring/docs/3.0.x/reference/jdbc.html 을 보실 수 있습니다.

저는 Spring JDBC batch template 로도 조금 안 좋은 시간을 보냈습니다.제 경우엔 순수한 JDBC를 사용하는 건 미친 짓일 테니까요NamedParameterJdbcTemplate은 제 에서 꼭 이것은 제 프로젝트에서 꼭 필요한 것이었습니다.하지만 데이터베이스에 수천 개의 행을 삽입하는 것은 매우 느렸습니다.

무슨 일이 일어나고 있는지 확인하기 위해 배치 업데이트 중에 VisualVM을 사용하여 샘플을 채취한 후 다음과 voila:

visualvm showing where it was slow

이 과정에서 속도가 느려진 것은 파라미터를 설정하는 동안 Spring JDBC가 데이터베이스에 각 파라미터의 메타데이터를 쿼리하고 있다는 것입니다.그리고 매번 라인별로 파라미터별 데이터베이스를 조회하는 것처럼 보였습니다.그래서 방금 Spring에게 매개 변수 유형을 무시하라고 가르쳤습니다(개체 목록 배치 작동에 대한 Spring 설명서에 나와 있듯이).

    @Bean(name = "named-jdbc-tenant")
    public synchronized NamedParameterJdbcTemplate getNamedJdbcTemplate(@Autowired TenantRoutingDataSource tenantDataSource) {
        System.setProperty("spring.jdbc.getParameterType.ignore", "true");
        return new NamedParameterJdbcTemplate(tenantDataSource);
    }

참고: JDBC Template 개체를 만들기 에 시스템 속성을 설정해야 합니다.그냥 셋팅을 하는 것이 가능할 것입니다.application.properties, 하지만 이 일은 해결되었고 난 이 일 이후로 다시는 이 일에 손을 대지 않았습니다.

이것이 당신에게 효과가 있을지 모르겠지만, 제가 결국 사용하게 된 스프링 프리 방법이 있습니다.제가 시도한 다양한 스프링 방식보다 훨씬 빨랐습니다.심지어 다른 답변이 설명하는 JDBC 템플릿 일괄 업데이트 방법을 사용해 보았지만, 그것도 제가 원하는 것보다 느렸습니다.저는 그 거래가 무엇이었는지 잘 모르겠고 인터넷에서도 많은 답변이 없었습니다.저는 그것이 커밋이 어떻게 처리되고 있는지와 관련이 있다고 의심했습니다.

이 접근 방식은 java.sql 패키지와 PreparedStatement의 배치 인터페이스를 사용하는 straight JDBC입니다.이것이 24M 레코드를 MySQL DB로 가져올 수 있는 가장 빠른 방법이었습니다.

저는 "기록" 개체의 컬렉션을 구축한 다음 모든 레코드를 일괄 삽입하는 방식으로 아래 코드를 호출했습니다.컬렉션을 구축한 루프는 배치 크기를 관리하는 역할을 담당했습니다.

MySQL DB에 24M 레코드를 삽입하려고 했는데 Spring batch를 사용하여 초당 200개의 레코드를 전송하고 있었습니다.이 방식으로 바꾸자 초당 ~2500개의 레코드가 올라갔습니다.그래서 제 24M 레코드 로드는 이론적으로 1.5일에서 약 2.5시간으로 증가했습니다.

먼저 연결 만들기...

Connection conn = null;
try{
    Class.forName("com.mysql.jdbc.Driver");
    conn = DriverManager.getConnection(connectionUrl, username, password);
}catch(SQLException e){}catch(ClassNotFoundException e){}

그런 다음 준비된 문을 만들어 삽입할 값의 배치와 함께 로드한 다음 단일 배치 삽입...으로 실행합니다.

PreparedStatement ps = null;
try{
    conn.setAutoCommit(false);
    ps = conn.prepareStatement(sql); // INSERT INTO TABLE(x, y, i) VALUES(1,2,3)
    for(MyRecord record : records){
        try{
            ps.setString(1, record.getX());
            ps.setString(2, record.getY());
            ps.setString(3, record.getI());

            ps.addBatch();
        } catch (Exception e){
            ps.clearParameters();
            logger.warn("Skipping record...", e);
        }
    }

    ps.executeBatch();
    conn.commit();
} catch (SQLException e){
} finally {
    if(null != ps){
        try {ps.close();} catch (SQLException e){}
    }
}

분명히 저는 오류 처리를 제거했고 쿼리 및 기록 개체는 개념적인 것과 같은 것입니다.

편집: 원래 질문은 삽입을 푸바 값(?, ??), (?, ??),...(?, ??) 메서드와 스프링 배치를 비교하는 것이었으므로 이에 대한 보다 직접적인 답변이 있습니다.

"LOAD DATA INFILE" 접근 방식과 같은 것을 사용하지 않고 MySQL로 대량 데이터 로드를 수행할 수 있는 가장 빠른 방법은 원래의 방법인 것 같습니다.MysQL 문서(http://dev.mysql.com/doc/refman/5.0/en/insert-speed.html) 에서 인용한 내용:

동일한 클라이언트에서 여러 행을 동시에 삽입하는 경우 여러 개의 값 목록이 있는 INSERT 문을 사용하여 한 번에 여러 행을 삽입합니다.이는 별도의 단일 행 INSERT 문을 사용하는 것보다 상당히 빠릅니다(경우에 따라서는 몇 배나 빠릅니다).

Spring JDBC Template batchUpdate 메서드를 수정하여 'setValues' 호출당 지정된 값이 여러 개인 삽입을 수행할 수 있지만 삽입되는 항목 집합에서 반복할 때 인덱스 값을 수동으로 추적해야 합니다.삽입되는 총 항목 수가 준비된 보고서에 있는 VALUES 목록의 수보다 많지 않을 경우 마지막에 심술궂은 엣지 케이스에 직면하게 됩니다.

개요를 설명하는 접근 방식을 사용하면 동일한 작업을 수행할 수 있습니다(여러 개의 VALUES 목록이 있는 준비된 문을 사용). 마지막에 해당 에지 케이스에 도달하면 정확한 수의 VALUES 목록으로 마지막 문을 만들고 실행할 수 있기 때문에 처리하기가 조금 더 쉽습니다.좀 까다롭긴 하지만 대부분의 최적화된 것들은.

@Rakesh가 제시한 해결책이 저에게는 통했습니다.성능이 크게 향상되었습니다.이전 시간은 8분이었고, 이 솔루션은 2분도 걸리지 않았습니다.

DataSource ds = jdbcTemplate.getDataSource();
Connection connection = ds.getConnection();
connection.setAutoCommit(false);
String sql = "insert into employee (name, city, phone) values (?, ?, ?)";
PreparedStatement ps = connection.prepareStatement(sql);
final int batchSize = 1000;
int count = 0;

for (Employee employee: employees) {

    ps.setString(1, employee.getName());
    ps.setString(2, employee.getCity());
    ps.setString(3, employee.getPhone());
    ps.addBatch();

    ++count;

    if(count % batchSize == 0 || count == employees.size()) {
        ps.executeBatch();
        ps.clearBatch(); 
    }
}

connection.commit();
ps.close();

다음과 같은 심각한 성능 문제가 발생했습니다.JdbcBatchItemWriter.write()Spring Batch에서 제공하는 (링크) 작성 로직 딜러를 찾아봅니다.JdbcTemplate.batchUpdate()결국.

Java 시스템 속성 추가spring.jdbc.getParameterType.ignore=true성능 문제를 완전히 해결했습니다(초당 200개의 레코드에서 ~ 5000개로).패치가 Postgresql 및 MsSql에서 모두 작동하는 것으로 테스트되었습니다(사투리에 특정되지 않을 수 있음).

... 그리고 아이러니하게도 스프링은 이러한 행동을 "노트" 섹션 링크로 기록했습니다.

이러한 시나리오에서는 기본 PreparedStatement에서 값을 자동으로 설정하는 경우 각 값에 해당하는 JDBC 유형을 주어진 Java 유형에서 도출해야 합니다.일반적으로 잘 작동하지만 문제가 발생할 가능성이 있습니다(예: 맵에 포함된 null 값).기본적으로 Spring은 ParameterMetaData.getParameter를 호출합니다.JDBC 드라이버의 경우 비용이 많이 들 수 있는 경우를 입력합니다.Oracle 12c(SPR-16139)에 보고된 것과 같이 성능 문제가 발생할 경우 최신 드라이버 버전을 사용하고 spring.jdbc.getParameterType.ignore 속성을 true(JVM 시스템 속성 또는 클래스 경로 루트의 spring.properties 파일로)로 설정하는 것을 고려해야 합니다.

또는 'BatchPreparedStatementSetter'(앞에서 보여준 것처럼)를 통해, 'List<Object[]>' 기반 호출에 제공되는 명시적 유형 배열을 통해, 사용자 지정 'MapSqlParameterSource' 인스턴스에서 'registerSqlType' 호출을 통해, 해당 JDBC 유형을 명시적으로 지정하는 것을 고려할 수 있습니다.또는 'BeanPropertySqlParameterSource'를 통해 Java 선언 속성 유형에서 SQL 유형을 도출하여 null 값인 경우에도 사용할 수 있습니다.

언급URL : https://stackoverflow.com/questions/20360574/why-springs-jdbctemplate-batchupdate-so-slow

반응형