Spring系列.事務管理
阿新 • • 發佈:2020-06-18
Spring提供了一致的事務管理抽象。這個抽象是Spring最重要的抽象之一, 它有如下的優點:
- 為不同的事務API提供一致的程式設計模型,如JTA、JDBC、Hibernate和MyBatis資料庫層 等;
- 提供比大多數事務API更簡單的,易於使用的程式設計式事務管理API;
- 完美整合Spring資料訪問抽象;
- 支援Spring宣告式事務管理;
這篇部落格就來介紹Spring事務管理相關的內容。
## 事務簡介
### 什麼是事務
> 事務(Transaction)一般是指對資料庫的一個或一組操作單元。
### 事務的作用
> 1、為資料庫操作提供了一個從失敗中恢復到正常狀態的方法,同時提供了資料庫即使在異常狀態下仍能保持一致性的方法。
> 2、當多個應用程式在併發訪問資料庫時,可以在這些應用程式之間提供一個隔離方法,以防止彼此的操作互相干擾。
當一個事務被提交給了DBMS(資料庫管理系統),則DBMS需要確保該事務中的所有操作都成功完成且其結果被永久儲存在資料庫中,如果事務中有的操作沒有成功完成,則事務中的所有操作都需要被回滾,回到事務執行前的狀態(要麼全執行,要麼全都不執行);同時,該事務對資料庫或者其他事務的執行無影響,所有的事務都好像在獨立的執行。
### 事務的特點
事務具有4個屬性:原子性、一致性、隔離性、永續性。這四個屬性通常稱為ACID特性。
> 原子性(Atomicity):事務作為一個整體被執行,包含在其中的對資料庫的操作要麼全部被執行,要麼都不執行。
>一致性(Consistency):事務應確保資料庫的狀態從一個一致狀態轉變為另一個一致狀態。一致狀態的含義是資料庫中的資料應滿足完整性約束。
>隔離性(Isolation):多個事務併發執行時,一個事務的執行不應影響其他事務的執行。
>永續性(Durability):一個事務一旦提交,他對資料庫的修改應該永久儲存在資料庫中。
### 事務的隔離級別
在多個事務併發操作的過程中,如果控制不好隔離級別,就有可能產生髒讀、不可重複讀或者幻讀等讀現象。資料操作過程中利用資料庫的鎖機制或者多版本併發控制機制獲取更高的隔離等級。但是,隨著資料庫隔離級別的提高,資料的併發能力也會有所下降。所以,如何在併發性和隔離性之間做一個很好的權衡就成了一個至關重要的問題。
ANSI/ISO SQL定義的標準隔離級別有四種,從高到底依次為:可序列化(Serializable)、可重複讀(Repeatable reads)、提交讀(Read committed)、未提交讀(Read uncommitted)。
1. **讀未提交**
未提交讀(READ UNCOMMITTED)是最低的隔離級別。通過名字我們就可以知道,在這種事務隔離級別下,一個事務可以讀到另外一個事務未提交的資料。**未提交讀會導致髒讀**
>事務在讀資料的時候並未對資料加鎖。
>務在修改資料的時候只對資料增加行級共享鎖。
2. 讀已提交
提交讀(READ COMMITTED)也可以翻譯成讀已提交,通過名字也可以分析出,在一個事務修改資料過程中,如果事務還沒提交,其他事務不能讀該資料。**讀已提交會導致不可重複讀**。
>事務對當前被讀取的資料加 行級共享鎖(當讀到時才加鎖),一旦讀完該行,立即釋放該行級共享鎖;
>事務在更新某資料的瞬間(就是發生更新的瞬間),必須先對其加 行級排他鎖,直到事務結束才釋放。
3. 可重複讀
可重複讀能保障一個事務在事務內讀到的某條資料是一致的。但是可重複讀不能解決幻讀的問題。就是在事務還沒結束時,其他事務又插入了一條新的資料。
>事務在讀取某資料的瞬間(就是開始讀取的瞬間),必須先對其加 行級共享鎖,直到事務結束才釋放;
>事務在更新某資料的瞬間(就是發生更新的瞬間),必須先對其加 行級排他鎖,直到事務結束才釋放
4. 序列化
可序列化(Serializable)是最高的隔離級別,前面提到的所有的隔離級別都無法解決的幻讀,在可序列化的隔離級別中可以解決。
>事務在讀取資料時,必須先對其加 表級共享鎖 ,直到事務結束才釋放;
>事務在更新資料時,必須先對其加 表級排他鎖 ,直到事務結束才釋放。
下面是髒讀、不可重複讀和幻讀的解釋。
> **髒讀**就是指當一個事務正在訪問資料,並且對資料進行了修改,而這種修改還沒有提交(commit)到資料庫中,這時,另外一個事務也訪問這個資料,然後使用了這個資料。因為這個資料是還沒有提交的資料,那麼另外一個事務讀到的這個資料是髒資料,依據髒資料所做的操作可能是不正確的。
> **不可重複讀**:在一個事務內,多次讀同一個資料。在這個事務還沒有結束時,另一個事務也訪問該同一資料。那麼,在第一個事務的兩次讀資料之間。由於第二個事務的修改,那麼第一個事務讀到的資料可能不一樣,這樣就發生了在一個事務內兩次讀到的資料是不一樣的,因此稱為不可重複讀,即原始讀取不可重複。
> **幻讀**指的是一個事務在前後兩次查詢同一個範圍的時候,後一次查詢看到了前一次查詢沒有看到的資料行。**幻讀專指“新插入的行”**是不可重複讀(Non-repeatable reads)的一種特殊場景
## Spring事務
### Spring事務模型的優勢
事務可以分為本地事務和全域性事務,這兩種事務都有一定程度的侷限性,Spring框架的事務管理支援解決全域性和本地事務模型的侷限性。
**1. 全域性事務**
全域性事務可以讓你跨多個事務進行工作,比如你的事務同事包含多個關係型資料庫,也可以包含關係型資料庫和JMS事務。一般情況下都是通過JTA來實現全域性事務,但是JTA一般需要具體的應用容器來支援,這就導致程式碼的通用性較低。
下面舉個全域性事務的列子,方便理解。
> 在電商網站上,在消費者點選購買按鈕後,交易後臺會進行庫存檢查、下單、減庫存、更新訂單狀態等一連串的服務呼叫,每一個操作對應一個獨立的服務,服務一般會有獨立的資料庫,因此會產生分散式事務問題。分散式事務就是一種比較常見的全域性事務。
**2. 本地事務**
本地事務和具體的某個事務關聯,比如說JDBC事務。本地事務比較簡單,但是不能實現分散式事務的功能。
Spring提供了統一方便的事務程式設計模型,可以解決上面本地事務和全域性事務的侷限。使用Spring的事務API進行事務管理,底層可以適應各種事務資源。
### Spring事務抽象
Spring為提供統一的事務程式設計模型,提供相關的介面。主要介面如下:
```java
public interface PlatformTransactionManager {
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
```
上面介面的`getTransaction`方法接收一個`TransactionDefinition`引數,返回一個`TransactionStatus` 值。其中`TransactionStatus` 可能代表一個新的事務,或者返回一個已經存在本次呼叫棧中的事務。(`TransactionStatus` 和具體的執行緒繫結。可以自己寫程式碼測試下)
`TransactionStatus`介面定義如下。
```java
public interface TransactionStatus extends SavepointManager {
boolean isNewTransaction();
boolean hasSavepoint();
void setRollbackOnly();
boolean isRollbackOnly();
void flush();
boolean isCompleted();
}
```
## 宣告式事務管理
使用Spring的事務管理,推薦使用宣告式事務管理。Spring的宣告式事務管理是通過Spring的AOP功能實現的。
![tx](https://docs.spring.io/spring/docs/5.2.6.RELEASE/spring-framework-reference/images/tx.png)
因為平時在開發過程中都是使用註解的方式使用宣告式事務。下面就介紹註解的方式。
step1:新增@EnableTransactionManagement註解
```java
@Configuration
@EnableTransactionManagement
@MapperScan("com.csx.demo.spring.boot.dao")
public class MyBatisConfig {
}
```
step2:新增@Transactional註解到介面的實現。
```java
@Service
@Transactional(readOnly = true,rollbackFor = Exception.class)
public class SysUserServiceImpl implements SysUserService {
@Autowired
private SysUserMapper sysUserMapper;
@Override
@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
public int saveSysUser(SysUser user) {
int i = sysUserMapper.insert(user);
return i;
}
}
```
使用Spring的宣告式事務就這麼簡單。
> **當你使用Spring的AOP方式來使用事務的話,你新增@Transactional註解的方法一定要是`public`的,不然事務不會生效。**
>
> 假如你需要讓非`public`的方法生效,你需要使用AspectJ 的AOP實現。(說明:Spring的AOP功能有兩種實現方式,一種是Spring自己實現的AOP功能,主要是通過JDK動態代理或者CGLIB動態代理實現的。還有一種方式是整合AspectJ 這個第三方AOP框架實現的)
另外,@Transactional註解可以新增到介面、介面中的方法定義、類和類裡面的方法。**Spring團隊建議將註解加到具體的類和方法實現上,而不是加到介面定義上(原因見下面英文描述)。**當然,您可以將@Transactional註釋放在介面(或介面方法)上,但是隻有在使用基於介面的代理時,才會像您期望的那樣工作。
> The fact that Java annotations are not inherited from interfaces means that, if you use class-based proxies (`proxy-target-class="true"`) or the weaving-based aspect (`mode="aspectj"`), the transaction settings are not recognized by the proxying and weaving infrastructure, and the object is not wrapped in a transactional proxy.
### @Transactional註解的配置
@Transactional註解可以進行以下配置。
| Property | Type | Description |
| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| [value](https://docs.spring.io/spring/docs/5.2.6.RELEASE/spring-framework-reference/data-access.html#tx-multiple-tx-mgrs-with-attransactional) | `String` | 一個專案中可以存在多個事務管理器,這個值用於指定具體使用哪個事務管理器。 |
| [propagation](https://docs.spring.io/spring/docs/5.2.6.RELEASE/spring-framework-reference/data-access.html#tx-propagation) | `enum`: `Propagation` | 設定傳播機制 |
| `isolation` | `enum`: `Isolation` | 設定隔離級別(只有當傳播機制設定成 `REQUIRED` or `REQUIRES_NEW`時這個配置才生效) |
| `timeout` | `int` (in seconds of granularity) | 設定超時時間(以秒為單位,只有當傳播機制設定成 `REQUIRED` or `REQUIRES_NEW`時這個配置才生效) |
| `readOnly` | `boolean` | 只讀事務配置(只有當傳播機制設定成 `REQUIRED` or `REQUIRES_NEW`時這個配置才生效) |
| `rollbackFor` | Array of `Class` objects, which must be derived from `Throwable.` | 回滾的異常 |
| `rollbackForClassName` | Array of class names. The classes must be derived from `Throwable.` | |
| `noRollbackFor` | Array of `Class` objects, which must be derived from `Throwable.` | 不回滾的異常 |
| `noRollbackForClassName` | Array of `String` class names, which must be derived from `Throwable.` | |
假如我們沒有配置上面的屬性,**這些屬性也都是有預設值的**。
- The propagation setting is `PROPAGATION_REQUIRED.`
- The isolation level is `ISOLATION_DEFAULT.`
- The transaction is read-write.
- The transaction timeout defaults to the default timeout of the underlying transaction system, or to none if timeouts are not supported.
- Any `RuntimeException` triggers rollback, and any checked `Exception` does not.(預設回滾RuntimeException )
### 多事務管理器
有時候專案中可能會存在多個事務管理器,比如JDBC事務,比如JMS事務。這時候我們可以通過transactionManager屬性指定。
```java
public class TransactionalService {
@Transactional("jdbc")
public void setSomething(String name) { ... }
@Transactional("jms")
public void doSomething() { ... }
}
```
上面的jdbc和jms是指兩個事務管理器在Spring容器中Bean的名字。
### 事務的傳播機制
在TransactionDefinition這個類中定義了6中傳播機制的型別。
**1. PROPAGATION_REQUIRED**
![](https://docs.spring.io/spring/docs/5.2.6.RELEASE/spring-framework-reference/images/tx_prop_required.png)
**2. PROPAGATION_REQUIRES_NEW**
![](https://docs.spring.io/spring/docs/5.2.6.RELEASE/spring-framework-reference/images/tx_prop_requires_new.png)
**3. PROPAGATION_NESTED**
只支援JDBC事務。
## 程式設計式事務管理
Spring框架提供兩種方式來進行程式設計式事務管理:
- The `TransactionTemplate`.
- `PlatformTransactionManager` 的實現。
Spring團隊推薦使用第一種方式進行程式設計式事務管理。
**1. 使用TransactionTemplate進行事務管理**
下面是使用TransactionTemplate進行事務管理的一個例子。
```java
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = TxApp.class)
public class TxTest {
@Autowired
private SqlSessionFactory sqlSessionFactory;
@Autowired
private TransactionTemplate transactionTemplate;
@Test
public void selectUserTest() {
SqlSession sqlSession1 = sqlSessionFactory.openSession();
//同一個sqlSession建立的Mapper
SysUserMapper mapper = sqlSession1.getMapper(SysUserMapper.class);
SysUser sysUser = new SysUser();
sysUser.setUsername("zyzl");
sysUser.setPassword("11");
//有返回值的操作
transactionTemplate.execute(new TransactionCallback() {
@Override
public Object doInTransaction(TransactionStatus status) {
try {
return mapper.insert(sysUser);
} catch (Exception e) {
status.setRollbackOnly();
throw e;
}
}
});
//沒返回值的操作
transactionTemplate.executeWithoutResult(new Consumer() {
@Override
public void accept(TransactionStatus transactionStatus) {
try {
mapper.insert(sysUser);
} catch (Exception e) {
transactionStatus.setRollbackOnly();
throw e;
}
}
});
}
}
```
我們也可以通過TransactionTemplate來設定事務的隔離級別等屬性。
```java
//設定隔離級別
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
//設定超時時間
transactionTemplate.setTimeout(30);
//設定傳播機制
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
```
> 對於不同的事務操作,如果需要不同的隔離級別和傳播機制的話,請使用不同的transactionTemplate。也就是說,你要建立不同的transactionTemplate物件來進行操作。
**2. 使用PlatformTransactionManager進行事務管理**
```java
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// explicitly setting the transaction name is something that can be done only programmatically
def.setName("SomeTxName");
// 設定傳播機制
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
//開啟事務
TransactionStatus status = txManager.getTransaction(def);
try {
// execute your business logic here
}
catch (MyException ex) {
txManager.rollback(status);
throw ex;
}
//提交事務
txManager.commit(status);
```
## 事務繫結事件
使用`@TransactionalEventListener`可以在事務提交前後,回滾後等階段觸發某些操作。但是這個功能暫時還沒想到很好的使用場景。後續有需要再來用。
```java
@Component
public class MyComponent {
@TransactionalEventListener
public void handleOrderCreatedEvent(CreationEvent creationEvent) {
// ...
}
}
```
## 重要類和介面
- PlatformTransactionManager:事務管理器,用於獲取事務,提交回滾事務;
- TransactionDefinition:
- TransactionStatus:代表一個事務
## 進一步閱讀
[Distributed transactions in Spring, with and without XA](https://www.javaworld.com/javaworld/jw-01-2009/jw-01-spring-transactions.html) is a JavaWorld presentation in which Spring’s David Syer guides you through seven patterns for distributed transactions in Spring applications, three of them with XA and four without.(Spring實現分散式事務的