轉載請注明出處: https://www.cnblogs.com/qnlcy/p/15237377.html
一、事務的定義
事務(Transaction),是指訪問并可能更新資料庫中各種資料項的一個程式執行單元(unit),是恢復和并發控制的基本單位,
事務的產生,其實是為了當應用程式訪問資料庫的時候,事務能夠簡化我們的編程模型,不需要我們去考慮各種各樣的潛在錯誤和并發問題.
二、事務的屬性
事務具有4個屬性,簡稱 ACID
| 屬性 | 說明 | |
|---|---|---|
| Atomicity | 原子性 | 一個事務是一個不可分割的作業單位,事務中包括的操作要么都做,要么都不做, |
| Consistency | 一致性 | 事務執行的結果必須是使資料庫從一個一致性狀態c0變到另一個一致性狀態c1 |
| Isolation | 隔離性 | 一個事務的執行不能被其他事務干擾,即一個事務內部的操作及使用的資料對并發的其他事務是隔離的,并發執行的各個事務之間不能互相干擾, |
| Durability | 持久性 | 指一個事務一旦提交,它對資料庫中資料的改變就應該是永久性的,接下來的其他操作或故障不應該對其有任何影響, |
三、Spring 事務的隔離級別
當多個執行緒都開啟事務操作資料庫中的資料時,資料庫系統要能進行隔離操作,以保證各個執行緒獲取資料的準確性,
在介紹資料庫提供的各種隔離級別之前,我們先看看如果不考慮事務的隔離性,會發生的幾種問題
3.1 隔離級別引出的問題
3.1.1 臟讀
是指在沒有隔離的情況下,一個事務讀取了另外一個事務已修改但未提交(有可能回滾也有可能繼續修改)的緩沖區資料,

3.1.2 不可重復讀
資料庫中的某項資料在一個事務多次讀取,但是在多次讀取期間,其他事務對其有修改并提交,導致回傳值不同,這就發生了不可重復讀,
不可重復讀側重修改,

3.1.3 幻讀
幻讀和不可重復讀相似,當一個事務(T1)讀取幾行記錄后(事務并沒有結束),另一個并發事務(T2)插入了一些記錄時,幻讀就發生了,在后來的查詢中,第一個事務(T1)就會發現一些原來沒有的額外記錄,
幻讀側重新增或者洗掉,

3.2 隔離級別
在理想狀態下,事務之間將完全隔離(即下表中的 Isolation.SERIALIZABLE ),從而可以防止這些問題發生,
然而,完全隔離會影響性能,因為隔離經常涉及到鎖定在資料庫中的記錄(甚至有時是鎖表),
完全隔離要求事務相互等待來完成作業,會阻礙并發,因此,可以根據業務場景選擇不同的隔離級別,
| 隔離級別 | 含義 |
|---|---|
| Isolation.DEFAULT | 使用后端資料庫默認的隔離級別 |
| Isolation.READ_UNCOMMITTED | 允許讀取尚未提交的更改,可能導致臟讀、幻讀或不可重復讀, |
| Isolation.READ_COMMITTED | (Oracle 默認級別)允許從已經提交的并發事務讀取,可防止臟讀,但幻讀和不可重復讀仍可能會發生, |
| Isolation.REPEATABLE_READ | (MYSQL默認級別)對相同欄位的多次讀取的結果是一致的,除非資料被當前事務本身改變,可防止臟讀和不可重復讀,但幻讀仍可能發生, |
| Isolation.SERIALIZABLE | 完全服從ACID的隔離級別,確保不發生臟讀、不可重復讀和幻讀,這在所有隔離級別中也是最慢的,因為它通常是通過完全鎖定當前事務所涉及的資料表來完成的, |
四、Spring 事務的傳播機制
Spring 事務的傳播機制描述了在嵌套事務當中,當前事務與外部事務(最近的那個,有可能沒有)的繼承關系,
比如一個事務方法里面呼叫了另外一個事務方法,那么兩個方法是各自作為獨立的方法提交還是內層的事務合并到外層的事務一起提交,這就是需要事務傳播機制的配置來確定怎么樣執行,
Spring 事務的傳播有如下機制
| 型別 | 描述 |
|---|---|
| PROPAGATION_REQUIRED | Spring默認的傳播機制,能滿足絕大部分業務需求,如果外層有事務,則當前事務加入到外層事務,一塊提交,一塊回滾,如果外層沒有事務,新建一個事務執行 |
| PROPAGATION_REQUES_NEW | 該事務傳播機制是每次都會新開啟一個事務,同時把外層事務掛起,當當前事務執行完畢,恢復上層事務的執行,如果外層沒有事務,執行當前新開啟的事務即可 |
| PROPAGATION_SUPPORT | 如果外層有事務,則加入外層事務,如果外層沒有事務,則直接使用非事務方式執行,完全依賴外層的事務 |
| PROPAGATION_NOT_SUPPORT | 該傳播機制不支持事務,如果外層存在事務則掛起,執行完當前代碼,則恢復外層事務,無論是否例外都不會回滾當前的代碼 |
| PROPAGATION_NEVER | 該傳播機制不支持外層事務,即如果外層有事務就拋出例外 |
| PROPAGATION_MANDATORY | 與NEVER相反,如果外層沒有事務,則拋出例外 |
| PROPAGATION_NESTED | 該傳播機制的特點是可以保存狀態保存點,當前事務回滾到某一個點,從而避免所有的嵌套事務都回滾,即各自回滾各自的,如果子事務沒有把例外吃掉,基本還是會引起全部回滾的, |
五、Spring 事務的應用(宣告式)
Spring 宣告式事務是指依托注解 @Transactional 和 AOP 功能,在其方法兩端添加事務的操作,實作對被注解修飾方法的增強,
5.1 事務只讀
從事務開始(時間點a)到這個事務結束的程序中,其他事務所提交的資料,該事務將看不見!(查詢中不會出現別人在時間點a之后提交的資料),
事務只讀只適用于 當傳播機制為
PROPAGATION_REQUIRED,PROPAGATION_REQUES_NEW的情況
5.1.1 應用場景
在諸如統計查詢、報表查詢的程序當中,需要多次查詢,為了避免在查詢程序當中對剩余查詢資料的修改,保證資料整體在某一時刻的一致性,需要使用只讀事務,
5.1.2 使用方式
@Transactional(propagation = Propagation.REQUIRES, readOnly = true)
public List<Product> findAllProducts() {
return this.productDao.findAllProducts();
}
5.2 事務回滾
在事務注解 @Transactional 中指定了某個例外后,捕獲到事務方法拋出了該例外或者其子類例外,會造成事務回滾,默認當捕獲到方法拋出的 RuntimeException 例外后,事務就會回滾,還可以設定當出現某例外時候不回滾,即使是運行時例外
5.2.1 使用方式
// 回滾Exception型別例外
@Transactional(rollbackFor = Exception.class)
public void test1() throws Exception {
// ..
}
// 回滾自定義型別例外
@Transactional(rollbackForClassName = "org.transaction.demo.CustomException")
public void test2() throws Exception {
// ..
}
// 不回滾自定義型別例外
@Transactional(noRollbackFor = CustomException.class)
public void test3() throws Exception {
// ..
}
5.3 事務超時
如果一個事務長時間占用資料庫連接,會導致服務等待從而引起服務雪崩效應,所以設定一個合理的超時時間,是必要的,默認不超時,事務超時會引起事務回滾,
事務超時只適用于 當傳播機制為
PROPAGATION_REQUIRED,PROPAGATION_REQUES_NEW的情況
5.3.1 使用方式
//設定事務超時時間,單位秒
@Transactional(timeout = 5)
public void test() {
// ..
}
5.4 事務傳播機制的使用方式
//每次外層事務呼叫都會開啟一個新事務
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void test() {
// ..
}
5.5 事務隔離機制的使用方式
指定事務隔離機制只適用于 當傳播機制為
PROPAGATION_REQUIRED,PROPAGATION_REQUES_NEW的情況
//設定事務隔離級別為串行
@Transactional(isolation = Isolation.SERIALIZABLE))
public void test() {
// ..
}
六、Spring 宣告式事務的 AOP 陷阱
總所周知,宣告式事務依托 AOP 功能實作對事務方法的增強,而 AOP 底層則是代理,存在代理陷阱,
6.1 AOP 代理陷阱復現
@Transactional(rollbackFor = RuntimeException.class)
public void insertUser(User user) {
userMapper.insertUser(user);
throw new RuntimeException("");
}
/**
* 內部呼叫新增方法
*/
public void insertMale(User user) {
user.setGender("male");
this.insertUser(user);
}
當外部方法直接呼叫 insertMale(user) 的時候,事務并不會生效,
6.2 原因分析
AOP使用的是動態代理的機制,它會給類生成一個代理類,事務的相關操作都在代理類上完成,內部呼叫使用的是實體呼叫,并沒有通過代理類呼叫方法,所以會導致事務失效,

6.2.1 偽代碼
- 代理類
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//代理之前做增強
System.out.println("代理之前...");
//根據需要添加事務處理邏輯
...
//呼叫原有方法 insertMale
Object obj = method.invoke(object, args);
//做增強
System.out.println("代理之后...");
//根據需要添加事務處理邏輯
...
return obj;
}
當執行 insertMale() 方法時,因為沒有事務注解,所以沒有添加事務處理邏輯,所以直接呼叫了目標類的 insertMale() 方法,
- 目標類執行情況
public void insertMale(User user) {
user.setGender("male");
//這里的 this 指向了目標類而不是代理類
//所以及時下面的方法添加了事務注解,但是并沒有除法增強實作,事務也還是不生效的
this.insertUser(user);
}
6.3 解決方案
6.3.1 注入自身
利用Spring可以回圈依賴來解決問題
@Service
public class TestService {
@Autowired
private TestService testService;
@Transactional(rollbackFor = RuntimeException.class)
public void insertUser(User user) {
userMapper.insertUser(user);
throw new RuntimeException("");
}
/**
* 內部呼叫新增方法
*/
public void insertMale(User user) {
user.setGender("male");
//這里使用 欄位 testService 呼叫事務方法
testService.insertUser(user);
}
}
6.3.2 使用 ApplicationContext 獲取目標類
注入 Spring 背景關系 ApplicationContex, 然后獲取到 目標 bean, 再呼叫事務方法
@Service
public class TestService {
@Autowired
private ApplicationContext applicationContext;
@Transactional(rollbackFor = RuntimeException.class)
public void insertUser(User user) {
userMapper.insertUser(user);
throw new RuntimeException("");
}
/**
* 內部呼叫新增方法
*/
public void insertMale(User user) {
user.setGender("male");
//這里使用背景關系獲取目標類實體
TestService testService = applicationContext.getBean(TestService.class);
testService.insertUser(user);
}
}
6.3.3 使用 AopContext
Aop 背景關系采用 ThreadLocal 保存了代理物件,可以使用 Aop 背景關系來進行目標方法的呼叫,
使用時候要在啟動類上添加 exposeProxy = true 配置
- 配置
@SpringBootApplication
//配置:匯出代理物件到AOP背景關系
@EnableAspectJAutoProxy(exposeProxy = true)
public class DemoApplication {
}
- 使用
public class TestService {
@Transactional(rollbackFor = RuntimeException.class)
public void insertUser(User user) {
userMapper.insertUser(user);
throw new RuntimeException("");
}
/**
* 內部呼叫新增方法
*/
public void insertMale(User user) {
user.setGender("male");
//使用AOP背景關系獲取目標代理類
TestService testService = (TestService) AopContext.currentProxy();
testService.insertUser(user);
}
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/298228.html
標籤:Java
