Spring事務管理是基於AOP編程思想實現,Spring框架被普遍使用的緣由之一,就是提供了強大的事務管理機制。java
AOP是什麼?咱們常說的AOP並非指一種開發技術,而是一種編程思想,AOP的核心概念就是面向切面編程,實現可插拔,下降程序以前的耦合性,提升重用性。git
Spring AOP 基於動態代理實現,一種是JDK動態代理,另外一種是CGLIB動態代理。github
spring2.5以前聲明式事務能夠這樣實現:算法
<!-- 聲明使用的spring事務管理器--> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> <tx:advice id="txAdvice" transaction-manager="txManager"> <tx:attributes> <tx:method name="get*" read-only="true" /> <tx:method name="add*" propagation="REQUIRED"/> </tx:attributes> </tx:advice> <aop:config proxy-target-class="true"> <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.junlong.demo.service.DemoService.*(..))"/> </aop:config>
在spring2.5以後聲明式事務,增長了<tx:annotation-driven />事務註解驅動,這樣能夠更靈活的控制在哪聲明事務,而且Spring2.5以後的一個核心主題就是全面的支持註解方式spring
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> <tx:annotation-driven />
1、Spring事務管理基於動態代理的兩種方式(JDKProxy、CGLIBProxy)數據庫
<tx:annotation-driven transaction-manager="transactionManager" />編程
1.若是代理對象實現了接口則採用JDK動態代理實現,JDK動態代理基於接口進行代理的,它是經過在運行時建立一個接口的實現類來完成對目標對象的代理,因此使用@Transactional 註解的類想要基於JDK動態代理進行事務控制必須實現一個接口。併發
Spring默認會在JDK動態代理和CGLIB動態代理之間進行切換,若是被代理的類實現了接口則會使用JDK動態代理,不然會使用CGLIB動態代理。框架
若是想強制使用基於類的代理,能夠設置proxy-target-class=true,這樣Spring會使用CGLIB代理,在使用CGLIB代理時@Transactional必須寫在具體的實現類上或者實現方法上,由於CGLIB動態代理是在運行時對代理對象進行繼承擴展子類,因此聲明final的類是沒法使用CGLIB代理的。優化
<xsd:attribute name="proxy-target-class" type="xsd:boolean" default="false"> <xsd:annotation> <xsd:documentation> <![CDATA[ Are class-based (CGLIB) proxies to be created? By default, standard Java interface-based proxies are created. Note: Class-based proxies require the @Transactional annotation to be defined on the concrete class. Annotations in interfaces will not work in that case (they will rather only work with interface-based proxies)! ]]> </xsd:documentation> </xsd:annotation> </xsd:attribute>
2.官方文檔介紹@Transactional 註解只有在public的方法上生效,這是由於Spring事務註解驅動是基於Spring AOP(也就是默認的方式是基於動態代理實現)實現的。
3.同一個java類內部方法調用本類內部的其餘方法並不會引發事務行爲,即便被調用方法使用@Transactional註解進行修飾,這也是由於Spring實現事務代理是默認是基於Spring AOP(也就是默認的方式是基於動態代理實現)實現的。
<tx:annotation-driven transaction-manager="transactionManager" mode="proxy" />
<xsd:attribute name="mode" default="proxy"> <xsd:annotation> <xsd:documentation> <![CDATA[ Should annotated beans be proxied using Spring's AOP framework, or should they rather be weaved with an AspectJ transaction aspect? AspectJ weaving requires spring-aspects.jar on the classpath, as well as load-time weaving (or compile-time weaving) enabled. Note: The weaving-based aspect requires the @Transactional annotation to be defined on the concrete class. Annotations in interfaces will not work in that case (they will rather only work with interface-based proxies)! ]]> </xsd:documentation> </xsd:annotation> <xsd:simpleType> <xsd:restriction base="xsd:string"> <xsd:enumeration value="proxy"/> <xsd:enumeration value="aspectj"/> </xsd:restriction> </xsd:simpleType> </xsd:attribute>
mode模式默認採用代理模式(mode="proxy"),在proxy模式中採用AOP的動態代理來進行事務管理,是在運行期間基於JDK動態代理或者GCLIB動態代理來實現,在代理模式中(mode="proxy")只有A類中方法調用另一個B類聲明@Transactional的方法纔會被代理截獲進行事務控制,在A類本類中調用本類的其餘帶有@Transactional的方法不會通過動態代理。
然而mode="aspectj"則採用靜態編織的方式(aspectj是一種編譯技術)其須要spring-aspects.jar,是在編譯期間或者載入期間對代理對象進行修改字節碼織入事務實現,因此無論你的方法的可見度(public、private)是什麼均可以實現事務管理,這跟proxy動態代理是徹底不一樣的。
<tx:annotation-driven />,實際上是Spring自定義標籤的擴展,Spring配置文件中的標籤都是基於XML Schema實現 ,spring的實現:http://www.springframework.org/schema/tx/spring-tx-4.1.xsd
Propagation Spring事務傳播行爲
事務傳播行爲 | 描述 |
REQUIRED | 若是當前沒有事務,就新建一個事務,若是已經存在一個事務中,加入到這個事務中。這是最多見的選擇 |
SUPPORTS | 支持當前事務,若是當前沒有事務,就以非事務方式執行 |
MANDATORY | 支持當前事務,若是沒有拋出異常 |
REQUIRES_NEW | 新建事務,若是當前存在事務,把當前事務掛起 |
NOT_SUPPORTED | 以非事務方式執行操做,若是當前存在事務,就把當前事務掛起 |
NEVER | 以非事務方式執行,若是當前存在事務,則拋出異常 |
NESTED | 執行在一個嵌套的事務,若是當前事務存在,執行相似REQUIRED操做 |
readOnly
若是事務是隻讀的使用該屬性,默認他是false
默認爲底層事務系統的默認超時
指定事務隔離級別,默認使用數據庫底層配置的事務隔離級別
spring2.5開始對事務註解驅動的支持 https://spring.io/blog/2008/01/28/spring-2-5-s-comprehensive-annotation-support
Spring Framework 開發參考手冊 http://shouce.jb51.net/spring/
《Spring3.x企業應用開發實戰》
配套代碼:
https://github.com/chenjunlong/spring-transaction-manager
MySQL InnoDB存儲引擎鎖概述
排它鎖又稱爲寫鎖((eXclusive lock,簡記爲X鎖)),若事務T對數據對象A加上X鎖,則只容許T讀取和修改A,其它任何事務都不能再對A加任何類型的鎖,直到T釋放A上的鎖。它防止任何其它事務獲取資源上的鎖,直到在事務的末尾將資源上的原始鎖釋放爲止。
在更新操做(INSERT、UPDATE 或 DELETE)過程當中始終應用排它鎖。
共享鎖
共享鎖又稱爲讀鎖(Share lock,簡記爲S鎖),若事務T對數據對象A加上S鎖,則其它事務只能再對A加S鎖,而不能加X鎖,直到T釋放A上的S鎖。
Record Lock:單個行記錄上的鎖
Gap Lock:間隙鎖,鎖定一個範圍,但不包含記錄自己
Next-Key Lock:Gap Lock + Record Lock,鎖定一個範圍,而且鎖定記錄自己,Record Lock老是會去鎖住索引記錄,若是表沒有設置任何一個索引,會使用隱式的主鍵進行鎖定。
InnoDB存儲引擎默認的事務隔離級別REPEATABLE READ,在事務隔離級別下,採用Next-key Locking的方式進行加鎖。
而在,READ COMMITTED下,僅採用Record Lock。
然而在 REPEATABLE READ 級別 InnoDB存儲引擎下,查詢使用含有惟一屬性時,會對Next-Key Lock 進行優化,將其降級爲Record Lock,即鎖住索引自己,而不是一個範圍。
一致性的非鎖定讀是指InnoDB存儲引擎經過行多版本控制方式讀取當前執行的時間數據庫中行的數據。
例如:
若是讀取的行正在執行DELETE或UPDATE操做,這時讀取操做不會所以等待行上的鎖釋放掉,而是去讀取該行數據的一個快照行數據。這種非鎖定讀機制極大的提升了數據庫的併發性,這是InnoDB存儲引擎默認的讀取方式,即讀取不會佔用等待數據行的鎖。
在事務隔離級別RC和RR對快照數據的定義有所不一樣,在RC下對於快照數據,非一致性鎖定讀老是讀取被鎖定行最新的一條數據快照(這種作法顯然不符合事務的隔離性),而在RR下對於快照數據,非一致性鎖定讀老是讀取事務開始時的行數據快照。
在某些業務場景下,須要顯示的對數據庫讀取操做進行加鎖操做,來保證業務邏輯數據的一致性,這種作法被稱爲一致性鎖定讀。
SELECT...FOR UPDATE 爲記錄加一個X鎖。
SELECT...LOCK IN SHARE MODE 爲記錄加一個S鎖。
必須嵌套在事務中,事務結束鎖釋放
下面主要介紹X鎖和顯示的加X鎖
CREATE TABLE `USER` (
`ID` int(11) NOT NULL AUTO_INCREMENT,
`MOBILE` varchar(11) DEFAULT NULL,
`NAME` varchar(255) DEFAULT NULL,
PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=322 DEFAULT CHARSET=utf8;
INSERT INTO `USER` VALUES ('222', '13121771001', 'test1001');
INSERT INTO `USER` VALUES ('223', '13121771002', 'test1002');
INSERT INTO `USER` VALUES ('224', '13121771003', 'test1003');
INSERT INTO `USER` VALUES ('225', '13121771004', 'test1004');
INSERT INTO `USER` VALUES ('226', '13121771005', 'test1005');
INSERT INTO `USER` VALUES ('227', '13121771006', 'test1006');
INSERT INTO `USER` VALUES ('228', '13121771007', 'test1007');
INSERT INTO `USER` VALUES ('229', '13121771008', 'test1008');
INSERT INTO `USER` VALUES ('230', '13121771009', 'test1009');
INSERT INTO `USER` VALUES ('231', '13121771010', 'test1010');
下面以主鍵爲條件鎖住一行數據
會話A | 會話B |
BEGIN; SELECT * FROM USER WHERE ID = 222 FOR UPDATE; |
從新打開一個查詢分析器,查詢相同記錄,發現行已被鎖
BEGIN; SELECT * FROM USER WHERE ID = 222 FOR UPDATE; |
查詢不相同記錄,發現行未被鎖,證實是行鎖
BEGIN; SELECT * FROM USER WHERE ID = 223 FOR UPDATE;
|
結論:以主鍵爲查詢條件的爲行鎖,不會影響其餘行,相同行查詢會產生等待。
超時異常: 1205 - Lock wait timeout exceeded; try restarting transaction
會話A | 會話B |
BEGIN; SELECT * FROM `USER` WHERE MOBILE = '13121771001' FOR UPDATE; |
從新打開一個查詢分析器,查詢不相同記錄,發現表以被鎖
BEGIN; SELECT * FROM `USER` WHERE MOBILE = '13121771002' FOR UPDATE; |
結論:以普通列查詢,未加惟一約束的狀況下,即便返回惟一一行,也會形成全表鎖。
ALTER TABLE `USER` ADD UNIQUE INDEX (`MOBILE`);
從新打開查詢分析器
會話A | 會話B |
BEGIN; SELECT * FROM `USER` WHERE MOBILE = '13121771001' FOR UPDATE |
BEGIN; SELECT * FROM `USER` WHERE MOBILE = '13121771002' FOR UPDATE |
結論:使用惟一索引和主鍵是相同效果。
查詢不到惟一行,會致使表鎖
會話A | 會話B |
BEGIN; SELECT * FROM `USER` WHERE ID = 1 FOR UPDATE; |
BEGIN; INSERT INTO `USER` VALUES (1, '13121771000', 'test1000'); |
結論:查詢不到數據行的狀況下,致使insert會失敗,這是由於MySQL沒法將其視爲行級鎖,會鎖住整表
會話A | 會話B |
BEGIN; SELECT * FROM `USER` WHERE ID = 222 FOR UPDATE; |
BEGIN; INSERT INTO `USER` VALUES (1, '13121771000', 'test1000'); |
where條件無索引,全部行被鎖
會話A | 會話B |
修改該行數據:
|
查詢其餘數據行: BEGIN; SELECT * FROM `USER` WHERE ID = 223 FOR UPDATE;
|
UPDATE `USER` SET `NAME` = 'test_*' WHERE `NAME` = 'test1001'; |
where字段是惟一索引或者主鍵只會鎖住一行數據
會話A | 會話B |
BEGIN; UPDATE `USER` SET `NAME` = 'demo1' WHERE ID = 222; |
BEGIN; SELECT * FROM `USER` WHERE ID = 223 FOR UPDATE; |
在RR級別下InnoDB數據存儲引擎行鎖的算法默認採用 NEXT-Key Lock,而在RC級別下使用的是 Record Lock,舉例說明:
會話A | 會話B |
事務未結束,該階段沒有提交 |
此時會話B,因爲RR事務隔離級別的特性,採用Next-Key Lock(前面介紹了,該算法是 Gap Lock + Record Lock) 算法,會鎖定一個區間 |
|
會話A | 會話B |
事務未結束,該階段沒有提交 |
能夠正常插入新記錄,證實此時RC級別默認採用的是行鎖機制 |
|
COMMIT; |
該操做論證了,RC事務隔離級別下,該事務是中出現不可重複讀的現象 |
|
參考資料:
《MySQL技術內幕InnoDB存儲引擎第2版》