Spring 事務管理
1、基本概念
理解Spring的事務管理,需要了解以下幾個概念:
1.1、 當前連接
每條線程只可以擁有一個活動的數據庫連接,稱爲“當前連接”。
一般數據庫事務遵循“開啓事務—>操作—>提交事務”三個步驟。在單線程環境中,不能調換它們的順序;但是在多線程環境中,如果數據庫連接需要共享,將會打破這個順序,如線程A將線程B的事務一起提交了。
爲了解決該問題,採用“當前連接”來存放數據庫連接,並與線程綁定(採用ThreadLocal)。也就是說在線程A下啓動的事務,不會影響到線程B的數據庫事務,它們之間使用的數據庫連接彼此互不干擾。
注意:當前的數據庫連接是可以被隨時更換的(此時不考慮引用計數的值)。在獨立事務中,如果當前連接已存在事務,則會新建一個數據庫連接作爲當前連接並開啓它的事務。
1.2、 引用計數
程序在執行期間,如果持有數據庫連接,需要使用“引用計數”標記。
引用計數用來確定當前數據庫連接(數據庫連接是共享的,連接池)是否可以被關閉。當引用計數爲0或小於0時,認爲應用程序不再需要該連接,可以放心關閉。
當從連接池獲取連接時,該連接的引用計數將增加1;當連接池回收該該連接時,引用計數減1。
1.3、 事務狀態
事務管理器在創建事務對象時,需要知道當前數據庫連接是否已經具有事務狀態。如果尚未開啓事務,事務管理器認爲這個連接是新的(new狀態),此時事務管理器收到commit請求時,可以放心提交事務。
如果當前連接存在事務,則很有可能在事務管理器創建事務對象之前已經對數據進行了操作,在這種情況下就不能隨意的進行commit或rollback操作。
事務狀態用來標記當前連接的事務狀態是如何的;並且輔助事務管理器決定究竟如何處理事務提交和回滾操作。如下
public void state()
{
DataSource ds= ......;
Connection conn = DataSourceUtil.getConnection(ds);//取得數據庫連接,會導致引用計數+1
conn.setAutoCommit(false);//開啓事務
conn.execute("update ...");//預先執行的 update 語句
TransactionStatus status = tm.getTransaction(PROPAGATION_REQUIRED);//加入到已有事務,引用計數+1
…… //執行數據庫插入
tm.commit(status);//引用計數-1
conn.commit();//遞交事務
DataSourceUtil.releaseConnection(conn,ds);//釋放連接,引用計數-1
}
在上面的代碼中,插入數據之前,將插入數據的操作加入到已有的事務中。在插入數據後,即使顯示進行了提交操作,但是由於它的事務已加入到外層事務中,因此這個提交操作是被忽略的;當外層事務提交時,纔會整體提交。
2、事務管理機制
2.1、事務特性
l 原子性(Atomicity):事務中的所有操作,要麼都做,要麼都不做
l 一致性(Consistency):事務前後,數據庫的狀態是一致的
l 隔離性(Isolation):一個事務的執行不被其他事務干擾
l 持久性(Duration):一個事務一旦提交,對數據庫的數據改變是永久性的
2.2、事務併發問題
l 髒讀
事務T1修改數據後,並將其寫會數據庫。事務T2讀取同一數據後,T1由於某種原因回滾,此時數據被恢復到原來的值,而T2讀取的值就與數據庫中的數據不一致,即T2讀取到的是錯誤的數據(讀到未提交的數據),如下
|
事務T1 |
事務T2 |
t1 |
開啓事務 |
|
t2 |
|
開啓事務 |
t3 |
取出數據age=20 |
|
t4 |
更新數據age=30 |
|
t5 |
|
讀取數據age=30 |
t6 |
rollback,age=20 |
|
t7 |
|
|
l 不可重複讀
不可重複讀是指事務T1讀取數據後,事務T2執行更新(插入或刪除)操作,當T1再次讀取數據時,前後兩次數據不一致。
對於事務T2插入或刪除的操作,造成T1再次讀取數據(報表統計)時前後兩次不一致的情況,稱爲“幻讀”。
l 修改丟失
兩個事務T1和T2讀入同一數據並修改,T2提交的結果破壞了T1提交的結果,導致T1的修改丟失,如下
|
事務T1 |
事務T2 |
t1 |
開啓事務 |
|
t2 |
|
開啓事務 |
t3 |
取出數據age=20 |
讀取age=20 |
t4 |
更新數據age=30 |
|
t5 |
|
更新數據age=32 |
t6 |
提交事務 |
|
t7 |
|
提交事務 |
2.3、事務隔離級別
爲了解決事務併發引起的問題,數據庫採用了事務的隔離級別來解決上述問題。
l READ_UNCOMMITED(不採用事務控制)
l READ_COMMIT
l REPEATABLE_READ
l SERIALIZABLE
隔離級別與可以解決的問題如下:
|
髒讀 |
重複讀 |
幻讀 |
Read Uncommitted |
否 |
否 |
否 |
Read Commited |
是 |
否 |
否 |
Repeatable Read |
是 |
是 |
否 |
Serializable |
是 |
是 |
是 |
注意:如果要完全正確統計報表,則需要設置隔離級別爲可串行化;如果允許一定範圍的誤差,可以設置隔離級別爲可重複讀。
2.4、事務併發處理
數據庫使用鎖來實現事務之間的隔離:
l 當一個事務訪問某個資源時,如果執行的是select語句,則加上共享鎖;如果執行的是insert、update或delete語句,則加上排它鎖。
l 當第二個事務訪問同樣的資源時,如果執行select語句,則加上共享鎖;如果執行的是insert、update、delete語句,則加上排他鎖。當第二個事務加鎖時,必須依據第一個事務對資源加鎖的類型,來確定是等待第一個事務解鎖,還是立即加鎖。
共享鎖用於讀取數據,它允許其他事務同時讀取鎖定的資源,但不允許其他事務更新它。但是否會對select語句加鎖,與數據庫的隔離級別有關,如Mysql只有當隔離級別爲Serializable時,纔會select語句加共享鎖。
排他鎖,它鎖定的資源,其他事務不能讀取也不能修改。當一個事務執行insert、update或delete語句時,數據庫會自動對操作的資源加排他鎖。
更新鎖在更新操作的初始化階段用來鎖定可能要修改的資源,從而避免使用共享鎖造成的死鎖,如下
|
事務T1 |
事務T2 |
t1 |
查詢Id爲1的記錄,加共享鎖 |
|
t2 |
|
查詢Id爲1的記錄,加共享鎖 |
t3 |
執行更新操作,申請排他鎖,發現記錄上已存在T2的共享鎖,等待事務T2結束 |
|
t4 |
|
執行更新操作,申請排他鎖,發現記錄上已存在T1的共享鎖,等待事務T1結束 |
死鎖發生 |
如執行updateuser set balance=1200 where id=1時,在數據庫中分爲兩個操作:
l 查詢ID爲1的記錄,查詢時爲更新鎖,允許其他事務查詢,以提高併發性;
l 執行更新操作,在更新前會先把鎖升級爲排他鎖;
|
事務T1 |
事務T2 |
t1 |
查詢Id爲1的記錄,加更新鎖 |
|
t2 |
|
查詢Id爲1的記錄,申請更新鎖,發現記錄上已存在T1的更新鎖,只能等待T1解鎖 |
t3 |
執行更新操作,更新鎖升級爲排他鎖 |
|
t4 |
事務結束,排他鎖解除 |
|
t5 |
|
執行更新操作,更新鎖升級爲排他鎖 |
t6 |
|
事務結束,排他鎖解除 |
共享鎖,排他鎖,更新鎖的鎖兼容矩陣如下:
申請鎖 |
共享鎖 |
更新鎖 |
排他鎖 |
已加鎖 |
|||
共享鎖 |
Yes |
Yes |
No |
更新鎖 |
Yes |
NO |
No |
排他鎖 |
No |
No |
No |
2.5、修改丟失
當採用數據庫默認的隔離級別(如Mysql爲RepeatabelRead)時,可能會出現修改丟失的問題,如下
|
事務T1 |
事務T2 |
t1 |
開始事務 |
|
t2 |
balance=20 |
開始事務 |
t3 |
|
balance=20 |
t4 |
balance+=10(30) |
|
t5 |
提交事務 |
|
t6 |
|
balance-=5(15) |
t7 |
|
提交事務 |
t1時刻,事務T1開始;
t2時刻,事務T1查詢數據,(Mysql默認隔離級別,並不爲會select語句加共享鎖);事務T2開始;
t3時刻,事務T2查詢數據(Mysql默認隔離級別,並不爲會select語句加共享鎖);
t4時刻:事務T1更新數據,加更新鎖;
t5時刻:事務T1提交,釋放更新鎖;
t6時刻:事務T2更新數據,加更新鎖;
t7時刻:事務T2提交,釋放更新鎖;
最後,事務T2更新的數據,覆蓋了事務T1的更新。由此可見,但兩個或多個併發事務讀取統一資源,然後基於最初讀到的數據更新該資源,就會發生更新都是問題。
對於簡單的更新操作來說,我們可以直接採用update語句,來避免更新丟失問題。但對於一些複雜的更新,往往需要先查詢數據,再更新,有如下解決方案:
1) 悲觀鎖
悲觀鎖通過爲select語句添加排他鎖,來防止修改丟失。手工爲select語句加排他鎖的SQL語法如下:
select * from user where id=1 for update
注意:JPA並沒有提供悲觀鎖的直接使用方式,若要使用悲觀鎖,需要使用nativesql來查詢。
2) 樂觀鎖
樂觀鎖,並不是一種鎖,而是一種樂觀的加鎖方案。原理如下:
它要求在數據庫表中增加一個版本字段(version),每次執行update語句時,這個字段都會累加;當其他事務以舊版本的值更新數據時,由於前面的事務的更新操作,使version字段累加,所以其他事務會找不到相應的記錄,依此來避免修改丟失。
在項目中使用樂觀鎖,需要在實體中增加version字段,並標註@javax.persistence.Version,在執行更新操作時實體管理器會自動更新version字段的值。
2.6、Spring 事務管理
2.6.1、事務傳播行爲
在Spring中,數據的插入、更新、刪除操作必須處於事務環境中,才能操作。當在一個Bean中調用另一個Bean中的方法時,就可能會產生嵌套事務。出現嵌套事務時,前一個方法是否處在事務環境將對下一個方法產生產生什麼樣的影響,這成爲事務的傳播屬性。在Spring中,事務的傳播屬性取值有:
l PROPAGATION_REQUIRED
如果處在事務環境中,將使用當前事務(加入已有事務);否則,開啓新事務,此時提交或回滾操作都交給新開啓的事務。
l PROPAGATION_SUPPORTS
如果存在事務,則使用當前事務;若不存在事務,則不使用事務;
l PROPAGATION_MANDATORY
如果存在事務,則使用當前事務;若不存在事務,將拋出異常;
l PROPAGATION_REQUIRES_NEW
創建一個新事務(內部事務);若當前存在事務,則掛起當前事務(外部事務);新開啓的內部事務被完全commite或rollback而不依賴於外部事務,它擁有自己的隔離範圍,自己的鎖等。當內部事務開始執行時,外部事務被掛起;內部事務結束時,外部事務將繼續執行。因此兩個事務是相對獨立的,互不影響。
所謂“掛起”指的是將當前線程使用的數據庫連接,暫時保存起來不使用;取而代之的是一個新的數據庫連接。當內部執行完畢後,將釋放當前的連接,將掛起的數據庫連接重新設爲當前數據庫連接。
l PROPAGATION_NOT_SUPPORT
不使用事務;若當前存在事務,則掛起當前事務,也就使用新的數據庫連接,新的連接不使用事務;
l PROPAGATION_NEVER
不使用事務;若當前存在事務,則拋出異常
l PROPAGATION_NESTED
若存在當前事務(外部事務),則使用嵌套事務;如果不存在當前事務,行爲類似於PROPAGATION_REQUIRED,要求要求事務管理器或者使用JDBC3.0 Savepoint API提供嵌套事務行爲(如Spring的DataSourceTransactionManager)。
PROPAGATION_NESTED開始一個“嵌套的”事務(子事務,Savepoint),它是已經存在事務的一個真正的子事務。嵌套事務開始執行時,它將先保存一個savepoint,如果嵌套事務失敗,將回滾到savepoint。嵌套事務是外部事務的一部分,只有外部事務提交時,嵌套事務也會被提交;外部事務回滾時,嵌套事務也回滾。因此,外部事務和嵌套事務是一個整體。但如果是嵌套事務回滾時,只回滾到嵌套事務開始執行時的保存點(對於外部事務選擇回滾,也可以選擇不回滾),因此,嵌套事務可以起到分支執行的效果。如果ServiceB.methodB 失敗,那麼執行ServiceC.methodC(), 而ServiceB.methodB 已經回滾到它執行之前的SavePoint, 所以不會產生髒數據(相當於此方法從未執行過),這種特性可以用在某些特殊的業務中,而PROPAGATION_REQUIRED 和PROPAGATION_REQUIRES_NEW 都沒有辦法做到這一點。
2.6.2、只讀事務
如果你一次執行單條查詢語句,則沒有必要啓用事務支持,數據庫默認支持SQL執行期間的讀一致性; 如果你一次執行多條查詢語句,例如統計查詢,報表查詢,在這種場景下,多條查詢SQL必須保證整體的讀一致性,否則,在前條SQL查詢之後,後條SQL查詢之前,數據被其他用戶改變,則該次整體的統計查詢將會出現讀數據不一致的狀態。
此時,應該啓用事務支持read-only="true"表示該事務爲只讀事務,比如上面說的多條查詢的這種情況可以使用只讀事務,由於只讀事務不存在數據的修改,因此數據庫將會爲只讀事務提供一些優化手段。
2.6.3、配置事務
<context:property-placeholder location="classpath:jdbc.properties"/>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="${databaseDriver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
<property name="initialSize" value="1"/>
<property name="minIdle" value="1"/>
<property name="maxActive" value="100"/>
<property name="maxIdle" value="20"/>
<property name="maxWait" value="1000"/>
</bean>
<!--jpa-->
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="persistenceXmlLocation" value="classpath:META-INF/persistence.xml"/>
<property name="persistenceUnitName" value="template"/>
<property name="packagesToScan" value="org.ssl.template.model"/>
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="showSql" value="true" />
<property name="generateDdl" value="true"/>
</bean>
</property>
</bean>
<!--hibernate事務管理-->
<!--
<beanid="transactionManager"
class="org.springframework.orm.hibernate4.HibernateTransactionManager"
p:sessionFactory-ref="sessionFactory"
/>
-->
<!--jpa事務管理-->
<bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<aop:config>
<aop:pointcut id="txMethod" expression="execution(*com.ssl.template.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txMethod"/>
</aop:config>
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="add*" propagation="REQUIRED" />
<tx:method name="update*" propagation="REQUIRED" />
<tx:method name="delete*" propagation="REQUIRED" />
<tx:method name="init*" propagation="REQUIRED" />
<tx:method name="*" propagation="REQUIRED" read-only="true"/>
</tx:attributes>
</tx:advice>
<!--註解管理事務-->
<!--
<tx:annotation-driventransaction-manager="txManager"/>
-->