面試官:@Transactional(readOnly=true) 有什麼用?還有誰不會?!

原文翻譯自:https://medium.com

今天,我想談談 Spring 提供的@Transactional(readOnly = true)

之所以聊這個是因爲我公司項目的代碼裏有很多@Transactional(readOnly = true),用過的同學都說@Transactional(readOnly = true)提高了性能。先思考以下幾點:

  • @Transactional(readOnly = true)是如何工作的,爲什麼使用它可以提高性能?
  • 當我們使用 JPA 時,是否應該總是將@Transactional(readOnly = true)添加到服務層的只讀方法?有什麼取捨嗎?

在開始之前,我們使用 Hibernate 來實現 JPA。

推薦一個開源免費的 Spring Boot 實戰項目:

https://github.com/javastacks/spring-boot-best-practice

1、@Transactional(readOnly = true)是如何工作的,爲什麼使用它可以提高性能?

首先,讓我們看一下事務接口。

/**
* 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;

我們可以看到 readOnly = true 選項允許優化。事務管理器將使用只讀選項作爲提示。讓我們看看用於事務管理器的JpaTransactionManager

@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()));
  //...
}

JpaTransactionManager中,doBegin方法委託JpaDialect來開始實際的事務,並在JpaDialect中調用beginTransaction。讓我們來看看HibernateJpaDialect類。

@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(Flusode.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;
}

在JpaDialect中,我們可以看到JpaDialect使用只讀選項準備刷新模式。當 readOnly = true 時, JpaDialect 禁止刷新。此外,您還可以看到,在準備刷新模式後,session.setDefaultReadOnly(true)將session的readOnly屬性設置爲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);

在Session接口中,通過將readOnly屬性設置爲true,將不會對只讀實體進行髒檢查,也不會維護持久狀態的快照。此外,只讀實體的更改也不會持久化。

總而言之,這些是在 Hibernate 中使用@Transactional(readOnly = true)所得到的結果

  • 性能改進:只讀實體不進行髒檢查
  • 節省內存:不維護持久狀態的快照
  • 數據一致性:只讀實體的更改不會持久化
  • 當我們使用主從或讀寫副本集(或集羣)時,@Transactional(readOnly = true)使我們能夠連接到只讀數據庫

2、當我們使用 JPA 時,是否應該總是將@Transactional(readOnly = true)添加到服務層的只讀方法?有什麼取捨嗎?

我看到,當使用@Transactional(readOnly = true)時,我們可以有很多優勢。但是,將@Transactional(readOnly = true)添加到服務層的只讀方法是否合適?以下是我擔心的事情

  1. 無限制地使用事務可能會導致數據庫死鎖、性能和吞吐量下降。
  2. 由於一個事務佔用一個DB連接,所以@Transactional(readOnly = true)添加到Service層的方法可能會導致DB連接飢餓。

推薦一個開源免費的 Spring Boot 實戰項目:

https://github.com/javastacks/spring-boot-best-practice

第一個問題很難重現,所以我做了一些測試來檢查第二個問題。

@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;
}

我在服務層測試了兩個方法,一個是@Transactional(readOnly = true),另一個是存儲庫層中的@Transactional (readOnly = true)(在 SimpleJpaRepository 中,它是 Jpa Respitory 的默認實現,在類的頂部有@Transformational(ready Only),因此 findAll()方法在默認情況下有@transactional(read only = True))。

我從DB中獲取userInfo並保持線程5秒鐘,然後檢查該方法何時釋放連接。

結果如下:

對於服務層方法中的@Transactional(readOnly = true)

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

對於存儲庫層方法中的@Transactional(readOnly = true)

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

正如您所看到的,@Transactional(readOnly = true)一旦查詢結果到達,存儲庫層就會釋放連接。

然而,@Transactional(readOnly = true)在服務層的方法中直到服務層的方法結束才釋放連接。

因此,當服務層的方法有需要大量時間的邏輯時要小心,因爲它可以長時間持有數據庫連接,這可能會導致數據庫連接匱乏。

3、回顧

很明顯,@Transactional(readOnly = true)有很多優點。

  • 性能改進:只讀實體不進行髒檢查
  • 節省內存:不維護持久狀態的快照
  • 數據一致性:只讀實體的更改不會持久化
  • 當我們使用主從或讀寫副本集(或集羣)時,@Transactional(readOnly = true)使我們能夠連接到只讀數據庫

但是,您還應該記住,@Transactional(readOnly = true)在服務層的方法中可能會導致數據庫死鎖、性能低下和數據庫連接匱乏!

當您需要將只讀查詢僅僅作爲一個事務執行時,請毫不猶豫選擇的在服務層的方法中使用@Transactional(readOnly = true),如果你的服務層的方法中有大量其他邏輯方法時,就要做取捨了!

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2022最新版)

2.勁爆!Java 協程要來了。。。

3.Spring Boot 2.x 教程,太全了!

4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這纔是優雅的方式!!

5.《Java開發手冊(嵩山版)》最新發布,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章