Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Understanding the Performance Impact of @Transactional(readOnly=true) in Spring

Tech 3

Today, I want to discuss the @Transactional(readOnly = true) annotation provided by Spring.

I'm bringing this up because there are many instances of @Transactional(readOnly = true) in my company's project code, and colleagues who have used it say that @Transactional(readOnly = true) improves performance. Let's first consider the following points:

  • How does @Transactional(readOnly = true) work, and why does using it enhance performance?
  • When using JPA, should we always add @Transactional(readOnly = true) to read-only methods at the service layer? What are the trade-offs?

Before we begin, note that we are using Hibernate to implement JPA.

1. How does @Transactional(readOnly = true) work, and why does using it enhance performance?

First, let's look at the transaction interface.

/**
 * A boolean flag that can be set to {@code true} if the transaction is
 * effectively read-only, allowing for corresponding optimizations at runtime.
 * <p>Defaults to {@code false}.
 * <p>This just serves as a hint for the actual transaction subsystem;
 * it will <i>not necessarily</i> cause failure of write access attempts.
 * A transaction manager which cannot interpret the read-only hint will
 * <i>not</i> throw an exception when asked for a read-only transaction
 * but rather silently ignore the hint.
 * @see org.springframework.transaction.interceptor.TransactionAttribute#isReadOnly()
 * @see org.springframework.transaction.support.TransactionSynchronizationManager#isCurrentTransactionReadOnly()
 */
boolean readOnly() default false;

We can see that the readOnly = true option allows for optimizations. The transaction manager will use the read-only option as a hint. Let's look at JpaTransactionManager, which is used to transaction management.

@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
    JpaTransactionObject txObject = (JpaTransactionObject) transaction;
    // ...
    // Delegate to JpaDialect for actual transaction begin.
    int timeoutToUse = determineTimeout(definition);
    Object transactionData = getJpaDialect().beginTransaction(em,
        new JpaTransactionDefinition(definition, timeoutToUse, txObject.isNewEntityManagerHolder()));
    //...
}

In JpaTransactionManager, the doBegin method delegates to JpaDialect to start the actual transaction, and beginTransaction is called within JpaDialect. Let's examine the HibernateJpaDialect class.

@Override
public Object beginTransaction(EntityManager entityManager, TransactionDefinition definition)
    throws PersistenceException, SQLException, TransactionException {
    // ...
    // Adapt flush mode and store previous isolation level, if any.
    FlushMode previousFlushMode = prepareFlushMode(session, definition.isReadOnly());
    if (definition instanceof ResourceTransactionDefinition &&
        ((ResourceTransactionDefinition) definition).isLocalResource()) {
        // As of 5.1, we explicitly optimize for a transaction-local EntityManager,
        // aligned with native HibernateTransactionManager behavior.
        previousFlushMode = null;
        if (definition.isReadOnly()) {
            session.setDefaultReadOnly(true);
        }
    }
    // ...
}

protected FlushMode prepareFlushMode(Session session, boolean readOnly) throws PersistenceException {
    FlushMode flushMode = session.getHibernateFlushMode();
    if (readOnly) {
        // We should suppress flushing for a read-only transaction.
        if (!flushMode.equals(FlushMode.MANUAL)) {
            session.setHibernateFlushMode(FlushMode.MANUAL);
            return flushMode;
        }
    }
    else {
        // We need AUTO or COMMIT for a non-read-only transaction.
        if (flushMode.lessThan(FlushMode.COMMIT)) {
            session.setHibernateFlushMode(FlushMode.AUTO);
            return flushMode;
        }
    }
    // No FlushMode change needed...
    return null;
}

In JpaDialect, we can see that JpaDialect uses the read-only option to prepare the flush mode. When readOnly = true, JpaDialect disables flushing. Additionally, you can see that after preparing the flush mode, session.setDefaultReadOnly(true) sets the session's read-only property to true.

/**
 * Change the default for entities and proxies loaded into this session
 * from modifiable to read-only mode, or from modifiable to read-only mode.
 *
 * Read-only entities are not dirty-checked and snapshots of persistent
 * state are not maintained. Read-only entities can be modified, but
 * changes are not persisted.
 *
 * When a proxy is initialized, the loaded entity will have the same
 * read-only/modifiable setting as the uninitialized
 * proxy has, regardless of the session's current setting.
 *
 * To change the read-only/modifiable setting for a particular entity
 * or proxy that is already in this session:
 * @see Session#setReadOnly(Object,boolean)
 *
 * To override this session's read-only/modifiable setting for entities
 * and proxies loaded by a Query:
 * @see Query#setReadOnly(boolean)
 *
 * @param readOnly true, the default for loaded entities/proxies is read-only;
 *                 false, the default for loaded entities/proxies is modifiable
 */
void setDefaultReadOnly(boolean readOnly);

In the Session interface, by setting the read-only property to true, dirty checking will not be performed on read-only entities, and snapshots of persistent state will not be maintained. Additionally, changes to read-only entities will not be persisted.

In summary, these are the results obtained by using @Transactional(readOnly = true) in Hibernate:

  • Performance improvement: Read-only entities do not undergo dirty checking.
  • Memory saving: Snapshots of persistent state are not maintained.
  • Data consistency: Changes to read-only entities are not persisted.
  • When using master-slave or read-write replica sets (or clusters), @Transactional(readOnly = true) allows us to connect to read-only databases.

2. When using JPA, should we always add @Transactional(readOnly = true) to read-only methods at the service layer? What are the trade-offs?

I see that when using @Transactional(readOnly = true), we can have many advantages. However, is it appropriate to add @Transactional(readOnly = true) to read-only methods at the service layer? Here are my concerns:

  1. Unlimtied use of transactions may lead to database deadlocks, performance degradation, and reduced throughput.
  2. Since a transaction occupies a DB connection, adding @Transactional(readOnly = true) to methods at the service layer may cause DB connection starvation.

The first issue is difficult to reproduce, so I conducted some tests to check the second issue.

@Transactional(readOnly = true)
public List<UserDto> transactionalReadOnlyOnService(){
    List<UserDto> userDtos = userRepository.findAll().stream()
            .map(userMapper::toDto)
            .toList();
    timeSleepAndPrintConnection();
    return userDtos;
}

public List<UserDto> transactionalReadOnlyOnRepository(){
    List<UserDto> userDtos = userRepository.findAll().stream()
            .map(userMapper::toDto)
            .toList();
    timeSleepAndPrintConnection();
    return userDtos;
}

I tested two methods at the service layer: one with @Transactional(readOnly = true) and another with @Transactional(readOnly = true) at the repository layer (in SimpleJpaRepository, wich is the default implementation of JpaRepository, there is @Transactional(readOnly = true) at the top of the class, so the findAll() method has @Transactional(readOnly = true) by default).

I retrieved userInfo from the DB and kept the thread for 5 seconds, then checked when the method releases the connection.

The results are as follows:

For @Transactional(readOnly = true) at the service layer method:

activeConnections:0, IdleConnections:10, TotalConnections:10
start transactionalReadOnlyOnService!!
Hibernate: 
    select
        u1_0.id,
        u1_0.email,
        u1_0.name,
        u1_0.profile_file_name 
    from
        users u1_0
activeConnections:1, IdleConnections:9, TotalConnections:10
activeConnections:1, IdleConnections:9, TotalConnections:10
activeConnections:1, IdleConnections:9, TotalConnections:10
activeConnections:1, IdleConnections:9, TotalConnections:10
activeConnections:1, IdleConnections:9, TotalConnections:10
end transactionalReadOnlyOnService!!
activeConnections:0, IdleConnections:10, TotalConnections:10

For @Transactional(readOnly = true) at the repository layer method:

activeConnections:0, IdleConnections:10, TotalConnections:10
start transactionalReadOnlyOnRepository!!
Hibernate: 
    select
        u1_0.id,
        u1_0.email,
        u1_0.name,
        u1_0.profile_file_name 
    from
        users u1_0
activeConnections:0, IdleConnections:10, TotalConnections:10
activeConnections:0, IdleConnections:10, TotalConnections:10
activeConnections:0, IdleConnections:10, TotalConnections:10
activeConnections:0, IdleConnections:10, TotalConnections:10
activeConnections:0, IdleConnections:10, TotalConnections:10
end transactionalReadOnlyOnRepository!!
activeConnections:0, IdleConnections:10, TotalConnections:10

As you can see, @Transactional(readOnly = true) at the repository layer releases the connection once the query results arrive.

However, @Transactional(readOnly = true) at the service layer method does not release the connection until the service layer method ends.

Therefore, be cautious when service layer methods have logic that requires a lot of time, as they can hold database connections for extended periods, potentially leading to database connection starvation.

3. Review

Clearly, @Transactional(readOnly = true) has many advantages.

  • Performance improvement: Read-only entities do not undergo dirty checking.
  • Memory saving: Snapshots of persistent state are not maintained.
  • Data consistency: Changes to read-only entities are not persisted.
  • When using master-slave or read-write replica sets (or clusters), @Transactional(readOnly = true) allows us to connect to read-only databases.

How ever, you should also remember that @Transactional(readOnly = true) at the service layer method may lead to database deadlocks, performance degradation, and database connection starvation!

When you need to execute read-only queries only as a transaction, do not hesitate to use @Transactional(readOnly = true) at the service layer method, but if your service layer method has a lot of other logic, you need to make trade-offs!

Tags: springjpa

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.