1. 程式人生 > >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);
    }

結果如下:

測試-1
測試-2

由圖觀之,異常出現之後,事務發生了回滾,庫存不再減少,錢也不會再減少,結果正常.
拓展問題(面試):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);
    }
}

測試結果1
測試結果2

觀察結果,在餘額不足的情況下,外部方法呼叫purchase方法,丟擲異常時,事務回滾,庫存沒有減少,原因同Q1相同,但正好相反,但是走了AOP代理,所以事務起作用了.


那麼如果在內部的方法purchaseAgain,和外部的方法中加入事務控制又會是怎樣的情況呢?
這裡直接給出結論:
purchaseAgain方法加入註解@Transactional後,呼叫purchase方法(無論是否新增@Transactional),事務控制起作用;外部類的testBookPurchase方法呼叫本類的purchase方法,事務控制也是起作用的.
由此引入spring關於事務的傳播行為的介紹:spring的事務傳播行為一共分為以下幾種:

  1. REQUIRED(常用)
  2. REQUIRES_NEW(常用)
  3. SUPPORTS
  4. NOT_SUPPORTED
  5. NEVER
  6. NESTED
  7. 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的事務相關的技術要點,包括什麼是事務,事務的四個基本屬性,為什麼要使用事務,事務的分類,事務的傳播種類以及事務的隔離級別.大體上涵蓋了事務的相關知識,但是並沒有深入到原始碼級別來研究事務的相關實現,有機會一定要深入原始碼瞭解實現,這樣才能對知識的學習理解達到庖丁解牛的地步,對自己以後的知識積累和提升也會有很大的幫助.