這篇文章一起來回顧複習下spring的事務操作.事務是spring的重點, 也是面試的必問知識點之一. 說來這次面試期間,也問到了我,由於平時用到的比較少,也沒有關註過這一塊的東西,所以回答的不是特別好,所以借這一篇文章來回顧總結一下,有需要的朋友,也可以點贊收藏一下,複習一下這方面的知識,為年後的 ...
這篇文章一起來回顧複習下spring的事務操作.事務是spring的重點, 也是面試的必問知識點之一.
說來這次面試期間,也問到了我,由於平時用到的比較少,也沒有關註過這一塊的東西,所以回答的不是特別好,所以借這一篇文章來回顧總結一下,有需要的朋友,也可以點贊收藏一下,複習一下這方面的知識,為年後的面試做準備.
首先,瞭解一下什麼是事務?
---
資料庫事務(Database Transaction) ,是指作為單個邏輯工作單元執行的一系列操作,要麼完全地執行,要麼完全地不執行。 事務處理可以確保除非事務性單元內的所有操作都成功完成,否則不會永久更新面向數據的資源。通過將一組相關操作組合為一個要麼全部成功要麼全部失敗的單元,可以簡化錯誤恢復並使應用程式更加可靠。一個邏輯工作單元要成為事務,必須滿足所謂的ACID(原子性、一致性、隔離性和持久性)屬性。事務是資料庫運行中的邏輯工作單位,由DBMS中的事務管理子系統負責事務的處理。這裡簡單提一下事務的四個基本屬性,
A(Atomic) 原子性
事務必須是原子工作單元;對於其[數據修改]事務必須是原子工作單元;對於其數據修改,要麼全都執行,要麼全都不執行.
C(Consistent) 一致性
事務在完成時,必須使所有的數據都保持一致狀態。在相關資料庫中,所有規則都必須應用於事務的修改,以保持所有數據的完整性。
I(Insulation) 隔離性
由併發事務所作的修改必須與任何其它併發事務所作的修改隔離。事務查看數據時數據所處的狀態,要麼是另一併發事務修改它之前的狀態,要麼是另一事務修改它之後的狀態,事務不會查看中間狀態的數據。
D(Duration) 一致性
事務完成之後,它對於系統的影響是永久性的。該修改即使出現致命的系統故障也將一直保持。
瞭解了事務之後,我們為什麼要使用事務呢?換句話說,用事務是為瞭解決什麼問題呢?
首先我們來看一個業務場景:Tom在書店買書,java和Oracle,2種書,單價都是100,庫存量都是10本,Tom目前身上有150元.現在Tom買1本書的錢是足夠的,ok,買起來,交易結束後,對於Tom來說,買到了1本書,還剩下50元.正好要出門時接到jack的電話,原來是jack要Tom幫他捎本java,他要用來複習,那接下來的交易是否可以正常進行呢?常識來說,50元買價值100元的東西肯定是買不到的,那我們看看程式中是什麼情況?
首先,需要構建三張表,餘額表,商品表,和商品庫存表,如下:
然後定義介面如下:
public interface BookShopDao {
/**
* 根據書名獲取書的單價
*/
public int findBookPriceByIsbn(String isbn);
/**
* 更新書的庫存,使書號對應的庫存-1
*/
public void updateBookStock(String isbn);
/**
* 更新用戶的餘額:使username的balance-price
* @param name
* @param price
*/
public void updateUserAccount(String name,int price);
}
@Repository
public class BookShopDaoImpl implements BookShopDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public int findBookPriceByIsbn(String isbn) {
String sql = "SELECT price FROM book WHERE isbn = ?";
return jdbcTemplate.queryForObject(sql,Integer.class,isbn);
}
@Override
public void updateBookStock(String isbn) {
//檢查書的庫存是否足夠,不足夠則拋出異常
String sql2 = "SELECT stock FROM book_stock WHERE isbn = ?";
int stock = jdbcTemplate.queryForObject(sql2, Integer.class, isbn);
if(stock == 0){
throw new BookStockException("庫存不足");
}
String sql ="UPDATE book_stock SET stock= stock-1 where isbn = ?";
jdbcTemplate.update(sql,isbn);
}
@Override
public void updateUserAccount(String name, int price) {
//驗證餘額是否足夠,不足則拋出異常
String sql2 ="SELECT balance FROM account WHERE username = ?";
int balance = jdbcTemplate.queryForObject(sql2, Integer.class, name);
if(balance <price) {
throw new UserAccountException("餘額不足");
}
String sql = "UPDATE account SET balance = balance - ? WHERE username = ?";
jdbcTemplate.update(sql,price,name);
}
}
上面代碼中有點要註意:庫存餘量是否充足,餘額是否充足,需要在代碼中去自己判斷,mysql不會幫我們加,例如,當庫存數為0時,如果仍需要減1,值會變為-1,這不是我們想要的結果.
接下里定義一個service:
public interface BookShopService {
/**
* 購物方法
* @param username
* @param isbn
*/
public void purchase(String username,String isbn);
}
@Service
public class BookShopServiceImpl implements BookShopService{
@Autowired
private BookShopDao shopDao;
/**
* @param username
* @param isbn
*/
@Override
public void purchase(String username, String isbn) {
//1.獲取書的單價
int price = shopDao.findBookPriceByIsbn(isbn);
//更新書的庫存
shopDao.updateBookStock(isbn);
//更新餘額
shopDao.updateUserAccount(username,price);
}
}
到此,基本購買流程都已經實現,我們來寫一個測試方法測試一下購買的結果是什麼?
由圖中可以看出,程式報了"餘額不足"的異常,tom的餘額沒有減少,但是書店的庫存量卻減少了,這明顯是違反常理的,書店不會白白把書送給tom的,怎麼辦呢?事務就可以幫助我們解決這個難題.
這裡要先瞭解下事務的分類:
編程式事務
將事務管理代碼嵌入到業務方法中來控制事務的提交和回滾,在編程式管理事務當中,必須在每個事務操作中包含額外的事務管理代碼,繁瑣,不便.
聲明式事務
是建立在AOP之上的。其本質是對方法前後進行攔截,然後在目標方法開始之前創建或者加入一個事務,在執行完目標方法之後根據執行情況提交或者回滾事務。聲明式事務最大的優點就是不需要通過編程的方式管理事務,這樣就不需要在業務邏輯代碼中摻雜事務管理的代碼,只需通過基於@Transactional註解的方式或者配置文件中做相關的事務規則聲明,便可以將事務規則應用到業務邏輯中。
採用聲明式事務,基於@Transactional註解,首先看下配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--掃描包-->
<context:component-scan base-package="com.springtest"></context:component-scan>
<!--導入資源文件-->
<context:property-placeholder location="classpath:db.properties" />
<!--配置數據源-->
<bean id="jdbcSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="user" value="${jdbc.user}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
<property name="driverClass" value="${jdbc.driverClass}"></property>
<property name="initialPoolSize" value="${jdbc.initialPoolSize}"></property>
<property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
</bean>
<!--配置spring的jdbctemplate模版-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="jdbcSource"></property>
</bean>
<!--配置事務管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="jdbcSource"></property>
</bean>
<!--啟用事務註解-->
<tx:annotation-driven transaction-manager="transactionManager" />
</beans>
接下來給方法purchase()加上註解
@Transactional()
@Override
public void purchase(String username, String isbn) {
//1.獲取書的單價
int price = shopDao.findBookPriceByIsbn(isbn);
//更新書的庫存
shopDao.updateBookStock(isbn);
//更新餘額
shopDao.updateUserAccount(username,price);
}
結果如下:
由圖觀之,異常出現之後,事務發生了回滾,庫存不再減少,錢也不會再減少,結果正常.
拓展問題(面試):Q1: 假如此時BookShopServiceImpl中另外一個方法調用了purchase方法,那麼在另外一個方法中,事務是否起作用呢?
Q2:假如此時另外一個類中方法調用了BookShopServiceImpl類中的purchase方法,那麼事務又是否起作用呢?
我們來一一驗證一下,首先Q1
@Service
public class BookShopServiceImpl implements BookShopService{
@Autowired
private BookShopDao shopDao;
@Transactional()
@Override
public void purchase(String username, String isbn) {
//1.獲取書的單價
int price = shopDao.findBookPriceByIsbn(isbn);
//更新書的庫存
shopDao.updateBookStock(isbn);
//更新餘額
shopDao.updateUserAccount(username,price);
}
@Override
public void purchaseAgain(String username, String isbn) {
purchase(username,isbn);
}
}
測試結果:
測試前,資料庫數據為:
結果觀之,事務並沒有起作用,原因是什麼?
啟用事務首先調用的是AOP代理對象而不是目標對象,首先執行事務切麵,事務切麵內部通過TransactionInterceptor環繞增強進行事務的增強,即進入目標方法之前開啟事務,退出目標方法時提交/回滾事務.而類內部的自我調用將無法實施切麵中的增強.,解決方案的話限於篇幅,以後再寫,這裡知道原因就可以了.
接下來驗證Q2,首先創建一個新的介面和實現類,裡面調用BookShopService 的purchase方法,觀察結果
@Service
public class TestBookShopServiceImpl implements TestBookShopService {
@Autowired
private BookShopService shopService;
@Override
public void testBookPurchase(String name, String isbn) {
shopService.purchase(name,isbn);
}
}
觀察結果,在餘額不足的情況下,外部方法調用purchase方法,拋出異常時,事務回滾,庫存沒有減少,原因同Q1相同,但正好相反,但是走了AOP代理,所以事務起作用了.
那麼如果在內部的方法purchaseAgain,和外部的方法中加入事務控制又會是怎樣的情況呢?
這裡直接給出結論:
purchaseAgain方法加入註解@Transactional後,調用purchase方法(無論是否添加@Transactional),事務控制起作用;外部類的testBookPurchase方法調用本類的purchase方法,事務控制也是起作用的.
由此引入spring關於事務的傳播行為的介紹:spring的事務傳播行為一共分為以下幾種:
- REQUIRED(
常用
) - REQUIRES_NEW(
常用
) - SUPPORTS
- NOT_SUPPORTED
- NEVER
- NESTED
MANDATORY
在@Transactional註解中是propagation屬性;
分別介紹:
PROPAGATION_REQUIRED 如果存在一個事務,則支持當前事務。如果沒有事務則開啟一個新的事務。(是spring 的預設事務傳播行為)。
PROPAGATION_REQUIRES_NEW 總是開啟一個新的事務。如果一個事務已經存在,則將這個存在的事務掛起。
PROPAGATION_SUPPORTS 如果存在一個事務,支持當前事務。如果沒有事務,則非事務的執行。但是對於事務同步的事務管理器,PROPAGATION_SUPPORTS與不使用事務有少許不同。
PROPAGATION_NOT_SUPPORTED 總是非事務地執行,並掛起任何存在的事務。
PROPAGATION_MANDATORY 如果已經存在一個事務,支持當前事務。如果沒有一個活動的事務,則拋出異常。
PROPAGATION_NEVER 總是非事務地執行,如果存在一個活動事務,則拋出異常。
PROPAGATION_NESTED 如果一個活動的事務存在,則運行在一個嵌套的事務中. 如果沒有活動事務, 則按TransactionDefinition.PROPAGATION_REQUIRED 屬性執行。
事務的傳播行為定義了事務的控制範圍,那麼事務的隔離級別定義的則是事務在資料庫讀寫方面的控制範圍.
有的時候,在程式併發的情況下,會發生以下的神奇情況:
- 臟讀:對於兩個事務T1,T2,T1讀取了T2更新但是還未提交的欄位,之後,若T2回滾,那麼T1讀取的內容就是臨時且無效的
- 不可重覆讀:對於兩個事務T1,T2, T1讀取了一個欄位,然後被T2更新了,之後T1再次讀取,欄位值變掉了.
- 幻讀:兩個事務T1,T2, T1從一個表中讀取了一個欄位,然後T2在該表中插入了一些新的行,之後,如果T1再次讀取同一個表,就會多出幾行數據.
那麼以上的問題要如何來解決呢,spring給出了它的解決方案,將事務的隔離性分為以下幾個等級 - READ_UNCOMMITTED
- READ_COMMITTED
- REPEATABLE_READ
SERIALIZABLE
在@Transactional註解中是propagation屬性;
分別介紹:
READ_UNCOMMITTED 這是事務最低的隔離級別,它充許別外一個事務可以看到這個事務未提交的數據。 這種隔離級別會產生臟讀,不可重覆讀和幻像讀;
READ_COMMITTED 保證一個事務修改的數據提交後才能被另外一個事務讀取。另外一個事務不能讀取該事務未提交的數據。 這種隔離級別可以避免臟讀出現,但是可能會出現不可重覆讀和幻像讀;
REPEATABLE_READ 這種事務隔離級別可以防止臟讀,不可重覆讀。但是可能出現幻像讀;
SERIALIZABLE 這是花費最高代價但是最可靠的事務隔離級別。事務被處理為順序執行。 除了防止臟讀,不可重覆讀外,還避免了幻像讀;
以上幾種隔離界別, 在瞭解了其作用及其可避免的情況之後,我們在工作中視情況採用,不過一般預設情況就可以處理大多數情況了.
---
最後小結
這篇文章回顧了spring的事務相關的技術要點,包括什麼是事務,事務的四個基本屬性,為什麼要使用事務,事務的分類,事務的傳播種類以及事務的隔離級別.大體上涵蓋了事務的相關知識,但是並沒有深入到源碼級別來研究事務的相關實現,有機會一定要深入源碼瞭解實現,這樣才能對知識的學習理解達到庖丁解牛的地步,對自己以後的知識積累和提升也會有很大的幫助.