一文搞定 Spring事務
Spring 事務
1、JDBC事務控制
不管你現在使用的是那一種ORM開發框架,只要你的核心是JDBC,那麼所有的事務處理都是圍繞着JDBC開展的,而JDBC之中的事務控制是由Connection接口提供的方法:
- 1、關閉自動事務提交:connection.setAutoCommit(false);
- 2、事務手工提交: connection.commit();
- 3、事務回滾: connection.rollback();
在程序的開發之中事務的使用是存在有
前提的
:如果某一個業務現在需要同時執行若干條數據更新處理操作,這個時候才會使用到事務控制,除此之外是不需要強制性處理的。
按照傳統的事務控制處理方法來講一般都是在業務層進行處理的,而在之前分析過了如何基於AOP 設計思想採用
動態代理
設計模式實現的事務處理模型,這種操作可以在不侵入業務代碼的情況下進行事務的控制,但是代碼的實現過程實在是繁瑣,現在既然都有了AOP處理模型
了,所以對於事務的控制就必須有一個完整的加強。
1.1、ACID事務原則
ACID主要指的是事務的四種特點:原子性(Atomicity)、一致性(Consistency)、隔離性或獨立性(lsolation)、持久性(Durabilily)
四個特徵:
- 原子性(Atomicity):整個事務中的所有操作,要麼全部完成,要麼全部不完成,不可能停滯在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣;
- 一致性(Consistency):一個事務可以封裝狀態改變(除非它是一個只讀的)。事務必須始終保持系統處於一致的狀態,不管在任何給定的時間並發事務有多少;
- 隔離性(lsolation):隔離狀態執行事務,使它們好像是系統在給定時間內執行的唯一操作。如果有兩個事務,運行在相同的時間內,執行相同的功能,事務的隔離性將確保每一事務在系統中認為只有該事務在使用系統;
- 持久性(Durability):在事務完成以後,該事務對數據庫所作的更改便持久的保存在數據庫之中,並不會被
回滾
。
2、Spring事務架構
Spring事務是對已有JDBC事務的進一步的包裝型處理,所以底層依然是
JDBC事務控制
,而後在這之上進行了更加合理的二次開發與設計,首先先來看一下Spring 與JDBC事務之間的結構圖。
只要是說到了事務的開發,那麼就必須考慮到ORM組件的整合問題各類的ORM開發組件實在是太多了,同時Spring在設計的時候無法預知未來,那麼這個時候在
Spring 框架
裏面就針對於事務的接入提供了一個開發標準
。 Spring事務的核心實現關鍵是在於:PlatformTransactionManager
通過以上的代碼可以發現,PlatfrmTransactionManager接口存在有一個TransactionManager父接口,下面打開該接口的定義來觀察其具體功能。
在現代的開發過程之中,最為核心的事務接口主要使用的是PlatformTransactionManager(這也就是長久以來的習慣),在Spring最早出現聲明式事務的時候,就有了這個處理接口了。在進行獲取事務的時候可以發現getTransaction()方法內部需要接收有一個TransactionDefinition接口實例,這個接口主要定義了
Spring事務的超時時間
,以及Spring事務的傳播屬性
(是面試的關鍵所在),而在getTransaction()方法內部會返回有一個TransactionStatus接口實例,打開這個接口來觀察一下。.
public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {
boolean hasSavepoint(); // 是否存在hasSavepoint (事務保存點)
void flush(); // 事務刷新
}
而後該接口內部定義的時候又需要繼承TransactionExecution、SavepointManager(事務保存點管理器)、Flushable(事務刷新)三個父接口。下圖就是Spring事務的整體架構。
3、編程式事務控制
由於現在很少用到這種編程式事務了,導致很多初學者根本不知道這其中是怎麼配置的。其實萬變不離其宗,都是基於JDBC的事務控制。
- 使用步驟
- 配置事務-》 是數據源
- 編寫代碼 控制事務
3.1如何使用
數據源使用的是文章開始前的SpringJDBC的環境 地址
1 配置事務
public class TransactionConfig {
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
// PlatformTransactionManager 類似於一個事務定義的標準
// DataSource 也是一個標準 規範數據源
DataSourceTransactionManager transactionManager =
new DataSourceTransactionManager(dataSource);
// transactionManager.setDataSource(dataSource); 二選一即可
return transactionManager;
}
}
面試題: PlatformTransactionManager 與 TransactionManager兩者區別?
TransactionManager是後爹,是屬於PlatformTransactionManager父接口,但是現在不要輕易使用,因為很多的傳統的Spring開發項目還是使用的是PlatformTransactionManager。TransactionManager是為響應式編程做的準備。
2 編寫代碼
@Test
public void testInsert() {
String sql = "insert into yootk.book(title,author,price) values(?,?,?)";
LOGGER.info("【插入執行結果】:{}", jdbcTemplate.update(sql, "Python入門", "李老師", 99.90));
LOGGER.info("【插入執行結果】:{}", jdbcTemplate.update(sql, "Java入門", null, 99.90));
LOGGER.info("【插入執行結果】:{}", jdbcTemplate.update(sql, "Js入門", "李老師", null));
}
執行代碼後會發現,出現異常提示信息。由於我們的表中配置了not null,所以在插入是會出現異常。這也就是我們異常信息的來源。
- 由於出現了異常,可是,數據還是插入到數據庫,在正常開發中是不允許這樣的情況發現的,那麼該如何解決呢。還記得上面配置的事務信息嗎。修改測試類如下:
@Test
public void testInsert() {
String sql = "insert into yootk.book(title,author,price) values(?,?,?)";
TransactionStatus status = transactionManager.getTransaction( //開啟事務
new DefaultTransactionAttribute()); // 默認事務屬性
try {
LOGGER.info("【插入執行結果】:{}", jdbcTemplate.update(sql, "Python入門", "李老師", 99.90));
LOGGER.info("【插入執行結果】:{}", jdbcTemplate.update(sql, "Java入門", null, 99.90));
LOGGER.info("【插入執行結果】:{}", jdbcTemplate.update(sql, "Js入門", "李老師", null));
transactionManager.commit(status); // 提交
} catch (DataAccessException e) {
transactionManager.rollback(status); // 回滾
throw new RuntimeException(e);
}
}
注意:執行先,需要先將數據庫表清空,能更好的觀察執行結果。
- 而後會發現,雖然我們的程序執行出現異常了,但數據庫沒有數據。
- 說明我們配置的事務生效了,使其出現異常,回滾了。
3.2TransactionStatus
如果現在僅僅是使用了TransactionManager提交和回滾的處理方法,僅僅是Spring提供的事務處理的皮毛所在,而如果要想深入的理解事務處理的特點,那麼就需要分析其每一個核心的組成類,首先分析的就是TransactionStatus。
在開啟事務的時候會返回有一個TransactionStatus接口實例,而後在提交或回滾事務的時候都需要針對於指定的status實例進行處理,首先來打開這個接口的定義關聯結構。
DefaultTransactionStatus是TransactionStatus默認實現的子類而後該類並不是直接實例化的,而是通過事務管理器負責實例化處理的,status所得到的是一個事務的處理標記,而後Spring依照此標記管理事務。
現我們有以下業務,在業務執行過程中,有一部分業務執行失敗,正常來說,是執行回滾操作,但是現在我們要讓某一個位置之前的執行的sql不回滾。那麼這個功能如何實現呢?
這裡就需要用到我們事務的保存點:
@Test
public void testInsertSavePoint() { // 測試事務的保存點
String sql = "insert into yootk.book(title,author,price) values(?,?,?)";
TransactionStatus status = transactionManager.getTransaction( // 開啟事務
new DefaultTransactionAttribute()); // 默認事務屬性
Object savepointA = null; //保存點
try {
LOGGER.info("【插入執行結果】:{}", jdbcTemplate.update(sql, "Python入門", "李老師", 99.90));
savepointA = status.createSavepoint(); // 創建保存點
LOGGER.info("【插入執行結果】:{}", jdbcTemplate.update(sql, "Java入門", null, 99.90));
transactionManager.commit(status); // 正常執行 事務提交
} catch (DataAccessException e) {
// 出現異常 先回滾到保存點 然後在提交保存點之前的事務
status.releaseSavepoint(savepointA); // 回滾到保存點
transactionManager.commit(status); // 提交
throw new RuntimeException(e);
}
}
4、Spring事務隔離級別
Spring面試之中隔離級別的面試問題是最為常見的,也是一個核心的基礎所在,但是所謂的隔離級別一定要記住,是在
並發環境
訪問下才會存在的問題。數據庫是一個項目應用中的公共存儲資源,所以在實際的項目開發過程中,很有可能會有兩個不同的線程(每個線程擁有各自的數據庫事務),要進行同一條數據的讀取以及更新操作。
下面就通過代碼的形式 一步步的揭開他的廬山真面目。
- 對於事務,
private class BookRowMapper implements RowMapper<Book> { // 對象映射關係
@Override
public Book mapRow(ResultSet rs, int rowNum) throws SQLException {
Book book = new Book();
book.setBid(rs.getInt(1));
book.setTitle(rs.getString(2));
book.setAuthor(rs.getString(3));
book.setPrice(rs.getDouble(4));
return book;
}
}
@Test
public void testInsertIsolation() throws InterruptedException { // 測試事務的隔離級別
String query = "select bid,title,author,price from yootk.book where bid = ?"; // 查詢
String update = "update yootk.book set title = ?, author =? where bid =?"; // 根據id修改
BookRowMapper bookRowMapper = new BookRowMapper(); // 對Book對象的映射
DefaultTransactionDefinition definition =
new DefaultTransactionDefinition(); // 創建默認事務對象
Thread threadA = new Thread(() -> {
TransactionStatus statusA = this.transactionManager.getTransaction(definition); //開始事務
Book book = this.jdbcTemplate.queryForObject(query, bookRowMapper, 1); // 查詢bid = 1的數據
String name = Thread.currentThread().getName();// 獲取線程名稱
System.out.println(11111 + "??????");
LOGGER.info("{}【查詢結果】:{}", name, book);
try {
TimeUnit.SECONDS.sleep(2); //等待兩秒 讓線程B修改之後再查詢
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
book = jdbcTemplate.queryForObject(query, bookRowMapper, 1); // 再次查詢
LOGGER.info("{}【查詢結果】:{}", name, book);
}, "事務線程-A");
Thread threadB = new Thread(() -> {
TransactionStatus statusB =
transactionManager.getTransaction(definition); // 開啟事務
String name = Thread.currentThread().getName();// 獲取線程名稱
int i = 0;
try {
i = jdbcTemplate.update(update, "Netty", "李老師", 1);
LOGGER.info("{} 執行結果:{}", name, i);
transactionManager.commit(statusB); // 提交事務
} catch (DataAccessException e) {
transactionManager.rollback(statusB); // 回滾事務
throw new RuntimeException(e);
}
}, "事務線程-B");
threadB.start();// 啟動線程
threadA.start();
threadA.join();// 等待相互執行完成
threadB.join();
}
執行結果
事務線程-A【查詢結果】:Book(bid=1, title=Netty, author=李老師, price=99.9)
事務線程-B 執行結果:1
事務線程-A【查詢結果】:Book(bid=1, title=Netty, author=李老師, price=99.9)
查看執行結果可知,我們線程B執行的是更新操作,但是更新成功後,在事務A進行查詢時,本應是我們更新後的數據,這才對呀。所以這個事務出現了事務不同步的問題。
為了保證並髮狀態下的數據讀取的正確性,就需要通過事務的隔離級別來進行控制,實際上控制的就是臟讀、幻讀以及不可重複讀的問題了。
4.1、臟讀
臟讀(Dirty reads):事務A在讀取數據時,讀取到了事務B未提交的數據,由於事務B有可能被回滾,所以該數據有可能是一個無效數據
4.2、不可重複讀
不可重複讀(Non-repeatable Reads):事務A對一個數據的兩次讀取返回了不同的數據內容,有可能在兩次讀取之間事務B對該數據進行了修改,一般此類操作出現在數據修改操作之中;
4.3、幻讀
幻讀(Phantom Reads):事務A在進行數據兩次查詢時產生了不一致的結果,有可能是事務B在事務A第二次查詢之前增加或刪除了數據內容所造成的.
Spring最大的優勢是在於將所有的配置過程都進行了標準化的定義,於是在TransactionDefintion接口裏面就提供了數據庫隔離級別的定義常量。
從正常的設計角度來講,在進行Spring事務控制的時候,不要輕易的去隨意修改隔離級別(需要記住這幾個隔離級別的概念),因為一般都使用默認的隔離級別,由數據庫自己來實現的控制。
【MySQL數據庫】查看MySQL數據庫之中的默認隔離級別
SHOW VARIABLES LIKE 'transaction_isolation';
舉個栗子,來看看隔離級別的作用吧
修改testInsertIsolation測試類
definition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);// 設置事務隔離級別為:讀已提交
執行結果:
因為我們線程B在修改後,就提交了,而我們設置的隔離級別是讀已提交,所以能讀到已提交的數據
事務線程-A【查詢結果】:Book(bid=1, title=Java入門到入土, author=李老師, price=99.9)
事務線程-B 執行結果:1
事務線程-A【查詢結果】:Book(bid=1, title=Netty, author=李老師, price=99.9)
5、Spring事務傳播機制
事務開發是和業務層有直接聯繫的,在進行開發的過程之中,很難出現業務層之間不互相調用的場景,例如:存在有一個A業務處理,但是A業務在處理的時候有可能會調用B業務,那麼如果此時A和B之間各自都存在有事務的機制,那麼這個時候就需要進行事務有效的傳播管理。
1、TransactionDefinition.PROPAGATION_REQUIRED:默認事務隔離級別,子業務直接支持當前父級事務,如果當前父業務之中沒有事務,則創建一個新的事務,如果當前父業務之中存在有事務,則合併為一個完整的事務。簡化的理解:不管任何的時候,只要進行了業務的調用,都需要創建出一個新的事務,這種機制是最為常用的事務傳播機制的配置。
2、TransactionDefinition.PROPAGATION_SUPPORTS:如果當前父業務存事務,則加入該父級事務。如果當前不存在有父級事務,則以非事務方式運行;
3、TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事務的方式運行,如果當前存在有父級事務,則先自動掛起父級事務後運行;
4、TransactionDefinition.PROPAGATION_MANDATORY:如果當前存在父級事務,則運行在父級事務之中,如果當前無事務則拋出異常(必須存在有父級事務);
5、TransactionDefinition.PROPAGATION_REQUIRES_NEW:建立一個新的子業務事務,如果存在有父級事務則會自動將其掛起,該操作可以實現子事務的獨立提交,不受調用者的事務影響,即便父級事務異常,也可以正常提交;
6、TransactionDefinition.PROPAGATION_NEVER:以非事務的方式運行,如果當前存在有事務則拋出異常;
7、TransactionDefinition.PROPAGATION_NESTED:如果當前存在父級事務,則當前子業務中的事務會自動成為該父級事務中的一個子事務,只有在父級事務提交後才會提交子事務。如果子事務產生異常則可以交由父級調用進行異常處理,如果父級事務產生異常,則其也會回滾。