Spring事務管理:非常規指南 - marcobehler

banq發表於2019-10-30

您可以使用本指南對Spring的事務管理(包括@Transactional批註)的工作方式進行深入的實際瞭解。

唯一的前提條件?您需要對ACID有一個大概的瞭解,即什麼是資料庫事務以及為什麼要使用它們。此外,儘管仍然適用於Spring的一般原則,但這裡也不介紹XATransactions或ReactiveTransactions。

Spring官方文件相反,本指南不會將您與諸如物理或邏輯事務之類的術語混淆。

相反,您將以非常規的方式學習Spring事務管理:從頭開始,逐步進行。

開始,提交和回滾事務

無論您使用的是Spring的@Transactional批註,純Hibernate,jOOQ還是任何其他資料庫庫,都沒有關係。最後,它們都在開啟和關閉資料庫事務中做同樣的事情,就是:

Connection connection = dataSource.getConnection(); // (1)
try (connection) {
    connection.setAutoCommit(false); // (2)
    // execute some SQL statements...
    connection.commit(); // (3)
} catch (SQLException e) {
    connection.rollback(); // (4)
}

  1. 您顯然需要與資料庫的連線。儘管在大多數企業級應用程式中,您將配置一個DataSource並從中獲取連線,但DriverManager.getConnection(...)也可以正常工作。
  2. 這是在Java中啟動資料庫事務的唯一方法,即使該名稱聽起來有些奇怪。 AutoCommit(true)在其自己的事務中包裝每個SQL語句,而AutoCommit(false)相反:您是事務的主控者。
  3. 讓我們進行交易...
  4. 或回滾我們的更改(如果有例外)。

是的,只要您使用@Transactional批註,這4行就簡化了,Spring幫你做了這一切。在下一章中,您將瞭解其工作原理。HikariCP之類的連線池庫可能會根據配置自動為您切換自動提交模式。但這是另一個主題。

儲存點和隔離級別

如果您已經使用過Spring的@Transactional批註,那麼您可能會遇到以下情況:

@Transactional(propagation=TransactionDefinition.NESTED,
               isolation=TransactionDefinition.ISOLATION_READ_UNCOMMITTED)

稍後我們將介紹巢狀的Spring事務和隔離級別,但是再次幫助您瞭解這些引數到底歸結為以下JDBC程式碼:

connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED); // (1)

Savepoint savePoint = connection.setSavepoint(); // (2)
...
connection.rollback(savePoint);

  1. 這就是Spring如何在資料庫連線上設定隔離級別。不完全是火箭科學,是嗎?
  2. Spring中的巢狀事務實際上只是JDBC儲存點。如果您不知道儲存點是什麼,請檢視本教程。請注意,儲存點支援取決於您的JDBC驅動程式/資料庫。

Spring中的事務管理如何工作

現在您有了一個良好的JDBC事務基礎,讓我們看一下Spring。

舊版事務管理:XML

過去,當XML配置成為Spring專案的規範時,您還可以直接在XML中配置事務。除了幾個遺留的企業專案,您將再也找不到這種方法了,因為它已經被更簡單的@Transactional註釋所取代。

因此,我們將在本指南中跳過XML配置,但是可以快速瀏覽一下它的外觀(直接取自Spring官方文件):

<!-- the transactional advice (what 'happens'; see the <aop:advisor/> bean below) -->
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <!-- the transactional semantics... -->
        <tx:attributes>
            <!-- all methods starting with 'get' are read-only -->
            <tx:method name="get*" read-only="true"/>
            <!-- other methods use the default transaction settings (see below) -->
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

Spring使用AOP(面向方面​​的程式設計)來進行事務處理。您可以在Spring官方文件中瞭解有關AOP的更多資訊。

Spring的@Transactional註釋

現在讓我們看一下現代Spring事務管理通常是什麼樣的:

public class UserService {

    @Transactional
    public void registerUser(User user) {
     //...validate the user
     userDao.save(user);
    }
}

只要您在Spring配置上設定了@EnableTransactionManagement批註(以及配置了其他兩個Bean-稍後將進行更多說明),就可以使用@Transactional批註對方法進行批註,並確保您的方法在資料庫事務內執行。

“在資料庫事務內部執行”的真正含義是什麼?有了上一部分的知識,上面的程式碼直接轉換(簡化)為:

public class UserService {

    public void registerUser(User user) {
        Connection connection = dataSource.getConnection(); // (1)
        try (connection) {
            connection.setAutoCommit(false); // (1)
            //...validate the user
            userDao.save(user); // (2)
            connection.commit(); // (1)
        } catch (SQLException e) {
            connection.rollback(); // (1)
        }
    }
}

  1. 這僅僅是JDBC連線的標準開啟和關閉。請參閱上一節。這就是Spring的事務註釋自動為您完成的,而無需您編寫它。
  2. 這是您自己的程式碼,可以通過DAO儲存使用者。

這個示例可能看起來有些簡化,但是讓我們看一下Spring如何神奇地為您插入此連線/事務程式碼。

代理

Spring不能像我上面那樣真正重寫您的Java類來插入連線程式碼。您的registerUser()方法實際上只是呼叫userDao.save(user),無法即時更改它。

但是Spring有一個優勢。它的核心是一個IoC容器。它為您例項化一個UserService,並確保將該UserService自動連線到需要UserService的任何其他bean中。

現在,每當在bean上使用@Transactional時,Spring都會使用一個小技巧。它不僅例項化UserService,而且例項化該UserService的事務代理。讓我們在圖片中看到它。

Spring事務管理:非常規指南 - marcobehler

從該圖中可以看到,代理只有一項工作。

  • 開啟和關閉資料庫連線/事務。
  • 然後委託給真正的UserService。
  • 而其他的bean,例如UserRestController,將永遠不會知道它們正在與代理對話,而不是真實的對話。

快速考試 看看下面的原始碼,並告訴我Spring會自動構造什麼樣的UserService,假設它已標有@Transactional或具有@Transactional方法。

@Configuration
@EnableTransactionManagement
public static class MyAppConfig {

    @Bean
    public UserService userService() {  // (1)
        return new UserService();
    }
}

Spring在這裡構造了UserService類的動態代理,可以為您開啟和關閉資料庫事務。在Cglib庫的幫助下通過子類代理。還有其他構造代理的方法,但現在暫時不做介紹。

PlatformTransactionManager

現在僅缺少一條關鍵資訊。您的UserService會即時進行代理,並且代理會開啟併為您關閉連線/事務。

但這意味著,代理需要一個資料來源:要獲得連線,提交,回滾,關閉連線等。在Spring中,有一個花哨的名稱表示處理所有事務狀態的介面,稱為PlatformTransactionManager

有許多不同的實現,但是最簡單的實現是DataSourceTransactionManager,並且憑藉從簡單的Java章節中學到的知識,您將確切知道它的作用。首先,讓我們看一下Spring配置:

@Bean
public DataSource dataSource() {
    return null; // (1)
}

@Bean
public PlatformTransactionManager txManager() {
    return new DataSourceTransactionManager(dataSource()); // (2)
}

  1. 在此處返回null顯然沒有意義,但為簡潔起見放在此處。您可建立MySQL,Postgres,Oracle或連線池資料來源。
  2. 您在此處建立一個新的TxManager,它將獲取您的DataSource。

因此,讓我們從上面擴充套件圖片:

Spring事務管理:非常規指南 - marcobehler

總結一下:

  1. 如果Spring在bean上檢測到@Transactional,它將建立該bean的動態代理。
  2. 代理有權訪問TransactionManager,並要求其開啟和關閉事務/連線。
  3. TransactionManager本身將簡單地執行您在普通Java部分中所做的事情:“操縱” JDBC連線。

@Transactional深度挖掘

現在,有兩個有趣的用例,涉及到Spring的事務性註釋。

讓我們看一下“物理”與“邏輯”事務。

想象以下兩個事務類。

@Service
public class UserService {

    @Autowired
    private InvoiceService invoiceService;

    @Transactional
    public void invoice() {
        invoiceService.createPdf();
        // send invoice as email, etc.
    }
}

@Service
public class InvoiceService {

    @Transactional
    public void createPdf() {
        // ...
    }
}

UserService具有事務性invoice()方法。它將呼叫另一個事務方法InvoiceService上的createPdf()。

現在就資料庫事務而言,這實際上應該僅僅是一個資料庫事務。(請記住:getConnection().setAutocommit(false).commit())Spring呼叫了此物理事務,即使這聽起來有些混亂。

從Spring的角度來看,事務有兩個邏輯部分:第一個在UserService中,另一個在InvoiceService中。Spring必須足夠聰明,才能知道兩個@Transactional方法都應使用相同的基礎資料庫事務。

在InvoiceService進行以下更改之後,情況會有什麼不同?

@Service
public class InvoiceService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void createPdf() {
        // ...
    }
}

這意味著您的程式碼將開啟與資料庫的兩個(物理)連線/事務。Spring現在足夠聰明,兩個邏輯事務塊(invoice()/ createPdf())現在也對映到兩個不同的資料庫事務。

傳播方式

檢視Spring原始碼時,您會發現各種傳播模式,可以將它們插入@Transactional方法中。

  • REQUIRED(0)
  • SUPPORTS(1)
  • MANDATORY(2)
  • REQUIRES_NEW(3)
  • NOT_SUPPORTED(4)
  • NEVER(5)
  • NESTED(6)

解釋:

  • REQUIRED(預設):我的方法需要一個事務,要麼為我開啟一個事務,要麼使用現有的事務:getConnection().setAutocommit(false)commit()。
  • SUPPORTS:我並不真正在乎事務是否開啟,我可以以任何一種方式工作:與JDBC無關
  • MANDATORY強制性的:我不會自己開啟一個事務,但是如果沒有人開啟一個事務,我會哭泣,與JDBC無關
  • Require_new:我要我完全擁有的事務: getConnection().setAutocommit(false)commit()。
  • Not_Supported:我真的不喜歡事務,我會嘗試掛起當前正在執行的事務,與JDBC無關
  • NEVER:如果其他人啟動了事務,我會哭泣→與JDBC無關
  • NESTED:聽起來很複雜,但實際上我們只是在談論儲存點!: connection.setSavepoint()

如您所見,大多數傳播模式實際上與資料庫或JDBC無關,而與您如何使用Spring構建程式的方式以及期望交易的方式/時間/位置有關。

隔離等級

當您像這樣配置@Transactional批註時會發生什麼:

@Transactional(isolation = Isolation.REPEATABLE_READ)

導致以下結果:

connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);

您可以在此處閱讀有關隔離級別的更多資訊但建議您在使用隔離級別甚至在事務中切換隔離級別時,必須確保諮詢JDBC驅動程式/資料庫,以瞭解受支援的內容和不支援的內容。

最常見的@Transactional陷阱

Spring初學者通常會遇到一個陷阱。看下面的程式碼:

@Service
public class UserService {

    @Transactional
    public void invoice() {
        createPdf();
        // send invoice as email, etc.
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void createPdf() {
        // ...
    }
}

您有一個帶有事務invoice()方法的UserService類。呼叫createPDF(),這也是事務性的。

一旦有人呼叫invoice(),您覺得會開啟幾各實際事務?

答案不是兩個,而是一個。為什麼?

讓我們回到本指南的代理部分。Spring為您注入了該事務代理,但是一旦您進入UserService類並呼叫其他內部方法,就不再涉及代理:沒有新事務可供您使用。

讓我們看一下圖片:

Spring事務管理:非常規指南 - marcobehler有一些技巧(例如self-injection),您可以用來解決此限制。但是主要的收穫是:始終牢記代理事務邊界。

Spring + Hibernate事務管理如何工作

在某些時候,您將希望您的Spring應用程式與另一個資料庫庫整合,例如HibernateJooq等。讓我們以Hibernate為例。

假設您有一個@Transactional Spring方法,並將其與DataSourcePlatformTransactionManager一起使用,就像上一節中討論的那樣。因此,Spring將為您開啟和關閉該DataSource上的連線。

但是,如果您的程式碼呼叫了Hibernate,則Hibernate本身將最終呼叫其自己的SessionFactory來建立和關閉新會話(〜=連線)並在沒有 Spring 的情況下管理它們的狀態。因此,Hibernate不會知道任何現有的Spring事務。

有一個簡單的解決方案(針對終端使用者):您將在HibernateTransactionManagerJpaTransactionManager中使用,而不是在Spring配置中使用DataSourcePlatformTransactionManager

專門的HibernateTransactionManager將確保:

  1. 通過Hibernate(即SessionFactory)開啟/關閉連線/事務
  2. 足夠聰明,可以讓您在非休眠狀態(即純JDBC程式碼)中使用相同的連線/事務

與往常一樣,圖片可能更容易理解(不過請注意,代理和真實服務之間的流在概念上只是正確的,而且過於簡化)。

Spring事務管理:非常規指南 - marcobehler

簡而言之,就是如何整合Spring和Hibernate。對於其他整合或更深入的瞭解,有助於快速檢視Spring提供的所有可能的PlatformTransactionManager實現。

 

相關文章