Spring的事務管理多執行緒的困惑
阿新 • • 發佈:2019-01-10
由於Spring的事務管理器是通過執行緒相關的ThreadLocal來儲存資料訪問基礎設施(也即Connection例項),再結合IoC和AOP實現高階宣告式事務的功能,所以Spring的事務天然地和執行緒有著千絲萬縷的聯絡。
我們知道Web容器本身就是多執行緒的,Web容器為一個HTTP請求建立一個獨立的執行緒(實際上大多數Web容器採用共享執行緒池),所以由此請求所牽涉到的Spring容器中的Bean也是運行於多執行緒的環境下。在絕大多數情況下,Spring的Bean都是單例項的(singleton),單例項Bean的最大好處是執行緒無關性,不存在多執行緒併發訪問的問題,也就是執行緒安全的。
一個類能夠以單例項的方式執行的前提是“無狀態”:即一個類不能擁有狀態化的成員變數。我們知道,在傳統的程式設計中,DAO必須持有一個Connection,而Connection即是狀態化的物件。所以傳統的DAO不能做成單例項的,每次要用時都必須建立一個新的例項。傳統的Service由於內部包含了若干個有狀態的DAO成員變數,所以其本身也是有狀態的。
但是在Spring中,DAO和Service都以單例項的方式存在。Spring是通過ThreadLocal將有狀態的變數(如Connection等)本地執行緒化,達到另一個層面上的“執行緒無關”,從而實現執行緒安全。Spring不遺餘力地將有狀態的物件無狀態化,就是要達到單例項化Bean的目的。
由於Spring已經通過ThreadLocal的設施將Bean無狀態化,所以Spring中單例項Bean對執行緒安全問題擁有了一種天生的免疫能力。不但單例項的Service可以成功運行於多執行緒環境中,Service本身還可以自由地啟動獨立執行緒以執行其他的Service。
啟動獨立執行緒呼叫事務方法
Java程式碼
將日誌級別設定為DEBUG,執行UserService#logon()方法,觀察以下輸出日誌:
引用 before userService.logon method...
//①建立一個事務
Creating new transaction with name [com.baobaotao.multithread.UserService.logon]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
Acquired Connection [jdbc:mysql://localhost:3306/sampledb, [email protected], MySQL-AB JDBC Driver] for JDBC transaction
…
SQL update affected 1 rows
after userService.updateLastLogonTime method...
Initiating transaction commit
//②提交①處開啟的事務
Committing JDBC transaction on Connection [jdbc:mysql://localhost:3306/sampledb, [email protected], MySQL-AB JDBC Driver]
Releasing JDBC Connection [jdbc:mysql://localhost:3306/sampledb, [email protected], MySQL-AB JDBC Driver] after transaction
…
Returning JDBC Connection to DataSource
before scoreService.addScor method...
//③建立一個事務
Creating new transaction with name [com.baobaotao.multithread.ScoreService.addScore]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
Acquired Connection [jdbc:mysql://localhost:3306/sampledb, [email protected], MySQL-AB JDBC Driver] for JDBC transaction
…
SQL update affected 0 rows
Initiating transaction commit
//④提交③處開啟的事務
Committing JDBC transaction on Connection [jdbc:mysql://localhost:3306/sampledb, [email protected], MySQL-AB JDBC Driver]
Releasing JDBC Connection [jdbc:mysql://localhost:3306/sampledb, [email protected], MySQL-AB JDBC Driver] after transaction
Returning JDBC Connection to DataSource
after scoreService.addScor method...
在①處,在主執行緒(main)執行的UserService#logon()方法的事務啟動,在②處,其對應的事務提交。而在子執行緒(Thread-2)執行的ScoreService#addScore()方法的事務在③處啟動,在④處對應的事務提交。
所以,我們可以得出這樣的結論:在相同執行緒中進行相互巢狀呼叫的事務方法工作於相同的事務中。如果這些相互巢狀呼叫的方法工作在不同的執行緒中,則不同執行緒下的事務方法工作在獨立的事務中。
我們知道Web容器本身就是多執行緒的,Web容器為一個HTTP請求建立一個獨立的執行緒(實際上大多數Web容器採用共享執行緒池),所以由此請求所牽涉到的Spring容器中的Bean也是運行於多執行緒的環境下。在絕大多數情況下,Spring的Bean都是單例項的(singleton),單例項Bean的最大好處是執行緒無關性,不存在多執行緒併發訪問的問題,也就是執行緒安全的。
一個類能夠以單例項的方式執行的前提是“無狀態”:即一個類不能擁有狀態化的成員變數。我們知道,在傳統的程式設計中,DAO必須持有一個Connection,而Connection即是狀態化的物件。所以傳統的DAO不能做成單例項的,每次要用時都必須建立一個新的例項。傳統的Service由於內部包含了若干個有狀態的DAO成員變數,所以其本身也是有狀態的。
但是在Spring中,DAO和Service都以單例項的方式存在。Spring是通過ThreadLocal將有狀態的變數(如Connection等)本地執行緒化,達到另一個層面上的“執行緒無關”,從而實現執行緒安全。Spring不遺餘力地將有狀態的物件無狀態化,就是要達到單例項化Bean的目的。
由於Spring已經通過ThreadLocal的設施將Bean無狀態化,所以Spring中單例項Bean對執行緒安全問題擁有了一種天生的免疫能力。不但單例項的Service可以成功運行於多執行緒環境中,Service本身還可以自由地啟動獨立執行緒以執行其他的Service。
啟動獨立執行緒呼叫事務方法
Java程式碼
- package com.baobaotao.multithread;
- import org.springframework.beans.factory.annotation.Autowired;
-
import org.springframework.jdbc.core.JdbcTemplate;
- import org.springframework.context.ApplicationContext;
- import org.springframework.context.support.ClassPathXmlApplicationContext;
- import org.springframework.stereotype.Service;
- import org.apache.commons.dbcp.BasicDataSource;
- @Service("userService")
- public class UserService extends BaseService {
- @Autowired
- private JdbcTemplate jdbcTemplate;
- @Autowired
- private ScoreService scoreService;
- public void logon(String userName) {
- System.out.println("before userService.updateLastLogonTime method...");
- updateLastLogonTime(userName);
- System.out.println("after userService.updateLastLogonTime method...");
- //scoreService.addScore(userName, 20);//①在同一執行緒中呼叫scoreService#addScore()
- //②在一個新執行緒中執行scoreService#addScore()
- Thread myThread = new MyThread(this.scoreService, userName, 20);//使用一個新執行緒執行
- myThread.start();
- }
- public void updateLastLogonTime(String userName) {
- String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?";
- jdbcTemplate.update(sql, System.currentTimeMillis(), userName);
- }
- //③負責執行scoreService#addScore()的執行緒類
- private class MyThread extends Thread {
- private ScoreService scoreService;
- private String userName;
- private int toAdd;
- private MyThread(ScoreService scoreService, String userName, int toAdd) {
- this.scoreService = scoreService;
- this.userName = userName;
- this.toAdd = toAdd;
- }
- public void run() {
- try {
- Thread.sleep(2000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("before scoreService.addScor method...");
- scoreService.addScore(userName, toAdd);
- System.out.println("after scoreService.addScor method...");
- }
- }
- }
將日誌級別設定為DEBUG,執行UserService#logon()方法,觀察以下輸出日誌:
引用 before userService.logon method...
//①建立一個事務
Creating new transaction with name [com.baobaotao.multithread.UserService.logon]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
Acquired Connection [jdbc:mysql://localhost:3306/sampledb, [email protected], MySQL-AB JDBC Driver] for JDBC transaction
…
SQL update affected 1 rows
after userService.updateLastLogonTime method...
Initiating transaction commit
//②提交①處開啟的事務
Committing JDBC transaction on Connection [jdbc:mysql://localhost:3306/sampledb, [email protected], MySQL-AB JDBC Driver]
Releasing JDBC Connection [jdbc:mysql://localhost:3306/sampledb, [email protected], MySQL-AB JDBC Driver] after transaction
…
Returning JDBC Connection to DataSource
before scoreService.addScor method...
//③建立一個事務
Creating new transaction with name [com.baobaotao.multithread.ScoreService.addScore]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
Acquired Connection [jdbc:mysql://localhost:3306/sampledb, [email protected], MySQL-AB JDBC Driver] for JDBC transaction
…
SQL update affected 0 rows
Initiating transaction commit
//④提交③處開啟的事務
Committing JDBC transaction on Connection [jdbc:mysql://localhost:3306/sampledb, [email protected], MySQL-AB JDBC Driver]
Releasing JDBC Connection [jdbc:mysql://localhost:3306/sampledb, [email protected], MySQL-AB JDBC Driver] after transaction
Returning JDBC Connection to DataSource
after scoreService.addScor method...
在①處,在主執行緒(main)執行的UserService#logon()方法的事務啟動,在②處,其對應的事務提交。而在子執行緒(Thread-2)執行的ScoreService#addScore()方法的事務在③處啟動,在④處對應的事務提交。
所以,我們可以得出這樣的結論:在相同執行緒中進行相互巢狀呼叫的事務方法工作於相同的事務中。如果這些相互巢狀呼叫的方法工作在不同的執行緒中,則不同執行緒下的事務方法工作在獨立的事務中。