不管你現在使用的是那一種ORM開發框架,只要你的核心是JDBC,那麼所有的事務處理都是圍繞著JDBC開展的,而JDBC之中的事務控制是由Connection介面提供的方法:
- 1、關閉自動事務提交:connection.setAutoCommit(false);
- 2、事務手工提交: connection.commit();
- 3、事務回滾: connection.rollback();
在程式的開發之中事務的使用是存在有
前提的
:如果某一個業務現在需要同時執行若干條資料更新處理操作,這個時候才會使用到事務控制,除此之外是不需要強制性處理的。
按照傳統的事務控制處理方法來講一般都是在業務層進行處理的,而在之前分析過了如何基於AOP 設計思想採用
動態代理
設計模式實現的事務處理模型,這種操作可以在不侵入業務程式碼的情況下進行事務的控制,但是程式碼的實現過程實在是繁瑣,現在既然都有了AOP處理模型
了,所以對於事務的控制就必須有一個完整的加強。
ACID主要指的是事務的四種特點:原子性(Atomicity)、一致性(Consistency)、隔離性或獨立性(lsolation)、永續性(Durabilily)
四個特徵:
- 原子性(Atomicity):整個事務中的所有操作,要麼全部完成,要麼全部不完成,不可能停滯在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣;
- 一致性(Consistency):一個事務可以封裝狀態改變(除非它是一個唯讀的)。事務必須始終保持系統處於一致的狀態,不管在任何給定的時間並行事務有多少;
- 隔離性(lsolation):隔離狀態執行事務,使它們好像是系統在給定時間內執行的唯一操作。如果有兩個事務,執行在相同的時間內,執行相同的功能,事務的隔離性將確保每一事務在系統中認為只有該事務在使用系統;
- 永續性(Durability):在事務完成以後,該事務對資料庫所作的更改便持久的儲存在資料庫之中,並不會被
回滾
。
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事務的整體架構。
由於現在很少用到這種程式設計式事務了,導致很多初學者根本不知道這其中是怎麼設定的。其實萬變不離其宗,都是基於JDBC的事務控制。
- 使用步驟
- 設定事務-》 是資料來源
- 編寫程式碼 控制事務
資料來源使用的是文章開始前的SpringJDBC的環境 地址
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是為響應式程式設計做的準備。
@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);
}
}
注意:執行先,需要先將資料庫表清空,能更好的觀察執行結果。
- 而後會發現,雖然我們的程式執行出現異常了,但資料庫沒有資料。
- 說明我們設定的事務生效了,使其出現異常,回滾了。
如果現在僅僅是使用了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);
}
}
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進行查詢時,本應是我們更新後的資料,這才對呀。所以這個事務出現了事務不同步的問題。
為了保證並行狀態下的資料讀取的正確性,就需要通過事務的隔離級別來進行控制,實際上控制的就是髒讀、幻讀以及不可重複讀的問題了。
髒讀(Dirty reads):事務A在讀取資料時,讀取到了事務B未提交的資料,由於事務B有可能被回滾,所以該資料有可能是一個無效資料
不可重複讀(Non-repeatable Reads):事務A對一個資料的兩次讀取返回了不同的資料內容,有可能在兩次讀取之間事務B對該資料進行了修改,一般此類操作出現在資料修改操作之中;
幻讀(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)
事務開發是和業務層有直接聯絡的,在進行開發的過程之中,很難出現業務層之間不互相呼叫的場景,例如:存在有一個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:如果當前存在父級事務,則當前子業務中的事務會自動成為該父級事務中的一個子事務,只有在父級事務提交後才會提交子事務。如果子事務產生異常則可以交由父級呼叫進行例外處理,如果父級事務產生異常,則其也會回滾。