【问题标题】:Improving performance of database tests using Spring 3.1, Hibernate 4.1, Dbunit, etc使用 Spring 3.1、Hibernate 4.1、Dbunit 等提高数据库测试的性能
【发布时间】:2012-10-13 19:33:06
【问题描述】:

我目前正在开始一个新项目,并且我有大约 190 个存储库测试。我注意到的一件事——我不完全确定为什么会发生这种情况——是针对 HSQLDB (2.2.8) 的集成测试的运行速度比我认为的要慢得多。

我想我已经在每次测试之前跟踪了数据插入的瓶颈。对于大多数测试,设置数据库的时间范围为 0.15 到 0.38 秒。这是无法接受的。我原以为内存数据库会快得多:(

这是我的所有存储库测试都扩展自的数据库测试类:

@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
@RunWith(SpringJUnit4ClassRunner.class)
@TransactionConfiguration(defaultRollback=true)
@Transactional
public abstract class DatabaseTest {

    public static final String TEST_RESOURCES = "src/test/resources/";

    @Autowired
    protected SessionFactory sessionFactory;

    @Autowired
    protected UserRepository userRepository;

    @Autowired
    protected DataSource dataSource;

    protected IDatabaseTester databaseTester;

    protected Map<String, Object> jdbcMap;
    protected JdbcTemplate jdbcTemplate;

    @PostConstruct
    public void initialize() throws SQLException, IOException, DataSetException {
        jdbcTemplate = new JdbcTemplate(dataSource);

        setupHsqlDb();

        databaseTester = new DataSourceDatabaseTester(dataSource);
        databaseTester.setSetUpOperation(DatabaseOperation.CLEAN_INSERT);
        databaseTester.setTearDownOperation(DatabaseOperation.NONE);
        databaseTester.setDataSet(getDataSet());
    }

    @Before
    public void insertDbUnitData() throws Exception {
        long time = System.currentTimeMillis();

        databaseTester.onSetup();

        long elapsed = System.currentTimeMillis() - time;
        System.out.println(getClass() + " Insert DB Unit Data took: " + elapsed);
    }

    @After
    public void cleanDbUnitData() throws Exception {
        databaseTester.onTearDown();
    }

    public IDataSet getDataSet() throws IOException, DataSetException {
        Set<String> filenames = getDataSets().getFilenames();

        IDataSet[] dataSets = new IDataSet[filenames.size()];
        Iterator<String> iterator = filenames.iterator();
        for(int i = 0; iterator.hasNext(); i++) {
            dataSets[i] = new FlatXmlDataSet(
                new FlatXmlProducer(
                    new InputSource(TEST_RESOURCES + iterator.next()), false, true
                )
            );
        }

        return new CompositeDataSet(dataSets);
    }

    public void setupHsqlDb() throws SQLException {
        Connection sqlConnection = DataSourceUtils.getConnection(dataSource);
        String databaseName = sqlConnection.getMetaData().getDatabaseProductName();
        sqlConnection.close();

        if("HSQL Database Engine".equals(databaseName)) {
            jdbcTemplate.update("SET DATABASE REFERENTIAL INTEGRITY FALSE;");

            // MD5
            jdbcTemplate.update("DROP FUNCTION MD5 IF EXISTS;");
            jdbcTemplate.update(
                "CREATE FUNCTION MD5(VARCHAR(226)) " +
                    "RETURNS VARCHAR(226) " +
                    "LANGUAGE JAVA " +
                    "DETERMINISTIC " +
                    "NO SQL " +
                    "EXTERNAL NAME 'CLASSPATH:org.apache.commons.codec.digest.DigestUtils.md5Hex';"
            );
        } else {
            jdbcTemplate.update("SET foreign_key_checks = 0;");
        }
    }

    protected abstract DataSet getDataSets();

    protected void flush() {
        sessionFactory.getCurrentSession().flush();
    }

    protected void clear() {
        sessionFactory.getCurrentSession().clear();
    }

    protected void setCurrentUser(User user) {
        if(user != null) {
            Authentication authentication = new UsernamePasswordAuthenticationToken(user,
                user, user.getAuthorities());

            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
    }

    protected void setNoCurrentUser() {
        SecurityContextHolder.getContext().setAuthentication(null);
    }

    protected User setCurrentUser(long userId) {
        User user = userRepository.find(userId);

        if(user.getId() != userId) {
            throw new IllegalArgumentException("There is no user with id: " + userId);
        }

        setCurrentUser(user);

        return user;
    }

    protected User getCurrentUser() {
        return (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    }

}

这是我的应用程序上下文中的相关 bean:

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="locations" value="classpath:applicationContext.properties"/>
</bean>

<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
      destroy-method="close">
    <property name="driverClass" value="${database.driver}"/>
    <property name="jdbcUrl" value="${database.url}"/>
    <property name="user" value="${database.username}"/>
    <property name="password" value="${database.password}"/>
    <property name="initialPoolSize" value="10"/>
    <property name="minPoolSize" value="10"/>
    <property name="maxPoolSize" value="50"/>
    <property name="idleConnectionTestPeriod" value="100"/>
    <property name="acquireIncrement" value="2"/>
    <property name="maxStatements" value="0"/>
    <property name="maxIdleTime" value="1800"/>
    <property name="numHelperThreads" value="3"/>
    <property name="acquireRetryAttempts" value="2"/>
    <property name="acquireRetryDelay" value="1000"/>
    <property name="checkoutTimeout" value="5000"/>
</bean>

<bean id="sessionFactory"
      class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="mappingResources">
        <list>
            <value>...</value>
        </list>
    </property>
    <property name="namingStrategy">
        <bean class="org.hibernate.cfg.ImprovedNamingStrategy"/>
    </property>
    <property name="hibernateProperties">
        <props>
            <prop key="javax.persistence.validation.mode">none</prop>

            <prop key="hibernate.dialect">${hibernate.dialect}</prop>
            <prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}
            </prop>
            <prop key="hibernate.generate_statistics">false</prop>

            <prop key="hibernate.show_sql">false</prop>
            <prop key="hibernate.format_sql">true</prop>

            <prop key="hibernate.cache.use_second_level_cache">false</prop>
            <prop key="hibernate.cache.provider_class">

            </prop>
        </props>
    </property>
</bean>

<bean class="org.springframework.orm.hibernate4.HibernateExceptionTranslator"/>

<bean id="transactionManager"
      class="org.springframework.orm.hibernate4.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory"/>
</bean>

为了尝试插入更少的数据,我允许每个测试类选择一个仅加载所需数据的 DataSet 枚举。它是这样指定的:

public enum DataSet {
    NONE(create()),
    CORE(create("core.xml")),
    USERS(combine(create("users.xml"), CORE)),
    TAGS(combine(create("tags.xml"), USERS)),

这会导致它运行得更慢而不是更快吗?这个想法是,如果我只想要核心 xml(语言、省份等),我只需要加载那些记录。我认为这会使测试套件更快,但还是太慢了。

我可以通过创建一个专门为每个测试类设计的单独的 xml 数据集来节省一些时间。这会删除一些插入语句。但即使我在单个 xml 数据集中有 20 个插入语句(因此,除了将数据集直接内联到 java 代码中之外,最小的 I/O 损失),在初始化期间每个测试仍然需要 0.1 到 0.15 秒数据库数据!我不敢相信将 20 条记录插入内存需要 0.15 秒。

在我使用 Spring 3.0 和 Hibernate 3.x 的另一个项目中,在每次测试之前插入所有内容需要 30 毫秒,但实际上每次测试插入 100 或更多行。对于只有 20 个插入的测试,它们就像没有延迟一样在飞行。这是我所期望的。我开始认为问题出在 Spring 的注释上——或者我在 DatabaseTest 类中设置它们的方式。这基本上是现在唯一不同的地方了。

另外,我的存储库使用 sessionFactory.getCurrentSession() 而不是 HibernateTemplate。这是我第一次开始使用 Spring 中基于注释的单元测试,因为 Spring 测试类已被弃用。这可能是他们进展缓慢的原因吗?

如果您还需要了解任何其他信息来帮助解决问题,请告诉我。我有点难过。

编辑:我输入了答案。问题是 hsqldb 2.2.x。恢复到 2.0.0 可以解决问题。

【问题讨论】:

    标签: performance spring hibernate unit-testing dbunit


    【解决方案1】:

    看起来很快,恕我直言。我见过慢得多的集成测试。也就是说,有多种方法可以使您的测试更快:

    • 减少插入数据量
    • 如果数据集相同并且之前的测试是只读测试(通常是这种情况,尤其是在每次测试结束时回滚时),请避免插入与之前的测试相同的数据。

    我想用 DbUnit 来做应该是可能的。如果你准备好使用另一个框架,你可以使用我自己的DbSetup,它支持开箱即用。

    【讨论】:

    • 不幸的是,我没有插入那么多记录——最大的情况下总共可能有 40 条记录。这基本上意味着插入 1 条记录需要 0.01 秒,对于具有 4 个 ram 和 4 个处理器的计算机,我发现这非常令人惊讶。 .15 到 .38 乘以 190 次测试会浪费很多时间。我有另一个项目使用带有 Spring 3.0 的 Hibernate 3,我可以在 100 秒内运行所有 1800 个测试。设置非常相似,只是项目使用的是 HibernateTemplate 和旧版本的 Spring/Hibernate。您对 DbSetup 有任何基准测试吗?使用 maven 吗?
    • 顺便说一句,你的框架不使用注释太糟糕了。由于读取比写入多,因此您应该只为写入情况指定它(而不是为读取情况添加额外代码)。
    • 这是我考虑过的,但我选择了安全路径:如果您忘记将测试声明为只读,则不会发生任何不好的事情(除了不需要的数据库设置)。另一方面,如果您忘记将测试标记为可读写,则后续测试可能会由于此遗漏而失败,这可能会导致调试噩梦。关于注释,我不希望项目与任何特定的测试框架相结合。
    • 给人们一个选择,这样他们就可以选择自己的噩梦:-)
    • 也许我会的。另一方面,DbSetupTracker 包含大约 15 行开源 Java 代码,因此编写一个应用反向逻辑的自定义代码应该不会太难。顺便说一句:您确定设置所花费的时间来自插入,而不是来自从文件系统读取数据集?
    【解决方案2】:

    问题是 Hsqldb 2.2.8。我恢复到 2.0.0,性能立即提升了 8-10 倍或更好。它不再需要 150-280 毫秒,而是下降到 7-15(有时是 20)毫秒。

    我的整个测试套件(490 个测试)现在只需 18 秒而不是 80 秒即可运行。

    我想给大家一个提示:避免使用 hsqldb 2.2.x。我认为他们添加了多线程支持,这会导致此类用例出现性能问题。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-02-19
      • 2016-09-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多