1. 程式人生 > >Spring中事務的(特性,傳播行為,隔離級別,不合理現象,丟失更新,案例..)

Spring中事務的(特性,傳播行為,隔離級別,不合理現象,丟失更新,案例..)

事務

事務的特性4個:

原子性

  事務必須是原子工作單元;對於其資料修改,要麼全都執行,要麼全都不執行。通常,與某個事務關聯的操作具有共同的目標,並且是相互依賴的。如果系統只執行這些操作的一個子集,則可能會破壞事務的總體目標。原子性消除了系統處理操作子集的可能性。

一致性

  事務在完成時,必須使所有的資料都保持一致狀態。在相關資料庫中,所有規則都必須應用於事務的修改,以保持所有資料的完整性。事務結束時,所有的內部資料結構(如 B 樹索引或雙向連結串列)都必須是正確的。某些維護一致性的責任由應用程式開發人員承擔,他們必須確保應用程式已強制所有已知的完整性約束。例如,當開發用於轉帳的應用程式時,應避免在轉帳過程中任意移動小數點。

隔離性

  由併發事務所作的修改必須與任何其它併發事務所作的修改隔離。事務檢視資料時資料所處的狀態,要麼是另一併發事務修改它之前的狀態,要麼是另一事務修改它之後的狀態,事務不會檢視中間狀態的資料。這稱為隔離性,因為它能夠重新裝載起始資料,並且重播一系列事務,以使資料結束時的狀態與原始事務執行的狀態相同。當事務可序列化時將獲得最高的隔離級別。在此級別上,從一組可並行執行的事務獲得的結果與通過連續執行每個事務所獲得的結果相同。由於高度隔離會限制可並行執行的事務數,所以一些應用程式降低隔離級別以換取更大的吞吐量。防止資料丟失。

永續性

  事務完成之後,它對於系統的影響是永久性的。該修改即使出現致命的系統故障也將一直保持。


事務的傳播行為有7種:

事務傳播行為型別

說明

PROPAGATION_REQUIRED

如果當前沒有事務,就新建一個事務,如果已經存在一個事務中,加入到這個事務中。這是最常見的選擇。

PROPAGATION_SUPPORTS

支援當前事務,如果當前沒有事務,就以非事務方式執行。

PROPAGATION_MANDATORY

使用當前的事務,如果當前沒有事務,就丟擲異常。

PROPAGATION_REQUIRES_NEW

新建事務,如果當前存在事務,把當前事務掛起。

PROPAGATION_NOT_SUPPORTED

以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。

PROPAGATION_NEVER

以非事務方式執行,如果當前存在事務,則丟擲異常。

PROPAGATION_NESTED

如果當前存在事務,則在巢狀事務內執行。如果當前沒有事務,則執行與PROPAGATION_REQUIRED類 似的操作。


事務的隔離級別:

1. ISOLATION_DEFAULT: 
這是一個PlatfromTransactionManager預設的隔離級別,使用資料庫預設的事務隔離級別.

  另外四個與JDBC的隔離級別相對應

  2. ISOLATION_READ_UNCOMMITTED: 這是事務最低的隔離級別,它充許令外一個事務可以看到這個事務未提交的資料。

  這種隔離級別會產生髒讀,不可重複讀和幻像讀。

  3. ISOLATION_READ_COMMITTED: 
保證一個事務修改的資料提交後才能被另外一個事務讀取。另外一個事務不能讀取該事務未提交的資料

  4. ISOLATION_REPEATABLE_READ: 這種事務隔離級別可以防止髒讀,不可重複讀。但是可能出現幻像讀。

  它除了保證一個事務不能讀取另一個事務未提交的資料外,還保證了避免下面的情況產生(不可重複讀)。

  5. ISOLATION_SERIALIZABLE 這是花費最高代價但是最可靠的事務隔離級別。事務被處理為順序執行。


其中 MySQL的預設隔離級別是:REPEATABLE_READ

其中SQL Server的預設隔離級別是:READ_COMMITTED

其中 Orache的隔離級別是:READ_COMMITTED

什麼是髒讀?什麼是幻象讀?什麼是不可重複讀?

1. 髒讀 :髒讀就是指當一個事務正在訪問資料,並且對資料進行了修改,而這種修改還沒有提交到資料庫中,這時,另外一個事務也訪問這個資料,然後使用了這個資料。


2. 不可重複讀 :是指在一個事務內,多次讀同一資料。在這個事務還沒有結束時,另外一個事務也訪問該同一資料。那麼,在第一個事務中的兩 次讀資料之間,由於第二個事務的修改,那麼第一個事務兩次讀到的的資料可能是不一樣的。這樣就發生了在一個事務內兩次讀到的資料是不一樣的,因此稱為是不 可重複讀。例如,一個編輯人員兩次讀取同一文件,但在兩次讀取之間,作者重寫了該文件。當編輯人員第二次讀取文件時,文件已更改。原始讀取不可重複。如果 只有在作者全部完成編寫後編輯人員才可以讀取文件,則可以避免該問題。


3. 幻讀 : 是指當事務不是獨立執行時發生的一種現象,例如第一個事務對一個表中的資料進行了修改,這種修改涉及到表中的全部資料行。 同時,第二個事務也修改這個表中的資料,這種修改是向表中插入一行新資料。那麼,以後就會發生操作第一個事務的使用者發現表中還有沒有修改的資料行,就好象 發生了幻覺一樣。例如,一個編輯人員更改作者提交的文件,但當生產部門將其更改內容合併到該文件的主複本時,發現作者已將未編輯的新材料新增到該文件中。 如果在編輯人員和生產部門完成對原始文件的處理之前,任何人都不能將新材料新增到文件中,則可以避免該問題。

什麼是丟失更新?

當兩個或多個事務選擇同一行,然後基於最初選定的值更新該行時,會發生丟失更新問題。每個事務都不知道其他事務的存在。最後的更新將覆蓋由其他事務所做的更新,這將導致資料丟失。

關於丟失更新的解決方案:1.樂觀鎖
                                  2.悲觀鎖

 1.樂觀鎖的原理是:認為事務不一定會產生丟失更新,讓事務進行併發修改,不對事務進行鎖定。發現併發修改某行資料時,樂觀鎖丟擲異常。讓使用者解決。可以通過給資料表新增自增的version欄位或時間戳timestamp。進行資料修改時,資料庫會檢測version欄位或者時間戳是否與原來的一致。若不一致,丟擲異常。

樂觀鎖不會鎖住任何東西,也就是說,它不依賴資料庫的事務機制,樂觀鎖完全是應用系統層面的東西。

如果使用樂觀鎖,那麼資料庫就必須加版本欄位,否則就只能比較所有欄位,但因為浮點型別不能比較,所以實際上沒有版本欄位是不可行的。


2.悲觀鎖是指假設併發更新衝突會發生,所以不管衝突是否真的發生,都會使用鎖機制。

悲觀鎖會完成以下功能:鎖住讀取的記錄,防止其它事務讀取和更新這些記錄。其它事務會一直阻塞,直到這個事務結束。

悲觀鎖是在使用了資料庫的事務隔離功能的基礎上,獨享佔用的資源,以此保證讀取資料一致性,避免修改丟失。

悲觀鎖可以使用Repeatable Read事務,它完全滿足悲觀鎖的要求。


還有一種是死鎖

除了Read UnCommittedSnapshot,其它型別的事務都可能產生死鎖。

當二或多個工作各自具有某個資源的鎖定,但其它工作嘗試要鎖定此資源,而造成工作永久封鎖彼此時,會發生死鎖。例如:

1.           事務 A 取得資料列 1 的共享鎖定。

2.           事務B 取得資料列 2 的共享鎖定。

3.           事務A 現在要求資料列 2 的獨佔鎖定,但會被封鎖直到事務B 完成並釋出對資料列 2 的共享鎖定為止。

4.           事務B 現在要求資料列 1 的獨佔鎖定,但會被封鎖直到事務A 完成並釋出對資料列 1 的共享鎖定為止。

等到事務B 完成後,事務A 才能完成,但事務B 被事務A 封鎖了。這個狀況也稱為「迴圈相依性」(Cyclic Dependency)。事務A 相依於事務B,並且事務B 也因為相依於事務A 而封閉了這個迴圈。

關於事務的案例:購買股票的案例,買股票,個人金錢減少,對應買的股票的股數增加

下面看一下資料庫 和分層結構


首先我們寫2個方法,一個是改變Account表的方法  一個是改變Stock表的方法

介面和實現類省略......

直接寫實現類中的程式碼:

首先是Account的方法:

這個方法要實現我們自己寫的介面,並且繼承jdbcDaoSupport這和類。

我們要寫修改的方法,返回值型別是Boolean型別

首先宣告一個boolean型別的變數,並且方法帶參,引數是對應表的id 和錢數,並宣告一個boolean isBuy這個是用於判斷是買股還是賣股的,

如果是買股 我們的錢數會減少,我們股數會增加。

如果是賣股相反。

接下來是編寫SQL語句的時候了。根據isBuy判斷走那個SQL語句

如果SQL語句那邊沒檔案 判讀一下真假 返回的是真,否則我們預設返回false;

Stock中的方法相似。。。。。。。

package cn.happy.spring23tx.dao;

import cn.happy.spring23tx.entity.Account;
import org.springframework.jdbc.core.support.JdbcDaoSupport;

/**
 * Created by linlin on 2017/8/4.
 */
public class AccountDao extends JdbcDaoSupport implements IAccountDao{
    public boolean addAccount(Account account) {
        return false;
    }

    public boolean updateAccount(int aid, double money,boolean isBuy) {
            boolean flag=false;
            String sql=null;
            if(isBuy){
                sql="update Account set balance=balance-? where aid=?";
            }else{
                sql="update Account set balance=balance+? where aid=?";
            }
            int count1=this.getJdbcTemplate().update(sql,money,aid);
            if(count1>0){
                flag=true;
            }
            return flag;

    }
}


在然後是Stock中的方法;


   public boolean updateStock(int sid, double count, boolean isBuy) {
        boolean flag=false;
        String sql=null;
        if(isBuy){
            sql="update Stock set count=count+? where sid=?";
        }else{
            sql="update Stock set count=count-? where sid=?";
        }
        int count1=this.getJdbcTemplate().update(sql,count,sid);
        if(count1>0){
            flag=true;
        }
        return flag;
    }


接下來是 service中的程式碼,

先在介面中編寫一個方法 我們會在這個方法中呼叫我們之前寫的所有方法  上面的那倆個 ,因為 增加股數,減少錢數這個是同步執行的。我們在這裡聲明瞭一個異常,自己寫的異常。StockExection.這個方法返回的是空,並且引數挺多的 有股票ID  股票數  購買人ID 錢數

宣告一個boolean型別的變數 我們先給他附一個定值,我們呼叫別的方法 ,並在每個方法都帶上引數isBuy isBuy是啥 在哪個方法裡面我們就走哪個SQL語句。

在這裡我們丟擲一個異常,我們讓他走這個異常,然後我們在測試類中會走一個配置  xml的配置 ,在這裡面 配置是核心。。。

    public void buyStock(int sid,int count,int aid,double money) throws StockExection;
package cn.happy.spring23tx.service;

import cn.happy.spring23tx.dao.IAccountDao;
import cn.happy.spring23tx.dao.IStockDao;
import cn.happy.spring23tx.entity.Account;
import cn.happy.spring23tx.entity.Stock;
import cn.happy.spring23tx.entity.StockExection;

/**
 * Created by linlin on 2017/8/4.
 */
public class ServiceImpl implements IService{

    private IAccountDao accountDao;
    private IStockDao stockDao;

    public IAccountDao getAccountDao() {
        return accountDao;
    }

    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }

    public IStockDao getStockDao() {
        return stockDao;
    }

    public void setStockDao(IStockDao stockDao) {
        this.stockDao = stockDao;
    }

    public boolean addAccount(Account account) {
        return false;
    }

    public boolean addStock(Stock stock) {
        return false;
    }

    public void buyStock(int sid, int count, int aid, double money) throws StockExection {
boolean isBuy=true;
accountDao.updateAccount(aid,money,isBuy);
if(1==1){
    throw new StockExection();
}
stockDao.updateStock(sid,count,isBuy);
    }

}


首先我們看一下配置。

我們先配置2個實現類 和資料來源 這些聯通以後我們配置service 聯通實現類的配置

最重要的一步是我們配置的事務這塊,事務 成功---提交,失敗----回滾

我們先建立一個bean節點配置事務管理器。

再建立一個bean 幾點裡面屬性包括transaxtionManager,tager,還有一個name=transacrionAttributes的節點裡面寫的是key value的值,分別是方法名是提交的方法啥的 是事務的隔離級別,提交方式,我們在測試類中的名是事務的id.

transactionManager

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
   http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd ">


<context:property-placeholder location="jdbc.properties"></context:property-placeholder>

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="${jdbc.driver}"></property>
    <property name="url" value="${jdbc.url}"></property>
    <property name="username" value="${jdbc.username}"></property>
    <property name="password" value="${jdbc.password}"></property>

</bean>

    <bean id="accountDao" class="cn.happy.spring23tx.dao.AccountDao">
        <property name="dataSource" ref="dataSource"></property>

    </bean>

    <bean id="stockDao" class="cn.happy.spring23tx.dao.StockDao">
        <property name="dataSource" ref="dataSource"></property>

    </bean>
    <bean id="service" class="cn.happy.spring23tx.service.ServiceImpl">

        <property name="accountDao" ref="accountDao"></property>
        <property name="stockDao" ref="stockDao"></property>
    </bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" >
    <property name="dataSource" ref="dataSource"></property>
</bean>

    <bean id="serviceProxys" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name="transactionManager" ref="transactionManager"></property>
        <property name="target" ref="service"></property>
    <property name="transactionAttributes">
        <props>
            <prop key="add*">ISOLATION_DEFAULT,PROPAGATION_REQUIRED</prop>
            <prop key="buy*">ISOLATION_DEFAULT,PROPAGATION_REQUIRED,-StockExection</prop>
        </props>

    </property>

    </bean>
</beans>

 @Test
    public void test01(){
        ApplicationContext ctx=new ClassPathXmlApplicationContext("applicationContext19tx.xml");
       IService i=(IService)ctx.getBean("serviceProxys");
        try {
            i.buyStock(1,2,1,20000);
        } catch (StockExection stockExection) {

        }

    }


測試類中我們要用xml  使用介面getBean(“。。。”)因為我們底層走的是JDK的動態代理 所以是介面 

如果是CGlib我們就可以直接用實現類了,

並 呼叫方法  把值放進去,因為我們丟擲了異常,所要以我們要異常環繞一道。。

我的部落格完結了。。。