【spring原始碼】十一、事務
3.宣告式事務都是針對於 ServiceImpl 類下方法的.
4.事務管理器基於通知(advice)的.
5.在 spring 配置檔案中配置宣告式事務
<context:property-placeholder location="classpath:db.properties,classpath:second.properties"/>
<!-- 定義資料來源bean,使用C3P0資料來源實現 -->
<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>
<!-- spring-jdbc.jar 中 -->
<!-- 使用PlatformTransactionManager介面的實現類DataSourceTransactionManager -->
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource"
ref="dataSource"></property>
</bean>
<!-- 配置宣告式事務 -->
<tx:advice id="txAdvice"
transaction-manager="txManager">
<tx:attributes>
<!-- 哪些方法需要有事務控制 -->
<!-- 方法以 ins 開頭事務管理 -->
<tx:method name="ins*" />
<tx:method name="del*" />
<tx:method name="upd*" />
<tx:method name="*" />
</tx:attributes>
</tx:advice>
<aop:config>
<!-- 切點範圍設定大一些 -->
<aop:pointcut expression="execution(*com.bjsxt.service.impl.*.*(..))"
id="mypoint" />
<aop:advisor advice-ref="txAdvice"
pointcut-ref="mypoint" />
</aop:config>
六.宣告式事務中屬性解釋
\1. name=”” 哪些方法需要有事務控制
1.1 支援*萬用字元
\2. readonly=”boolean” 是否是隻讀事務.
2.1 如果為 true,告訴資料庫此事務為只讀事務.資料化優化,會對效能有一定提升,所以只要是查詢的方法,建議使用此資料.
2.2 如果為 false(預設值),事務需要提交的事務.建議新增,刪除,修改.
\3. propagation 控制事務傳播行為.
3.1 當一個具有事務控制的方法被另一個有事務控制的方法呼叫後,需要如何管理事務(新建事務?在事務中執行?把事務掛起?報異常?)
3.2 REQUIRED (預設值): 如果當前有事務,就在事務中執行,如果當前沒有事務,新建一個事務.
3.3 SUPPORTS:如果當前有事務就在事務中執行,如果當前沒有事務,就在非事務狀態下執行.
3.4 MANDATORY:必須在事務內部執行,如果當前有事務,就在事務中執行,如果沒有事務,報錯.
3.5 REQUIRES_NEW:必須在事務中執行,如果當前沒有事務,新建事務,如果當前有事務,把當前事務掛起.
3.6 NOT_SUPPORTED:必須在非事務下執行,如果當前沒有事務,正常執行,如果當前有事務,把當前事務掛起.
3.7 NEVER:必須在非事務狀態下執行,如果當前沒有事務,正常執行,如果當前有事務,報錯.
3.8 NESTED:必須在事務狀態下執行.如果沒有事務,新建事務,如果當前有事務,建立一個巢狀事務.
\4. isolation=”” 事務隔離級別
4.1 在多執行緒或併發訪問下如何保證訪問到的資料具有完整性的.
4.2 髒讀:
4.2.1 一個事務(A)讀取到另一個事務(B)中未提交的資料,另一個事務中資料可能進行了改變,此時 A事務讀取的資料可能和資料庫中資料是不一致的,此時認為資料是髒資料,讀取髒資料過程叫做髒讀.
4.3 不可重複讀:
4.3.1 主要針對的是某行資料.(或行中某列)
4.3.2 主要針對的操作是修改操作.
4.3.3 兩次讀取在同一個事務內
4.3.4 當事務 A 第一次讀取事務後,事務 B 對事務 A 讀取的淑君進行修改,事務 A 中再次讀取的資料和之前讀取的資料不一致,過程不可重複讀.
4.4 幻讀:
4.4.1 主要針對的操作是新增或刪除
4.4.2 兩次事務的結果.
4.4.3 事務 A 按照特定條件查詢出結果,事務 B 新增了一條符合條件的資料.事務 A 中查詢的資料和資料庫中的資料不一致的,事務 A 好像出現了幻覺,這種情況稱為幻讀.
4.5 DEFAULT: 預設值,由底層資料庫自動判斷應該使用什麼隔離界別
4.6 READ_UNCOMMITTED: 可以讀取未提交資料,可能出現髒讀,不重複讀,幻讀.
4.6.1 效率最高.
4.7 READ_COMMITTED:只能讀取其他事務已提交資料.可以防止髒讀,可能出現不可重複讀和幻讀.
4.8 REPEATABLE_READ: 讀取的資料被新增鎖,防止其他事務修改此資料,可以防止不可重複讀.髒讀,可能出現幻讀.
4.9 SERIALIZABLE: 排隊操作,對整個表新增鎖.一個事務在運算元據時,另一個事務等待事務操作完成後才能操作這個表.
4.9.1 最安全的
4.9.2 效率最低的.
\5. rollback-for=”異常型別全限定路徑”
5.1 當出現什麼異常時需要進行回滾
5.2 建議:給定該屬性值.
5.2.1 手動拋異常一定要給該屬性值.
\6. no-rollback-for=””
6.1 當出現什麼異常時不滾回事務.
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
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
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd"
default-autowire="byName">
<context:property-placeholder location="classpath:db.properties,classpath:second.properties"/>
<context:component-scan base-package="com.bjsxt.service.impl"></context:component-scan>
<!-- 資料來源 -->
<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>
<!-- SqlSessinFactory物件 -->
<bean id="factory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="typeAliasesPackage" value="com.bjsxt.pojo"></property>
</bean>
<!-- 掃描器 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.bjsxt.mapper"></property>
<property name="sqlSessionFactoryBeanName" value="factory"></property>
</bean>
<!-- 注入 -->
<!-- <bean id="usersService" class="com.bjsxt.service.impl.UsersServiceImpl">
<property name=""></property>
</bean> -->
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置宣告式事務 -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<!-- 哪些方法需要有事務控制 -->
<!-- 方法以ins開頭事務管理 -->
<tx:method name="ins*" />
<tx:method name="del*"/>
<tx:method name="upd*"/>
<tx:method name="*" read-only="true"/>
</tx:attributes>
</tx:advice>
<aop:config>
<!-- 切點範圍設定大一些 -->
<aop:pointcut expression="execution(* com.bjsxt.service.impl.*.*(..))"
id="mypoint" />
<aop:advisor advice-ref="txAdvice" pointcut-ref="mypoint" />
</aop:config>
<!-- aop -->
<!-- <aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>
<bean id="mybefore" class="com.bjsxt.advice.MyBefore"></bean>
<bean id="myafter" class="com.bjsxt.advice.MyAfter"></bean>
<aop:config>
<aop:pointcut expression="execution(* com.bjsxt.service.impl.UsersServiceImpl.login(..))" id="mypoint"/>
<aop:advisor advice-ref="mybefore" pointcut-ref="mypoint"/>
<aop:advisor advice-ref="myafter" pointcut-ref="mypoint"/>
</aop:config> -->
</beans>
Spring 事務管理分為程式設計式和宣告式的兩種方式。程式設計式事務指的是通過編碼方式實現事務;宣告式事務基於 AOP,將具體業務邏輯與事務處理解耦。宣告式事務管理使業務程式碼邏輯不受汙染, 因此在實際使用中宣告式事務用的比較多。
宣告式事務有兩種方式,一種是在配置檔案(xml)中做相關的事務規則宣告,另一種是基於 @Transactional
註解的方式。
需要明確幾點:
1、預設配置下 Spring 只會回滾執行時、未檢查異常(繼承自 RuntimeException 的異常)或者 Error。
2、@Transactional 註解只能應用到 public 方法才有效。
3、@Transactional 註解可以被應用於介面定義和介面方法、類定義和類的 public 方法上。然而僅僅 @Transactional 註解的出現不足以開啟事務行為,它僅僅是一種後設資料,能夠被可以識別 @Transactional 註解和上述的配置適當的具有事務行為的beans所使用。其實是 <tx:annotation-driven/>
元素的出現開啟了事務行為。
4、註解不可以繼承,建議在具體的類(或類的方法)上使用 @Transactional 註解,而不要使用在類所要實現的任何介面上。當然可以在介面上使用 @Transactional 註解,但是這將只有當你設定了基於介面的代理時它才生效。
Spring配置檔案中關於事務配置總是由三個組成部分,分別是DataSource、TransactionManager和代理機制這三部分,無論哪種配置方式,一般變化的只是代理機制這部分。
DataSource、TransactionManager這兩部分只是會根據資料訪問方式有所變化,比如使用Hibernate進行資料訪問時,DataSource實際為SessionFactory,TransactionManager的實現為HibernateTransactionManager。
事務的TransactionManager事務管理器總共有5種,與DataSource關聯關係如圖所示:
根據代理機制的不同,總結了五種Spring事務的配置方式,配置檔案如下:
https://www.cnblogs.com/jing99/p/11495252.html
1 情景前要
0 情景:張三給李四轉賬100
-- 檢視全域性和當前session的事務隔離級別
SELECT @@GLOBAL.tx_isolation, @@tx_isolation;
-- 設定隔離級別:SET SESSION TRANSACTION ISOLATION LEVEL
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
-- level: READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE
-- session 1:A開始事務後進行轉賬。先把自己賬戶-100,然後給對方賬戶+100
START TRANSACTION;
UPDATE t_user SET amount = amount - 100 WHERE username = 'A';
UPDATE t_user SET amount = amount + 100 WHERE username = 'B';
COMMIT;
-- ROLLBACK;
-- session 2:B開始事務後進行查詢兩次。查詢全部與查自己
START TRANSACTION;
SELECT * FROM t_user;
SELECT * FROM t_user where username="B"
COMMIT;
1.1 隔離級別情景
1.1.1 隔離級別情景:可重複讀
mysql預設的可重複讀:當session 2只進行了select,而未commit時,session1即使commit了,session2查到的內容還是原來的。
1.1.2 隔離級別情景:髒讀
A開始事務後只進行了-操作,還沒進行+操作。但B查的時候沒收到100元,但A的賬戶已經少了100。READ UNCOMMITED
1.1.3 隔離級別情景:SERIALIZABLE
所有的事務必須是排隊執行,A轉完提交後B才可以查。
private static final Logger LOG= LoggerFactory.getLogger(LocalJDBCTransApplication.class);
LOG.error(e.getLocalizedMessage());
jdbc程式碼測試隔離機制
jdbc常見的7步步驟:
package com.iot.mybatis.jdbc;
//import java.sql.*;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* 可以在每個方法內都進行一整套過程。可想而知很麻煩
*/
public class JdbcTest {
public static void main(String[] args) {
//資料庫連線
Connection connection = null;
//預編譯的Statement,使用預編譯的Statement提高資料庫效能
PreparedStatement preparedStatement = null;
//結果集
ResultSet resultSet = null;
try {
//載入資料庫驅動
Class.forName("com.mysql.jdbc.Driver");
//通過驅動管理類獲取資料庫連結
connection = DriverManager.getConnection("jdbc:mysql://120.25.162.238:3306/mybatis001?characterEncoding=utf-8", "root", "123");
//定義sql語句 ?表示佔位符
String sql = "select * from user where username = ?";
//獲取預處理statement
preparedStatement = connection.prepareStatement(sql);
//設定引數,第一個引數為sql語句中引數的序號(從1開始),第二個引數為設定的引數值
preparedStatement.setString(1, "王五");
//向資料庫發出sql執行查詢,查詢出結果集
resultSet = preparedStatement.executeQuery();
//遍歷查詢結果集
while(resultSet.next()){
System.out.println(resultSet.getString("id")+" "+resultSet.getString("username"));
}
} catch (Exception e) {
e.printStackTrace();
}finally{
//釋放資源
if(resultSet!=null){//釋放結果集
try {
resultSet.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(preparedStatement!=null){//釋放stmt
try {
preparedStatement.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(connection!=null){//釋放連結
try {
connection.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
現在我們模擬出兩個java程式碼
需要用到的xml如下。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mav.example</groupId>
<artifactId>local-tran-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>local-tran-jdbc</name>
<description>Example project for JDBC transaction management.</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.11</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.39</version>
</dependency>
</dependencies>
</project>
第一個java
jdbc的隔離
package com.imooc.example.localtranjdbc;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class LocalTranJdbcApplication {
private static final Logger LOG = LoggerFactory.getLogger(LocalTranJdbcApplication.class);//用日誌去列印,而不是用system.out.print
public static void main(String[] args) throws SQLException {
String plusAmountSQL = "UPDATE T_USER SET amount = amount + 100 WHERE username = ?";
String minusAmountSQL = "UPDATE T_USER SET amount = amount - 100 WHERE username = ?";
Connection dbConnection = getDBConnection();
LOG.debug("Begin");
dbConnection.setAutoCommit(false);//關閉自動提交//相當於開啟一個事務
// 設定statement
PreparedStatement plusAmountPS = dbConnection.prepareStatement(plusAmountSQL);
plusAmountPS.setString(1, "SuperMan");//SuperMan加錢。BatMan減錢
// 執行
plusAmountPS.executeUpdate();
//simulateError();//模擬出錯的情況//讓這個程式只執行一半
PreparedStatement minusAmountPS = dbConnection.prepareStatement(minusAmountSQL);
minusAmountPS.setString(1, "BatMan");
minusAmountPS.executeUpdate();
// 提交
dbConnection.commit();
LOG.debug("Done!");
plusAmountPS.close();
minusAmountPS.close();
dbConnection.close();//自動rollback
}
private static void simulateError() throws SQLException {
throw new SQLException("Simulate some error!");
}
private static Connection getDBConnection() throws SQLException {
String DB_DRIVER = "com.mysql.jdbc.Driver";
String DB_CONNECTION = "jdbc:mysql://localhost:3306/dist_tran_course";
String DB_USER = "mt";
String DB_PASSWORD = "111111";
try {
Class.forName(DB_DRIVER);
} catch (ClassNotFoundException e) {
LOG.error(e.getMessage());
}
return DriverManager.getConnection(DB_CONNECTION, DB_USER, DB_PASSWORD);
}
}
接下來需要使用debug
第二個java
package com.imooc.example.localtranjdbc;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.*;
public class LocalTranJdbcApplication2 {
private static final Logger LOG = LoggerFactory.getLogger(LocalTranJdbcApplication2.class);
public static void main(String[] args) throws SQLException {
String sql = "SELECT * FROM T_USER FOR UPDATE";
String plusAmountSQL = "UPDATE T_USER SET amount = ? WHERE username = ?";
Connection dbConnection = getDBConnection();
LOG.debug("Begin session2");
PreparedStatement queryPS = dbConnection.prepareStatement(sql);
ResultSet rs = queryPS.executeQuery();
Long superManAmount = 0L;
while (rs.next()) {
String name = rs.getString(2);
Long amount = rs.getLong(3);
LOG.info("{} has amount:{}", name, amount);
if (name.equals("SuperMan")) {
superManAmount = amount;
}
}
PreparedStatement updatePS = dbConnection.prepareStatement(plusAmountSQL);
updatePS.setLong(1, superManAmount + 100);
updatePS.setString(2, "SuperMan");
updatePS.executeUpdate();
LOG.debug("Done session2!");
queryPS.close();
updatePS.close();
dbConnection.close();
}
private static Connection getDBConnection() throws SQLException {
String DB_DRIVER = "com.mysql.jdbc.Driver";
String DB_CONNECTION = "jdbc:mysql://localhost:3306/dist_tran_course";
String DB_USER = "mt";
String DB_PASSWORD = "111111";
try {
Class.forName(DB_DRIVER);
} catch (ClassNotFoundException e) {
LOG.error(e.getMessage());
}
return DriverManager.getConnection(DB_CONNECTION, DB_USER, DB_PASSWORD);
}
}
依賴
- jdbc(或orm)
- aspectJ
- spring-tx
事務的幾種配置方式
https://www.cnblogs.com/myseries/p/10834172.html
(1)基於 TransactionProxyFactoryBean的宣告式事務管理
- 配置事務管理器bean,事務管理器管理DataSource
- 配置TransactionProxyFactoryBean
- 指定事務管理器
- 指定target,即方法
- 指定transactionAttributes,即事務的屬性
<context:property-placeholder location="classpath:jdbc.properties"/>
<!-- 註冊資料來源 C3P0 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" >
<property name="driverClass" value="${jdbc.driverClass}"></property>
<property name="jdbcUrl" value="${jdbc.url}"></property>
<property name="user" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<bean id="accountDao" class="transaction.test2.dao.AccountDaoImpl">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="stockDao" class="transaction.test2.dao.StockDaoImpl">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="buyStockService" class="transaction.test2.service.BuyStockServiceImpl">
<property name="accountDao" ref="accountDao"></property>
<property name="stockDao" ref="stockDao"></property>
</bean>
<!-- 事務管理器 -->
<bean id="myTracnsactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 事務代理工廠 -->
<!-- 生成事務代理物件 -->
<bean id="serviceProxy" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager" ref="myTracnsactionManager"></property>
<property name="target" ref="buyStockService"></property>
<property name="transactionAttributes">
<props>
<!-- 主要 key 是方法
ISOLATION_DEFAULT 事務的隔離級別
PROPAGATION_REQUIRED 傳播行為
-->
<prop key="add*">ISOLATION_DEFAULT,PROPAGATION_REQUIRED</prop>
<!-- -Exception 表示發生指定異常回滾,+Exception 表示發生指定異常提交 -->
<prop key="buyStock">ISOLATION_DEFAULT,PROPAGATION_REQUIRED,-BuyStockException</prop>
</props>
</property>
</bean>
(2)基於 @Transactional 的宣告式事務管理
- 配置事務管理器bean,事務管理器管理DataSource
- 將事務管理器配置到事務驅動中
<tx:annotation-driven>
中
<context:property-placeholder location="classpath:jdbc.properties"/>
<!-- 註冊資料來源 C3P0 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" >
<property name="driverClass" value="${jdbc.driverClass}"></property>
<property name="jdbcUrl" value="${jdbc.url}"></property>
<property name="user" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<bean id="accountDao" class="transaction.test3.dao.AccountDaoImpl">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="stockDao" class="transaction.test3.dao.StockDaoImpl">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="buyStockService" class="transaction.test3.service.BuyStockServiceImpl">
<property name="accountDao" ref="accountDao"></property>
<property name="stockDao" ref="stockDao"></property>
</bean>
<!-- 事務管理器 -->
<bean id="myTracnsactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 啟用事務註解 -->
<tx:annotation-driven transaction-manager="myTracnsactionManager"/>
(3)基於Aspectj AOP配置事務
- 配置事務管理器bean,事務管理器管理DataSource
- 配置
<aop:config>
- 指定切點
<aop:pointcut>
- 指定
<aop:advisor>
,他可以ref bean</tx:advice>
</tx:advice>
bean來指定方法+事務屬性
- 指定切點
<context:property-placeholder location="classpath:jdbc.properties"/>
<!-- 註冊資料來源 C3P0 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" >
<property name="driverClass" value="${jdbc.driverClass}"></property>
<property name="jdbcUrl" value="${jdbc.url}"></property>
<property name="user" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<bean id="accountDao" class="transaction.test4.dao.AccountDaoImpl">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="stockDao" class="transaction.test4.dao.StockDaoImpl">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="buyStockService" class="transaction.test4.service.BuyStockServiceImpl">
<property name="accountDao" ref="accountDao"></property>
<property name="stockDao" ref="stockDao"></property>
</bean>
<!-- 事務管理器 -->
<bean id="myTracnsactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<tx:advice id="txAdvice" transaction-manager="myTracnsactionManager">
<tx:attributes>
<!-- 為連線點指定事務屬性 -->
<tx:method name="add*" isolation="DEFAULT" propagation="REQUIRED"/>
<tx:method name="buyStock" isolation="DEFAULT" propagation="REQUIRED" rollback-for="BuyStockException"/>
</tx:attributes>
</tx:advice>
<aop:config>
<!-- 切入點配置 -->
<aop:pointcut expression="execution(* *..service.*.*(..))" id="point"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="point"/>
</aop:config>
慕課
事務管理抽象
- PlatformTransactionManager
- TransactionDefinition
- TransactionStatus
Spring事務抽象–事務管理器
public interface PlatformTransactionManager{
TransactionStatus getTransaction(TransactionDefinition definition) throws TransationException;
void commit(TransactionStatus status) throws TransationException;
void rollback(TransactionStatus status) throws TransationException;
}
Spring事務抽象–事務定義
public interface TransactionDefinition{
int getPropagationBehavior();//傳播屬性
int getIsolationLevel();//隔離級別
String getName();//事務名字
int getTimeout();//超時時間
boolean isReadOnly();
}
Spring事務抽象–事務隔離機制
TransactionDefinition.ISOLATION_DEFAULT ;//資料庫用的隔離機制是什麼,我用的隔離機制就是什麼
TransactionDefinition.ISOLATION_READ_COMMITTED;//一個事務可以讀取另外一個事務已經提交的資料
TransactionDefinition.ISOLATION_READ_UNCOMMITTED;//一個事務可以讀取另外一個事務還沒提交的資料(髒讀)
TransactionDefinition.ISOLATION_REPEATABLE_READ;//可重複讀//讀期間不管別人改的資料
TransactionDefinition.ISOLATION_SERIALIZABLE;//排隊,防止併發//效率有影響
Spring事務抽象–事務傳播機制:
兩個方法在各自的事務中,互相呼叫,呼叫時候事務是怎麼傳播的
TransactionDefinition.PROPAGATION_REQUIRED;//(Default)//A調B,A在事務中,B
TransactionDefinition.PROPAGATION_SUPPORT;
TransactionDefinition.PROPAGATION_MANDATORY;
TransactionDefinition.PROPAGATION_REQUIRES_NEW;
TransactionDefinition.PROPAGATION_NOT_SUPPORTED;
TransactionDefinition.PROPAGATION_NEVER;
TransactionDefinition.PROPAGATION_NESTED;
Spring事務抽象–事務管理器
public interface TransationStatus extends SavepointManager{
boolean isNewTransaction();
boolean hasSavapoint();
void setRollbackOnly();
boolean isRollbackOnly();
boolean isCompleted();
}
第8章 宣告式事務管理
8.1 事務概述
是以一種可靠、一致的方式,訪問和運算元據庫中資料的程式單元。
-
在JavaEE企業級開發的應用領域,為了保證資料的完整性和一致性,必須引入資料庫事務的概念,所以事務管理是企業級應用程式開發中必不可少的技術。
-
事務就是一組由於邏輯上緊密關聯而合併成一個整體(工作單元)的多個資料庫操作,這些操作要麼都執行,要麼都不執行。
-
事務的四個關鍵屬性(ACID)
①原子性(atomicity):“原子”的本意是“不可再分”,事務的原子性表現為一個事務中涉及到的多個操作在邏輯上缺一不可。事務的原子性要求事務中的所有操作要麼都執行,要麼都不執行。
②一致性(consistency):“一致”指的是資料的一致,具體是指:所有資料都處於滿足業務規則的一致性狀態。一致性原則要求:一個事務中不管涉及到多少個操作,都必須保證事務執行之前資料是正確的,事務執行之後資料仍然是正確的。如果一個事務在執行的過程中,其中某一個或某幾個操作失敗了,則必須將其他所有操作撤銷,將資料恢復到事務執行之前的狀態,這就是回滾。
③隔離性(isolation):在應用程式實際執行過程中,事務往往是併發執行的,所以很有可能有許多事務同時處理相同的資料,因此每個事務都應該與其他事務隔離開來,防止資料損壞。隔離性原則要求多個事務在併發執行過程中不會互相干擾。
④永續性(durability):永續性原則要求事務執行完成後,對資料的修改永久的儲存下來,不會因各種系統錯誤或其他意外情況而受到影響。通常情況下,事務對資料的修改應該被寫入到持久化儲存器中。
例子:張三給李四轉賬,李四在查餘額。
張三的內容包括:給自己-100,給李四+100。原子性指的是不會出現給自己-100而李四沒有+100的情況。一致性是指總的餘額不會變化。
隔離性是指張三在轉,李四在查。張三在轉一般過程中李四查餘額李四能否看到轉賬的結果
永續性:只有張三提交了,才回永久儲存到資料庫中。
MYSQL:
BEGIN TRANSACTION:
UPDATE t_user SET amount=amount-100 where username='zhangsan';
UPDATE t_user SET amount=amount+100 where username='lisi';
COMMIT
或--ROLLBACK
8.2 Spring事務管理
事務:分為程式設計式事務、宣告式事務
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-7eJsBbdC-1608402277177)(https://raw.githubusercontent.com/FermHan/tuchuang/master/20191202003222.png)]
在java中不可能用sql去進行事務管理,得用高等的API去進行管理。業務系統一般都有一個ResourceManager(TransactionManager),去進行事務管理。ResourceManager會管理Resource,Resource就是資料庫的資源。ResourceManager會使用代理等方式去呼叫某種事務管理的實現類去進行事務管理,而Resource的具體實現類可能是DB Connection
java JDBC
Connection conn = getConnection();
conn.setAutoCommit(false);//關閉自動提交
PreparedStatement stnt1 = conn.prepareStatement(updateUser1SQL);
stmt1.executeUpdate();
PreparedStatement stnt2 = conn.prepareStatement(updateUser2SQL);
stmt2.executeUpdate();
conn.commit();// 提交或者回滾
//conn.rollback();
8.2.1程式設計式事務管理
- 使用原生的JDBC API進行事務管理:由程式設計師程式設計事務控制程式碼.
①獲取資料庫連線Connection物件
②取消事務的自動提交
③執行操作
④正常完成操作時手動提交事務
⑤執行失敗時回滾事務
⑥關閉相關資源
- 評價
使用原生的JDBC API實現事務管理是所有事務管理方式的基石,同時也是最典型的程式設計式事務管理。程式設計式事務管理需要將事務管理程式碼嵌入到業務方法中來控制事務的提交和回滾。在使用程式設計的方式管理事務時,必須在每個事務操作中包含額外的事務 管理程式碼。相對於核心業務而言,事務管理的程式碼顯然屬於非核心業務,如果多個模組都使用同樣模式的程式碼進行事務管理,顯然會造成較大程度的程式碼冗餘。
8.2.2 宣告式事務管理
宣告式事務:事務控制程式碼已經由 spring 寫好,程式設計師只需要宣告出哪些方法需要進行事務控制和如何進行事務控制。功能,業務,事務。宣告式事務是針對於ServiceImpl類下方法的,一個業務對應一個事務。事務管理器是基於通知的
大多數情況下宣告式事務比程式設計式事務管理更好:它將事務管理程式碼從業務方法中分離出來,以宣告的方式來實現事務管理。
事務管理程式碼的固定模式作為一種橫切關注點,可以通過AOP方法模組化,進而藉助Spring AOP框架實現宣告式事務管理。
Spring在不同的事務管理API之上定義了一個抽象層,通過配置的方式使其生效,從而讓應用程式開發人員不必瞭解事務管理API的底層實現細節,就可以使用Spring的事務管理機制。
Spring既支援程式設計式事務管理,也支援宣告式的事務管理。
public class UserServiceImpl implements UserService{
@Override
public int insert(Users users){// 把這個方法配置為切點。
return 0;
}
}
在applicationContext.xml中配置
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
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
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd"
default-autowire="byName">
<context:property-placeholder location="classpath:db.properties,classpath:second.properties"/>
<context:component-scan base-package="com.bjsxt.service.impl"></context:component-scan>
<!-- 資料來源 -->
<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>
<!-- SqlSessinFactory物件 -->
<bean id="factory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="typeAliasesPackage" value="com.bjsxt.pojo"></property>
</bean>
<!-- 掃描器 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.bjsxt.mapper"></property>
<property name="sqlSessionFactoryBeanName" value="factory"></property>
</bean>
<!-- 注入 -->
<!-- <bean id="usersService" class="com.bjsxt.service.impl.UsersServiceImpl">
<property name=""></property>
</bean> -->
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置事務管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:advice id="txAdvice" transactiontransaction-manager="transactionManager">
<tx:attributes>
<tx:method name="insert"/>下面的切點寫大點,這裡控制到底哪裡真正有事務
</tx:attributes>
</tx:advice>
<!-- 啟用事務註解 ,如果上面的是id不是transactionManager,則要一致;如果是transactionManager,tx:annotation-driven在tx:annotation-driven中可以省略-->
<tx:annotation-driven transaction-manager="transactionManager"/>
<aop:config>
<aop:pointcut expression="execution(* com.bjsxtt.service.impl.*.*(..))" id="mypoint"/>
<aop:advisor advice-ref="txManager" pointcut-ref="mypoint"/>
</aop:config>
maven依賴
spring-jdbc
8.2.3 Spring提供的事務管理器
Spring從不同的事務管理API中抽象出了一整套事務管理機制,讓事務管理程式碼從特定的事務技術中獨立出來。開發人員通過配置的方式進行事務管理,而不必瞭解其底層是如何實現的。事務管理器是基於AOP通知的
Spring的核心事務管理抽象是PlatformTransactionManager。它為事務管理封裝了一組獨立於技術的方法。無論使用Spring的哪種事務管理策略(程式設計式或宣告式),事務管理器都是必須的。
事務管理器可以以普通的bean的形式宣告在Spring IOC容器中。
- 提供統一的API介面支援不同的資源,在spring.tx庫中
- 提供宣告式事務管理。。使用AOP,而不是嵌入到業務程式碼中
- 方便的與spring框架繼承
- 多個資源的事務管理、同步
8.2.4事務管理器的主要實現
介面:PlatformTransactionManager
-
DataSourceTransactionManager
:在應用程式中只需要處理一個資料來源,而且通過JDBC存取。 -
JtaTransactionManager
:在JavaEE應用伺服器上用JTA(Java Transaction API)進行事務管理 -
HibernateTransactionManager
:用Hibernate框架存取資料庫
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-p8L5aZL2-1608402277179)(https://raw.githubusercontent.com/FermHan/tuchuang/master/20191114153555.png)]
Spring並不直接管理事務,而是提供了多種事務管理器,他們將事務管理的職責委託給Hibernate或者JTA等持久化機制所提供的相關平臺框架的事務來實現。
Spring事務管理器的介面是org.springframework.transaction.PlatformTransactionManager,通過這個介面,Spring為各個平臺如JDBC、Hibernate等都提供了對應的事務管理器,但是具體的實現就是各個平臺自己的事情了。此介面的內容如下:
Public interface PlatformTransactionManager()...{
// 由TransactionDefinition得到TransactionStatus物件
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
// 提交
Void commit(TransactionStatus status) throws TransactionException;
// 回滾
Void rollback(TransactionStatus status) throws TransactionException;
}
從這裡可知具體的具體的事務管理機制對Spring來說是透明的,它並不關心那些,那些是對應各個平臺需要關心的,所以Spring事務管理的一個優點就是為不同的事務API提供一致的程式設計模型,如JTA、JDBC、Hibernate、JPA。下面分別介紹各個平臺框架實現事務管理的機制。
2.1.1 JDBC事務
如果應用程式中直接使用JDBC來進行持久化,DataSourceTransactionManager會為你處理事務邊界。為了使用DataSourceTransactionManager,你需要使用如下的XML將其裝配到應用程式的上下文定義中:
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
實際上,DataSourceTransactionManager是通過呼叫java.sql.Connection來管理事務,而後者是通過DataSource獲取到的。通過呼叫連線的commit()方法來提交事務,同樣,事務失敗則通過呼叫rollback()方法進行回滾。
2.1.2 Hibernate事務
如果應用程式的持久化是通過Hibernate實習的,那麼你需要使用HibernateTransactionManager。對於Hibernate3,需要在Spring上下文定義中新增如下的``宣告:
<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
sessionFactory屬性需要裝配一個Hibernate的session工廠,HibernateTransactionManager的實現細節是它將事務管理的職責委託給org.hibernate.Transaction物件,而後者是從Hibernate Session中獲取到的。當事務成功完成時,HibernateTransactionManager將會呼叫Transaction物件的commit()方法,反之,將會呼叫rollback()方法。
2.1.3 Java持久化API事務(JPA)
Hibernate多年來一直是事實上的Java持久化標準,但是現在Java持久化API作為真正的Java持久化標準進入大家的視野。如果你計劃使用JPA的話,那你需要使用Spring的JpaTransactionManager來處理事務。你需要在Spring中這樣配置JpaTransactionManager:
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
JpaTransactionManager只需要裝配一個JPA實體管理工廠(javax.persistence.EntityManagerFactory介面的任意實現)。JpaTransactionManager將與由工廠所產生的JPA EntityManager合作來構建事務。
2.1.4 Java原生API事務
如果你沒有使用以上所述的事務管理,或者是跨越了多個事務管理源(比如兩個或者是多個不同的資料來源),你就需要使用JtaTransactionManager:
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">
<property name="transactionManagerName" value="java:/TransactionManager" />
</bean>
JtaTransactionManager將事務管理的責任委託給javax.transaction.UserTransaction和javax.transaction.TransactionManager物件,其中事務成功完成通過UserTransaction.commit()方法提交,事務失敗通過UserTransaction.rollback()方法回滾。
2.2 基本事務屬性的定義TransactionDefinition
上面講到的事務管理器介面PlatformTransactionManager通過getTransaction(TransactionDefinition definition)方法來得到事務,這個方法裡面的引數是TransactionDefinition類,這個類就定義了一些基本的事務屬性。
那麼什麼是事務屬性呢?事務屬性可以理解成事務的一些基本配置,描述了事務策略如何應用到方法上。事務屬性包含了5個方面,如圖所示:
而TransactionDefinition介面內容如下:
public interface TransactionDefinition {
int getPropagationBehavior(); // 返回事務的傳播行為
int getIsolationLevel(); // 返回事務的隔離級別,事務管理器根據它來控制另外一個事務可以看到本事務內的哪些資料
int getTimeout(); // 返回事務必須在多少秒內完成
boolean isReadOnly(); // 事務是否只讀,事務管理器能夠根據這個返回值進行優化,確保事務是隻讀的
}
我們可以發現TransactionDefinition正好用來定義事務屬性,下面詳細介紹一下各個事務屬性。
2.2.1 傳播行為
事務的第一個方面是傳播行為(propagation behavior)。當事務方法被另一個事務方法呼叫時,必須指定事務應該如何傳播。例如:方法可能繼續在現有事務中執行,也可能開啟一個新事務,並在自己的事務中執行。Spring定義了七種傳播行為:
傳播行為 | 含義 |
---|---|
PROPAGATION_REQUIRED | 表示當前方法必須執行在事務中 。如果當前事務存在,方法將會在該事務中執行。否則,會啟動一個新的事務 |
PROPAGATION_SUPPORTS | 表示當前方法不需要事務上下文,但是如果存在當前事務的話,那麼該方法會在這個事務中執行 |
PROPAGATION_MANDATORY | 表示該方法必須在事務中執行,如果當前事務不存在,則會丟擲一個異常 |
PROPAGATION_REQUIRED_NEW | 表示當前方法必須執行在它自己的事務中 。一個新的事務將被啟動。如果存在當前事務,在該方法執行期間,當前事務會被掛起 。如果沒有當前事務,則新建 事務。如果使用JTATransactionManager的話,則需要訪問TransactionManager |
PROPAGATION_NOT_SUPPORTED | 表示該方法不應該執行在事務中。如果存在當前事務,在該方法執行期間,當前事務將被掛起。如果使用JTATransactionManager的話,則需要訪問TransactionManager |
PROPAGATION_NEVER | 表示當前方法不應該執行在事務上下文中。如果當前正有一個事務在執行,則會丟擲異常 |
PROPAGATION_NESTED | 表示如果當前已經存在一個事務,那麼該方法將會在巢狀事務中執行。巢狀的事務可以獨立於當前事務進行單獨地提交或回滾。如果當前事務不存在,那麼其行為與PROPAGATION_REQUIRED一樣。注意各廠商對這種傳播行為的支援是有所差異的。可以參考資源管理器的文件來確認它們是否支援巢狀事務 |
注:以下具體講解傳播行為的內容參考自Spring事務機制詳解
(1)PROPAGATION_REQUIRED 如果存在一個事務,則支援當前事務。如果沒有事務則開啟一個新的事務。
//事務屬性 PROPAGATION_REQUIRED
methodA{
……
methodB();
……
}
//事務屬性 PROPAGATION_REQUIRED
methodB{
……
}
使用spring宣告式事務,spring使用AOP來支援宣告式事務,會根據事務屬性,自動在方法呼叫之前決定是否開啟一個事務,並在方法執行之後決定事務提交或回滾事務。
單獨呼叫methodB方法:
main{
methodB();
}
相當於
Main{
Connection con=null;
try{
con = getConnection();
con.setAutoCommit(false);
//方法呼叫
methodB();
//提交事務
con.commit();
} Catch(RuntimeException ex) {
//回滾事務
con.rollback();
} finally {
//釋放資源
closeCon();
}
}
Spring保證在methodB方法中所有的呼叫都獲得到一個相同的連線。在呼叫methodB時,沒有一個存在的事務,所以獲得一個新的連線,開啟了一個新的事務。
單獨呼叫MethodA時,在MethodA內又會呼叫MethodB.
執行效果相當於:
main{
Connection con = null;
try{
con = getConnection();
methodA();
con.commit();
} catch(RuntimeException ex) {
con.rollback();
} finally {
closeCon();
}
}
呼叫MethodA時,環境中沒有事務,所以開啟一個新的事務。當在MethodA中呼叫MethodB時,環境中已經有了一個事務,所以methodB就加入當前事務。
(2)PROPAGATION_SUPPORTS 如果存在一個事務,支援當前事務。如果沒有事務,則非事務的執行。但是對於事務同步的事務管理器,PROPAGATION_SUPPORTS與不使用事務有少許不同。
//事務屬性 PROPAGATION_REQUIRED
methodA(){
methodB();
}
//事務屬性 PROPAGATION_SUPPORTS
methodB(){
……
}
單純的呼叫methodB時,methodB方法是非事務的執行的。當呼叫methdA時,methodB則加入了methodA的事務中,事務地執行。
(3)PROPAGATION_MANDATORY 如果已經存在一個事務,支援當前事務。如果沒有一個活動的事務,則丟擲異常。
//事務屬性 PROPAGATION_REQUIRED
methodA(){
methodB();
}
//事務屬性 PROPAGATION_MANDATORY
methodB(){
……
}
當單獨呼叫methodB時,因為當前沒有一個活動的事務,則會丟擲異常throw new IllegalTransactionStateException(“Transaction propagation ‘mandatory’ but no existing transaction found”);當呼叫methodA時,methodB則加入到methodA的事務中,事務地執行。
(4)PROPAGATION_REQUIRES_NEW 總是開啟一個新的事務。如果一個事務已經存在,則將這個存在的事務掛起。
//事務屬性 PROPAGATION_REQUIRED
methodA(){
doSomeThingA();
methodB();
doSomeThingB();
}
//事務屬性 PROPAGATION_REQUIRES_NEW
methodB(){
……
}
呼叫A方法:
main(){
methodA();
}
相當於
main(){
TransactionManager tm = null;
try{
//獲得一個JTA事務管理器
tm = getTransactionManager();
tm.begin();//開啟一個新的事務
Transaction ts1 = tm.getTransaction();
doSomeThing();
tm.suspend();//掛起當前事務
try{
tm.begin();//重新開啟第二個事務
Transaction ts2 = tm.getTransaction();
methodB();
ts2.commit();//提交第二個事務
} Catch(RunTimeException ex) {
ts2.rollback();//回滾第二個事務
} finally {
//釋放資源
}
//methodB執行完後,恢復第一個事務
tm.resume(ts1);
doSomeThingB();
ts1.commit();//提交第一個事務
} catch(RunTimeException ex) {
ts1.rollback();//回滾第一個事務
} finally {
//釋放資源
}
}
在這裡,我把ts1稱為外層事務,ts2稱為內層事務。從上面的程式碼可以看出,ts2與ts1是兩個獨立的事務,互不相干。Ts2是否成功並不依賴於 ts1。如果methodA方法在呼叫methodB方法後的doSomeThingB方法失敗了,而methodB方法所做的結果依然被提交。而除了 methodB之外的其它程式碼導致的結果卻被回滾了。使用PROPAGATION_REQUIRES_NEW,需要使用 JtaTransactionManager作為事務管理器。
(5)PROPAGATION_NOT_SUPPORTED 總是非事務地執行,並掛起任何存在的事務。使用PROPAGATION_NOT_SUPPORTED,也需要使用JtaTransactionManager作為事務管理器。(程式碼示例同上,可同理推出)
(6)PROPAGATION_NEVER 總是非事務地執行,如果存在一個活動事務,則丟擲異常。
(7)PROPAGATION_NESTED如果一個活動的事務存在,則執行在一個巢狀的事務中. 如果沒有活動事務, 則按TransactionDefinition.PROPAGATION_REQUIRED 屬性執行。這是一個巢狀事務,使用JDBC 3.0驅動時,僅僅支援DataSourceTransactionManager作為事務管理器。需要JDBC 驅動的java.sql.Savepoint類。有一些J他的事務管理器實現可能也提供了同樣的功能。使用PROPAGATION_NESTED,還需要把PlatformTransactionManager的nestedTransactionAllowed屬性設為true;而 nestedTransactionAllowed屬性值預設為false。
//事務屬性 PROPAGATION_REQUIRED
methodA(){
doSomeThingA();
methodB();
doSomeThingB();
}
//事務屬性 PROPAGATION_NESTED
methodB(){
……
}
如果單獨呼叫methodB方法,則按REQUIRED屬性執行。如果呼叫methodA方法,相當於下面的效果:
main(){
Connection con = null;
Savepoint savepoint = null;
try{
con = getConnection();
con.setAutoCommit(false);
doSomeThingA();
savepoint = con2.setSavepoint();
try{
methodB();
} catch(RuntimeException ex) {
con.rollback(savepoint);
} finally {
//釋放資源
}
doSomeThingB();
con.commit();
} catch(RuntimeException ex) {
con.rollback();
} finally {
//釋放資源
}
}
當methodB方法呼叫之前,呼叫setSavepoint方法,儲存當前的狀態到savepoint。如果methodB方法呼叫失敗,則恢復到之前儲存的狀態。但是需要注意的是,這時的事務並沒有進行提交,如果後續的程式碼(doSomeThingB()方法)呼叫失敗,則回滾包括methodB方法的所有操作。
巢狀事務一個非常重要的概念就是內層事務依賴於外層事務。外層事務失敗時,會回滾內層事務所做的動作。而內層事務操作失敗並不會引起外層事務的回滾。
PROPAGATION_NESTED 與PROPAGATION_REQUIRES_NEW的區別:它們非常類似,都像一個巢狀事務,如果不存在一個活動的事務,都會開啟一個新的事務。使用 PROPAGATION_REQUIRES_NEW時,內層事務與外層事務就像兩個獨立的事務一樣,一旦內層事務進行了提交後,外層事務不能對其進行回滾。兩個事務互不影響。兩個事務不是一個真正的巢狀事務。同時它需要JTA事務管理器的支援。
使用PROPAGATION_NESTED時,外層事務的回滾可以引起內層事務的回滾。而內層事務的異常並不會導致外層事務的回滾,它是一個真正的巢狀事務。DataSourceTransactionManager使用savepoint支援PROPAGATION_NESTED時,需要JDBC 3.0以上驅動及1.4以上的JDK版本支援。其它的JTA TrasactionManager實現可能有不同的支援方式。
PROPAGATION_REQUIRES_NEW 啟動一個新的, 不依賴於環境的 “內部” 事務. 這個事務將被完全 commited 或 rolled back 而不依賴於外部事務, 它擁有自己的隔離範圍, 自己的鎖, 等等. 當內部事務開始執行時, 外部事務將被掛起, 內務事務結束時, 外部事務將繼續執行。
另一方面, PROPAGATION_NESTED 開始一個 “巢狀的” 事務, 它是已經存在事務的一個真正的子事務. 潛套事務開始執行時, 它將取得一個 savepoint. 如果這個巢狀事務失敗, 我們將回滾到此 savepoint. 潛套事務是外部事務的一部分, 只有外部事務結束後它才會被提交。
由此可見, PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大區別在於, PROPAGATION_REQUIRES_NEW 完全是一個新的事務, 而 PROPAGATION_NESTED 則是外部事務的子事務, 如果外部事務 commit, 巢狀事務也會被 commit, 這個規則同樣適用於 roll back.
PROPAGATION_REQUIRED應該是我們首先的事務傳播行為。它能夠滿足我們大多數的事務需求。
2.2.2 隔離級別
事務的第二個維度就是隔離級別(isolation level)。隔離級別定義了一個事務可能受其他併發事務影響的程度。
(1)併發事務引起的問題
在典型的應用程式中,多個事務併發執行,經常會操作相同的資料來完成各自的任務。併發雖然是必須的,但可能會導致一下的問題。
- 髒讀(Dirty reads)——髒讀發生在一個事務讀取了另一個事務改寫但尚未提交的資料時。如果改寫在稍後被回滾了,那麼第一個事務獲取的資料就是無效的。
- 不可重複讀(Nonrepeatable read)——不可重複讀發生在一個事務執行相同的查詢兩次或兩次以上,但是每次都得到不同的資料時。這通常是因為另一個併發事務在兩次查詢期間進行了更新。
- 幻讀(Phantom read)——幻讀與不可重複讀類似。它發生在一個事務(T1)讀取了幾行資料,接著另一個併發事務(T2)插入了一些資料時。在隨後的查詢中,第一個事務(T1)就會發現多了一些原本不存在的記錄。
不可重複讀與幻讀的區別
不可重複讀的重點是修改:
同樣的條件, 你讀取過的資料, 再次讀取出來發現值不一樣了
例如:在事務1中,Mary 讀取了自己的工資為1000,操作並沒有完成
con1 = getConnection();
select salary from employee empId ="Mary";
在事務2中,這時財務人員修改了Mary的工資為2000,並提交了事務.
con2 = getConnection();
update employee set salary = 2000;
con2.commit();
在事務1中,Mary 再次讀取自己的工資時,工資變為了2000
//con1
select salary from employee empId ="Mary";
在一個事務中前後兩次讀取的結果並不一致,導致了不可重複讀。
幻讀的重點在於新增或者刪除:
同樣的條件, 第1次和第2次讀出來的記錄數不一樣
例如:目前工資為1000的員工有10人。事務1,讀取所有工資為1000的員工。
con1 = getConnection();
Select * from employee where salary =1000;
共讀取10條記錄
這時另一個事務向employee表插入了一條員工記錄,工資也為1000
con2 = getConnection();
Insert into employee(empId,salary) values("Lili",1000);
con2.commit();
事務1再次讀取所有工資為1000的員工
//con1
select * from employee where salary =1000;
共讀取到了11條記錄,這就產生了幻像讀。
從總的結果來看, 似乎不可重複讀和幻讀都表現為兩次讀取的結果不一致。但如果你從控制的角度來看, 兩者的區別就比較大。
對於前者, 只需要鎖住滿足條件的記錄。
對於後者, 要鎖住滿足條件及其相近的記錄。
(2)隔離級別
隔離級別 | 含義 |
---|---|
ISOLATION_DEFAULT | 使用後端資料庫預設的隔離級別 |
ISOLATION_READ_UNCOMMITTED | 最低的隔離級別,允許讀取尚未提交的資料變更,可能會導致髒讀、幻讀或不可重複讀 |
ISOLATION_READ_COMMITTED | 允許讀取併發事務已經提交的資料,可以阻止髒讀,但是幻讀或不可重複讀仍有可能發生 |
ISOLATION_REPEATABLE_READ | 對同一欄位的多次讀取結果都是一致的,除非資料是被本身事務自己所修改,可以阻止髒讀和不可重複讀,但幻讀仍有可能發生 |
ISOLATION_SERIALIZABLE | 最高的隔離級別,完全服從ACID的隔離級別,確保阻止髒讀、不可重複讀以及幻讀,也是最慢的事務隔離級別,因為它通常是通過完全鎖定事務相關的資料庫表來實現的 |
2.2.3 只讀
事務的第三個特性是它是否為只讀事務。如果事務只對後端的資料庫進行該操作,資料庫可以利用事務的只讀特性來進行一些特定的優化。通過將事務設定為只讀,你就可以給資料庫一個機會,讓它應用它認為合適的優化措施。
2.2.4 事務超時
為了使應用程式很好地執行,事務不能執行太長的時間。因為事務可能涉及對後端資料庫的鎖定,所以長時間的事務會不必要的佔用資料庫資源。事務超時就是事務的一個定時器,在特定時間內事務如果沒有執行完畢,那麼就會自動回滾,而不是一直等待其結束。
2.2.5 回滾規則
事務五邊形的最後一個方面是一組規則,這些規則定義了哪些異常會導致事務回滾而哪些不會。預設情況下,事務只有遇到執行期異常時才會回滾,而在遇到檢查型異常時不會回滾(這一行為與EJB的回滾行為是一致的)
但是你可以宣告事務在遇到特定的檢查型異常時像遇到執行期異常那樣回滾。同樣,你還可以宣告事務遇到特定的異常不回滾,即使這些異常是執行期異常。
2.3 事務狀態
上面講到的呼叫PlatformTransactionManager介面的getTransaction()的方法得到的是TransactionStatus介面的一個實現,這個介面的內容如下:
public interface TransactionStatus{
boolean isNewTransaction(); // 是否是新的事物
boolean hasSavepoint(); // 是否有恢復點
void setRollbackOnly(); // 設定為只回滾
boolean isRollbackOnly(); // 是否為只回滾
boolean isCompleted; // 是否已完成
}
可以發現這個介面描述的是一些處理事務提供簡單的控制事務執行和查詢事務狀態的方法,在回滾或提交的時候需要應用對應的事務狀態。
3 程式設計式事務
3.1 程式設計式和宣告式事務的區別
Spring提供了對程式設計式事務和宣告式事務的支援,程式設計式事務允許使用者在程式碼中精確定義事務的邊界,而宣告式事務(基於AOP)有助於使用者將操作與事務規則進行解耦。
簡單地說,程式設計式事務侵入到了業務程式碼裡面,但是提供了更加詳細的事務管理;而宣告式事務由於基於AOP,所以既能起到事務管理的作用,又可以不影響業務程式碼的具體實現。
3.2 如何實現程式設計式事務?
Spring提供兩種方式的程式設計式事務管理,分別是:使用TransactionTemplate和直接使用PlatformTransactionManager。
3.2.1 使用TransactionTemplate
採用TransactionTemplate和採用其他Spring模板,如JdbcTempalte和HibernateTemplate是一樣的方法。它使用回撥方法,把應用程式從處理取得和釋放資源中解脫出來。如同其他模板,TransactionTemplate是執行緒安全的。程式碼片段:
TransactionTemplate tt = new TransactionTemplate(); // 新建一個TransactionTemplate
Object result = tt.execute(
new TransactionCallback(){
public Object doTransaction(TransactionStatus status){
updateOperation();
return resultOfUpdateOperation();
}
}); // 執行execute方法進行事務管理
使用TransactionCallback()可以返回一個值。如果使用TransactionCallbackWithoutResult則沒有返回值。
3.2.2 使用PlatformTransactionManager
示例程式碼如下:
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(); //定義一個某個框架平臺的TransactionManager,如JDBC、Hibernate
dataSourceTransactionManager.setDataSource(this.getJdbcTemplate().getDataSource()); // 設定資料來源
DefaultTransactionDefinition transDef = new DefaultTransactionDefinition(); // 定義事務屬性
transDef.setPropagationBehavior(DefaultTransactionDefinition.PROPAGATION_REQUIRED); // 設定傳播行為屬性
TransactionStatus status = dataSourceTransactionManager.getTransaction(transDef); // 獲得事務狀態
try {
// 資料庫操作
dataSourceTransactionManager.commit(status);// 提交
} catch (Exception e) {
dataSourceTransactionManager.rollback(status);// 回滾
}
4 宣告式事務
4.1 事務的5種配置方式
*注:以下配置程式碼參考自http://www.blogjava.net/robbie/archive/2009/04/05/264003.html
根據代理機制的不同,總結了五種Spring事務的配置方式,配置檔案如下:
(1)每個Bean都有一個代理
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="configLocation" value="classpath:hibernate.cfg.xml" />
<property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration" />
</bean>
<!-- 事務管理器(宣告式的事務) -->
<bean id="transactionManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<!-- 配置DAO -->
<bean id="userDaoTarget" class="com.bluesky.spring.dao.UserDaoImpl">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<bean id="userDao"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<!-- 配置事務管理器 -->
<property name="transactionManager" ref="transactionManager" />
<property name="target" ref="userDaoTarget" />
<property name="proxyInterfaces" value="com.bluesky.spring.dao.GeneratorDao" />
<!-- 配置事務屬性 -->
<property name="transactionAttributes">
<props>
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
</beans>
(2)所有Bean共享一個代理基類
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="configLocation" value="classpath:hibernate.cfg.xml" />
<property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration" />
</bean>
<!-- 定義事務管理器(宣告式的事務) -->
<bean id="transactionManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<bean id="transactionBase"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"
lazy-init="true" abstract="true">
<!-- 配置事務管理器 -->
<property name="transactionManager" ref="transactionManager" />
<!-- 配置事務屬性 -->
<property name="transactionAttributes">
<props>
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
<!-- 配置DAO -->
<bean id="userDaoTarget" class="com.bluesky.spring.dao.UserDaoImpl">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<bean id="userDao" parent="transactionBase" >
<property name="target" ref="userDaoTarget" />
</bean>
</beans>
(3)使用攔截器
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="configLocation" value="classpath:hibernate.cfg.xml" />
<property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration" />
</bean>
<!-- 定義事務管理器(宣告式的事務) -->
<bean id="transactionManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<bean id="transactionInterceptor"
class="org.springframework.transaction.interceptor.TransactionInterceptor">
<property name="transactionManager" ref="transactionManager" />
<!-- 配置事務屬性 -->
<property name="transactionAttributes">
<props>
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames">
<list>
<value>*Dao</value>
</list>
</property>
<property name="interceptorNames">
<list>
<value>transactionInterceptor</value>
</list>
</property>
</bean>
<!-- 配置DAO -->
<bean id="userDao" class="com.bluesky.spring.dao.UserDaoImpl">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
</beans>
(4)使用tx標籤配置的攔截器
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
<context:annotation-config />
<context:component-scan base-package="com.bluesky" />
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="configLocation" value="classpath:hibernate.cfg.xml" />
<property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration" />
</bean>
<!-- 定義事務管理器(宣告式的事務) -->
<bean id="transactionManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED" />
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="interceptorPointCuts"
expression="execution(* com.bluesky.spring.dao.*.*(..))" />
<aop:advisor advice-ref="txAdvice"
pointcut-ref="interceptorPointCuts" />
</aop:config>
</beans>
(5)全註解
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
<context:annotation-config />
<context:component-scan base-package="com.bluesky" />
<!--開啟事務的註解支援-->
<tx:annotation-driven transaction-manager="transactionManager"/>
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="configLocation" value="classpath:hibernate.cfg.xml" />
<property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration" />
</bean>
<!-- 定義事務管理器(宣告式的事務) -->
<bean id="transactionManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
</beans>
<!--MyBatis自動參與到spring事務管理中,無需額外配置,只要org.mybatis.spring.SqlSessionFactoryBean引用的資料來源與DataSourceTransactionManager引用的資料來源一致即可。-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation">
<value>classpath:mybatis-config.xml</value>
</property>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
@Transactional 可以作用於介面、介面方法、類以及類方法上。當作用於類上時,該類的所有 public 方法將都具有該型別的事務屬性,同時,我們也可以在方法級別使用該註解來覆蓋類級別的定義。
雖然 @Transactional 註解可以作用於介面、介面方法、類以及類方法上,但是 Spring 建議不要在介面或者介面方法上使用該註解,因為這隻有在使用基於介面的代理時它才會生效。另外, @Transactional 註解應該只被應用到 public 方法上,這是由 Spring AOP 的本質決定的。如果你在 protected、private 或者預設可見性的方法上使用 @Transactional 註解,這將被忽略,也不會丟擲任何異常。
此時在DAO上需加上@Transactional註解,如下:
package com.bluesky.spring.dao;
import java.util.List;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
import org.springframework.stereotype.Component;
import com.bluesky.spring.domain.User;
@Transactional
@Component("userDao")
public class UserDaoImpl extends HibernateDaoSupport implements UserDao {
public List<User> listUsers() {
return this.getSession().createQuery("from User").list();
}
}
4.2 一個宣告式事務的例項
注:該例項參考自[Spring中的事務管理例項詳解](http://www.jb51.net/article/57589.htm)
首先是資料庫表
book(isbn, book_name, price)
account(username, balance)
book_stock(isbn, stock)
然後是XML配置
<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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
<import resource="applicationContext-db.xml" />
<context:component-scan
base-package="com.springinaction.transaction">
</context:component-scan>
<tx:annotation-driven transaction-manager="txManager"/>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
使用的類
BookShopDao
package com.springinaction.transaction;
public interface BookShopDao {
// 根據書號獲取書的單價
public int findBookPriceByIsbn(String isbn);
// 更新書的庫存,使書號對應的庫存-1
public void updateBookStock(String isbn);
// 更新使用者的賬戶餘額:account的balance-price
public void updateUserAccount(String username, int price);
}
BookShopDaoImpl
package com.springinaction.transaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository("bookShopDao")
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 username, int price) {
//檢查餘額是否不足,若不足,則丟擲異常
String sql2 = "SELECT balance FROM account WHERE username = ?";
int balance = JdbcTemplate.queryForObject(sql2, Integer.class, username);
if (balance < price) {
throw new UserAccountException("餘額不足!");
}
String sql = "UPDATE account SET balance = balance - ? WHERE username = ?";
JdbcTemplate.update(sql, price, username);
}
}
BookShopService
package com.springinaction.transaction;
public interface BookShopService {
public void purchase(String username, String isbn);
}
BookShopServiceImpl
package com.springinaction.transaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service("bookShopService")
public class BookShopServiceImpl implements BookShopService {
@Autowired
private BookShopDao bookShopDao;
/**
* 1.新增事務註解
* 使用propagation 指定事務的傳播行為,即當前的事務方法被另外一個事務方法呼叫時如何使用事務。
* 預設取值為REQUIRED,即使用呼叫方法的事務。如果當前事務存在,方法會在該事務中執行。否則,會啟動一個新的事務
* REQUIRES_NEW:使用自己的事務,呼叫的事務方法的事務被掛起。
*
* 2.使用isolation 指定事務的隔離級別,最常用的取值為READ_COMMITTED
* 3.預設情況下 Spring 的宣告式事務對所有的執行時異常進行回滾,也可以通過對應的屬性進行設定。通常情況下,預設值即可。
* 4.使用readOnly 指定事務是否為只讀。 表示這個事務只讀取資料但不更新資料,這樣可以幫助資料庫引擎優化事務。若真的是一個只讀取資料庫值得方法,應設定readOnly=true
* 5.使用timeOut 指定強制回滾之前事務可以佔用的時間。
*/
@Transactional(propagation=Propagation.REQUIRES_NEW,
isolation=Isolation.READ_COMMITTED,
noRollbackFor={UserAccountException.class},
readOnly=true, timeout=3)
@Override
public void purchase(String username, String isbn) {
//1.獲取書的單價
int price = bookShopDao.findBookPriceByIsbn(isbn);
//2.更新書的庫存
bookShopDao.updateBookStock(isbn);
//3.更新使用者餘額
bookShopDao.updateUserAccount(username, price);
}
}
Cashier
package com.springinaction.transaction;
import java.util.List;
public interface Cashier {
public void checkout(String username, List<String>isbns);
}
CashierImpl:CashierImpl.checkout和bookShopService.purchase聯合測試了事務的傳播行為
package com.springinaction.transaction;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service("cashier")
public class CashierImpl implements Cashier {
@Autowired
private BookShopService bookShopService;
@Transactional
@Override
public void checkout(String username, List<String> isbns) {
for(String isbn : isbns) {
bookShopService.purchase(username, isbn);
}
}
}
BookStockException
package com.springinaction.transaction;
public class BookStockException extends RuntimeException {
private static final long serialVersionUID = 1L;
public BookStockException() {
super();
// TODO Auto-generated constructor stub
}
public BookStockException(String arg0, Throwable arg1, boolean arg2,
boolean arg3) {
super(arg0, arg1, arg2, arg3);
// TODO Auto-generated constructor stub
}
public BookStockException(String arg0, Throwable arg1) {
super(arg0, arg1);
// TODO Auto-generated constructor stub
}
public BookStockException(String arg0) {
super(arg0);
// TODO Auto-generated constructor stub
}
public BookStockException(Throwable arg0) {
super(arg0);
// TODO Auto-generated constructor stub
}
}
UserAccountException
package com.springinaction.transaction;
public class UserAccountException extends RuntimeException {
private static final long serialVersionUID = 1L;
public UserAccountException() {
super();
// TODO Auto-generated constructor stub
}
public UserAccountException(String arg0, Throwable arg1, boolean arg2,
boolean arg3) {
super(arg0, arg1, arg2, arg3);
// TODO Auto-generated constructor stub
}
public UserAccountException(String arg0, Throwable arg1) {
super(arg0, arg1);
// TODO Auto-generated constructor stub
}
public UserAccountException(String arg0) {
super(arg0);
// TODO Auto-generated constructor stub
}
public UserAccountException(Throwable arg0) {
super(arg0);
// TODO Auto-generated constructor stub
}
}
測試類
package com.springinaction.transaction;
import java.util.Arrays;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class SpringTransitionTest {
private ApplicationContext ctx = null;
private BookShopDao bookShopDao = null;
private BookShopService bookShopService = null;
private Cashier cashier = null;
{
ctx = new ClassPathXmlApplicationContext("config/transaction.xml");
bookShopDao = ctx.getBean(BookShopDao.class);
bookShopService = ctx.getBean(BookShopService.class);
cashier = ctx.getBean(Cashier.class);
}
@Test
public void testBookShopDaoFindPriceByIsbn() {
System.out.println(bookShopDao.findBookPriceByIsbn("1001"));
}
@Test
public void testBookShopDaoUpdateBookStock(){
bookShopDao.updateBookStock("1001");
}
@Test
public void testBookShopDaoUpdateUserAccount(){
bookShopDao.updateUserAccount("AA", 100);
}
@Test
public void testBookShopService(){
bookShopService.purchase("AA", "1001");
}
@Test
public void testTransactionPropagation(){
cashier.checkout("AA", Arrays.asList("1001", "1002"));
}
}
8.3 測試資料準備
8.3.1 需求
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-LmVs0oC1-1608402277183)(https://raw.githubusercontent.com/FermHan/tuchuang/master/20191114153607.png)]
DAO層分別有:
- 根據書號找書的價格
- 更新書的庫存 -1
- 更新使用者的餘額 -XX
8.3.2 資料庫表
書:書號、書名、價格
庫存:書號,庫存量
賬戶:使用者名稱,餘額
tom和Jerry買書,
CREATE TABLE book (
isbn VARCHAR (50) PRIMARY KEY,
book_name VARCHAR (100),
price INT
) ;
CREATE TABLE book_stock (
isbn VARCHAR (50) PRIMARY KEY,
stock INT,
) ;
CREATE TABLE account (
username VARCHAR (50) PRIMARY KEY,
balance INT,
) ;
INSERT INTO account (`username`,`balance`) VALUES ('Tom',100000);
INSERT INTO account (`username`,`balance`) VALUES ('Jerry',150000);
INSERT INTO book (`isbn`,`book_name`,`price`) VALUES ('ISBN-001','book01',100);
INSERT INTO book (`isbn`,`book_name`,`price`) VALUES ('ISBN-002','book02',200);
INSERT INTO book (`isbn`,`book_name`,`price`) VALUES ('ISBN-003','book03',300);
INSERT INTO book (`isbn`,`book_name`,`price`) VALUES ('ISBN-004','book04',400);
INSERT INTO book (`isbn`,`book_name`,`price`) VALUES ('ISBN-005','book05',500);
INSERT INTO book_stock (`isbn`,`stock`) VALUES ('ISBN-001',1000);
INSERT INTO book_stock (`isbn`,`stock`) VALUES ('ISBN-002',2000);
INSERT INTO book_stock (`isbn`,`stock`) VALUES ('ISBN-003',3000);
INSERT INTO book_stock (`isbn`,`stock`) VALUES ('ISBN-004',4000);
INSERT INTO book_stock (`isbn`,`stock`) VALUES ('ISBN-005',5000);
8.4 初步實現
- 配置檔案
<!-- 配置事務管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 啟用事務註解 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
- 在需要進行事務控制的方法上加註解 @Transactional
8.5 事務的傳播行為
8.5.1 簡介
當事務方法被另一個事務方法呼叫時,必須指定事務應該如何傳播。例如:方法可能繼續在現有事務中執行,也可能開啟一個新事務,並在自己的事務中執行。
事務的傳播行為可以由傳播屬性指定。Spring定義了7種類傳播行為。
事務傳播行為型別 | 說明 |
---|---|
PROPAGATION_REQUIRED | 如果當前沒有事務,就新建一個事務,如果已經存在一個事務中,加入到這個事務中,當前的方法就在這個事務中執行。這是最常見的選擇。 |
PROPAGATION_SUPPORTS | 支援當前事務,如果當前沒有事務,就以非事務方式執行。如果有事務在執行,當前方法就在這個事務中執行,否則它可以不執行在事務中 |
PROPAGATION_MANDATORY | 使用當前的事務,如果當前沒有事務,就丟擲異常。當前的方法必須執行在事務內部,如果沒有正在執行的事務,將它掛起 |
PROPAGATION_REQUIRES_NEW | 當前的方法必須啟動新事務 。新建 事務,如果當前存在事務,有事務正在執行,把當前事務掛起 。 |
PROPAGATION_NOT_SUPPORTED | 以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。當前的方法不應該執行在事務中,如果有執行的事務,將它掛起。 |
PROPAGATION_NEVER | 以非事務方式執行,如果當前存在事務,則丟擲異常。當前的方法不應該執行在事務中,如果有執行的事務,就丟擲異常 |
PROPAGATION_NESTED | 如果當前存在事務,則在巢狀事務內執行。如果當前沒有事務,則執行與PROPAGATION_REQUIRED類似的操作。如果有事務在執行,當前的方法就應該在這個事務的巢狀事務事務內執行,否則,就啟動一個新的事務,並在它自己的事務內執行。 |
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-v2bIpRku-1608402277184)(https://raw.githubusercontent.com/FermHan/tuchuang/master/20191114153654.png)]
事務傳播屬性可以在@Transactional註解的propagation屬性中定義。
8.5.2 測試
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-15pZPdqL-1608402277185)(https://raw.githubusercontent.com/FermHan/tuchuang/master/20191114153704.png)]
8.5.3 說明
①REQUIRED傳播行為
當bookService的purchase()方法被另一個事務方法checkout()呼叫時,它預設會在現有的事務內執行。這個預設的傳播行為就是REQUIRED。因此在checkout()方法的開始和終止邊界內只有一個事務。這個事務只在checkout()方法結束的時候被提交,結果使用者一本書都買不了。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-WwTPyMDX-1608402277186)(https://raw.githubusercontent.com/FermHan/tuchuang/master/20191114153718.png)]
②. REQUIRES_NEW傳播行為
表示該方法必須啟動一個新事務,並在自己的事務內執行。如果有事務在執行,就應該先掛起它。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-bHgD9mZV-1608402277187)(https://raw.githubusercontent.com/FermHan/tuchuang/master/20191114153730.png)]
8.5.4 補充
在Spring 2.x事務通知中,可以像下面這樣在<tx:method>
元素中設定傳播事務屬性。
<tx:advice id="bookShopTxAdvice" transaction-manager="transactionMagager">
<tx:attribute>
<tx:method name="purchase" propagation="REQUIRES_NEW"/>
</tx:attribute>
</tx:advice>
8.6 事務的隔離級別
8.6.1 資料庫事務併發問題
假設現在有兩個事務:Transaction01和Transaction02併發執行。
- 髒讀
①Transaction01將某條記錄的AGE值從20修改為30。
②Transaction02讀取了Transaction01更新後的值:30。
③Transaction01回滾,AGE值恢復到了20。
④Transaction02讀取到的30就是一個無效的值。
- 不可重複讀
①Transaction01讀取了AGE值為20。
②Transaction02將AGE值修改為30。
③Transaction01再次讀取AGE值為30,和第一次讀取不一致。
- 幻讀
①Transaction01讀取了STUDENT表中的一部分資料。
②Transaction02向STUDENT表中插入了新的行。
③Transaction01讀取了STUDENT表時,多出了一些行。
8.6.2 隔離級別
資料庫系統必須具有隔離併發執行各個事務的能力,使它們不會相互影響,避免各種併發問題。一個事務與其他事務隔離的程度稱為隔離級別。SQL標準中規定了多種事務隔離級別,不同隔離級別對應不同的干擾程度,隔離級別越高,資料一致性就越好,但併發性越弱。
- 讀未提交:READ UNCOMMITTED
允許Transaction01讀取Transaction02未提交的修改。
- 讀已提交:READ COMMITTED
要求Transaction01只能讀取Transaction02已提交的修改。
- 可重複讀:REPEATABLE READ
確保Transaction01可以多次從一個欄位中讀取到相同的值,即Transaction01執行期間禁止其它事務對這個欄位進行更新。
- 序列化:SERIALIZABLE
確保Transaction01可以多次從一個表中讀取到相同的行,在Transaction01執行期間,禁止其它事務對這個表進行新增、更新、刪除操作。可以避免任何併發問題,但效能十分低下。
- 各個隔離級別解決併發問題的能力見下表
髒讀 | 不可重複讀 | 幻讀 | |
---|---|---|---|
READ UNCOMMITTED | 有 | 有 | 有 |
READ COMMITTED | 無 | 有 | 有 |
REPEATABLE READ | 無 | 無 | 有 |
SERIALIZABLE | 無 | 無 | 無 |
- 各種資料庫產品對事務隔離級別的支援程度
Oracle | MySQL | |
---|---|---|
READ UNCOMMITTED | × | √ |
READ COMMITTED | √(預設) | √ |
REPEATABLE READ | × | √(預設) |
SERIALIZABLE | √ | √ |
8.6.3 在Spring中指定事務隔離級別
- 註解
用@Transactional註解宣告式地管理事務時可以在@Transactional的isolation屬性中設定隔離級別
- XML
在Spring 2.x事務通知中,可以在<tx:method>
元素中指定隔離級別
<tx:advice id="bookShopTxAdvice" transaction-manager="transactionMagager">
<tx:attribute>
<tx:method name="purchase" propagation="REQUIRES_NEW" isolation="READ_COMMITTED"/>
</tx:attribute>
</tx:advoce>
8.7 觸發事務回滾的異常
8.7.1預設情況
捕獲到RuntimeException或Error時回滾,而捕獲到編譯時異常不回滾。
8.7.2設定途經
- 註解@Transactional 註解
① rollbackFor屬性:指定遇到時必須進行回滾的異常型別,可以為多個
② noRollbackFor屬性:指定遇到時不回滾的異常型別,可以為多個
@Transactional(propagation=Propagation.REQUIRES_NEW,
isolation=Isolation.READ_COMMITTED,
rollbackFor={IOException.class,SQLException.class},
noRollbackFor=ArithmeticException.class )
public void purchase(String isbn,String username){
}
- XML
在Spring 2.x事務通知中,可以在<tx:method>
元素中指定回滾規則。如果有不止一種異常則用逗號分隔。
<tx:advice id="bookShopTxAdvice" transaction-manager="transactionMagager">
<tx:attribute>
<tx:method name="purchase"
propagation="REQUIRES_NEW"
isolation="READ_COMMITTED"
rollback-for="java.io.IOException,java.sql.SQLException"/>
</tx:attribute>
</tx:advice>
8.8 事務的超時和只讀屬性
8.8.1簡介
由於事務可以在行和表上獲得鎖,因此長事務會佔用資源,並對整體效能產生影響。
如果一個事務只讀取資料但不做修改,資料庫引擎可以對這個事務進行優化。
超時事務屬性:事務在強制回滾之前可以保持多久。這樣可以防止長期執行的事務佔用資源。事務都卡著不動,卡的時間太長就進行回滾
8.8.2設定
只讀事務屬性: 表示這個事務只讀取資料但不更新資料, 這樣可以幫助資料庫引擎優化事務。
- 註解
@Transaction註解
@Transactional(propagation=Propagatio.REQUIRES_NEW,
isolation=Isolation.READ_COMMITTED,
rollbackFor=(IOException.class,SQLException.class),
noRollbackFor=ArithmeticException.class
readOnly=true,//只讀
timeout=30)//超時
public void purchase(String isbn,String username){
}
只讀並不會限制方法內對資料庫的操作,只是spring會認為這個方法他隨便用
- XML
在Spring 2.x事務通知中,超時和只讀屬性可以在<tx:method>
元素中進行指定
<tx:advice id="bookShopTxAdvice" transaction-manager="transactionMagager">
<tx:attribute>
<tx:method name="purchase"
propagation="REQUIRES_NEW"
isolation="READ_COMMITTED"
rollback-for="java.io.IOException,java.sql.SQLException"
timeout="30"
read-only="true"/>
</tx:attribute>
</tx:advice>
8.9 基於XML文件的宣告式事務配置
<!-- 配置事務切面 -->
<aop:config>
<aop:pointcut
expression="execution(* com.atguigu.tx.component.service.BookShopServiceImpl.purchase(..))"
id="txPointCut"/>
<!-- 將切入點表示式和事務屬性配置關聯到一起 -->
<aop:advisor advice-ref="myTx" pointcut-ref="txPointCut"/>
</aop:config>
<!-- 配置基於XML的宣告式事務 -->
<tx:advice id="myTx" transaction-manager="transactionManager">
<tx:attributes>
<!-- 設定具體方法的事務屬性 -->
<tx:method name="find*" read-only="true"/>
<tx:method name="get*" read-only="true"/>
<tx:method name="purchase"
isolation="READ_COMMITTED"
no-rollback-for="java.lang.ArithmeticException,java.lang.NullPointerException"
propagation="REQUIRES_NEW"
read-only="false"
timeout="10"/>
</tx:attributes>
</tx:advice>
完整的helloworld
這個helloworld使用的jdbc方式是jdbcTemplate
------------------spring-tx.xml----------------
<?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-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">
<context:component-scan base-package="com.atguigu.spring.tx.annotation"></context:component-scan>
<!-- 資料來源 -->
<context:property-placeholder location="classpath:db.properties"/>
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"></property>
<property name="jdbcUrl" value="${jdbc.url}"></property>
<property name="user" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!-- JdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- NamedParameterJdbcTemplate -->
<bean id="namedParameterJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
<constructor-arg ref="dataSource"></constructor-arg>
</bean>
<!-- 事務管理器 -->
<bean id="dataSourceTransactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 開啟事務註解 :使用註解來開發事務
transaction-manager 用來指定事務管理器,如果上一個bean事務管理器的id值 是 transactionManager,可以省略不進行指定。
-->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
</beans>
DAO層
------------BookShopDao介面-----------------------
package com.atguigu.spring.tx.annotation.dao;
public interface BookShopDao {
//根據書號查詢書的價格
public int findPriceByIsbn(String isbn );
//更新書的庫存
public void updateStock(String isbn);
//更新使用者的餘額
public void updateUserAccount(String username,Integer price);
}
----------------BookShopDao實現類-------------------------
package com.atguigu.spring.tx.annotation.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import com.atguigu.spring.tx.annotation.exception.BookStockException;
import com.atguigu.spring.tx.annotation.exception.UserAccountException;
@Repository
public class BookShopDaoImpl implements BookShopDao {
@Autowired
private JdbcTemplate jdbcTemplate ;
@Override
public int findPriceByIsbn(String isbn) {
String sql = "select price from book where isbn = ? ";
return jdbcTemplate.queryForObject(sql, Integer.class, isbn);
// SQL語句,返回型別,傳入引數
}
@Override
public void updateStock(String isbn) {
//判斷庫存是否足夠
String sql ="select stock from book_stock where isbn = ?";
Integer stock = jdbcTemplate.queryForObject(sql, Integer.class, isbn);
if(stock <=0) {
throw new BookStockException("庫存不足.....");
}
sql = "update book_stock set stock = stock -1 where isbn = ? ";
jdbcTemplate.update(sql, isbn);
}
@Override
public void updateUserAccount(String username, Integer price) {
//判斷餘額是否足夠
String sql ="select balance from account where username = ? ";
Integer balance = jdbcTemplate.queryForObject(sql, Integer.class,username);
if(balance < price) {
throw new UserAccountException("餘額不足......");
}
sql = "update account set balance = balance - ? where username = ? ";
jdbcTemplate.update(sql, price,username);
}
}
SERVICE層
-------------------BookShopService.java-----------
package com.atguigu.spring.tx.annotation.service;
public interface BookShopService {
public void buyBook(String username, String isbn);
}
---------------BookShopServiceImpl.java------------------
package com.atguigu.spring.tx.annotation.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.atguigu.spring.tx.annotation.dao.BookShopDao;
import com.atguigu.spring.tx.annotation.exception.UserAccountException;
@Transactional //對當前類中所有的方法都起作用
@Service
public class BookShopServiceImpl implements BookShopService {
@Autowired
private BookShopDao bookShopDao ;
/**
* 事務屬性:
* 1. 事務的傳播行為 propagation: 一個事務方法被另外一個事務方法呼叫時,當前的事務如何使用事務.
* Propagation.REQUIRED 預設值. 使用呼叫者的事務.
* Propagation.REQUIRES_NEW 將呼叫者的事務掛起, 重新開啟事務來使用.
* 2. 事務的隔離級別 isolation
* 1 讀未提交 髒讀
* 2 讀已提交 不可重複讀
* 4 可重複讀 幻讀
* 8 序列化 效率低。
* 3. 事務的回滾與不回滾 預設情況下, Spring會對所有的執行時異常進行事務回滾.
* rollbackFor
* rollbackForClassName
* noRollbackFor
* noRollbackForClassName
* 4. 事務的只讀設定:
* readOnly
* true: 只讀 代表著只會對資料庫進行讀取操作, 不會有修改的操作.
* 如果確保當前的事務只有讀取操作,就有必要設定為只讀,可以幫助資料庫
* 引擎優化事務
* false: 非只讀 不僅會讀取資料還會有修改操作。
* 5. 事務的超時設定: 設定事務在強制回滾之前可以佔用的時間.
* timeout:
*
*/
//只對當前的方法起作用 //加上這個註解代表這個方法中的3條語句形成了一個事務
@Transactional(propagation=Propagation.REQUIRES_NEW,
isolation=Isolation.READ_COMMITTED,
/* noRollbackFor={UserAccountException.class}*/
readOnly=false,
timeout=3)
public void buyBook(String username, String isbn) {
/*try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
Integer price = bookShopDao.findPriceByIsbn(isbn);
bookShopDao.updateStock(isbn);
bookShopDao.updateUserAccount(username, price);
}
}
-------------Cashier.java-----------------------
package com.atguigu.spring.tx.annotation.service;
import java.util.List;
public interface Cashier {
public void checkOut(String username, List<String> isbns );
}
-------------CashierImpl.java-------------------------------
package com.atguigu.spring.tx.annotation.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class CashierImpl implements Cashier {
@Autowired
private BookShopService bookShopService ;
@Transactional
public void checkOut(String username, List<String> isbns) {
for (String isbn : isbns) {
bookShopService.buyBook(username, isbn);
}
}
}
Exception.java
------------圖書庫存不足異常-----------------
package com.atguigu.spring.tx.annotation.exception;
public class BookStockException extends RuntimeException{
//自己定義異常
public BookStockException() {
super();
// TODO Auto-generated constructor stub
}
public BookStockException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
// TODO Auto-generated constructor stub
}
public BookStockException(String message, Throwable cause) {
super(message, cause);
// TODO Auto-generated constructor stub
}
public BookStockException(String message) {
super(message);
// TODO Auto-generated constructor stub
}
public BookStockException(Throwable cause) {
super(cause);
// TODO Auto-generated constructor stub
}
}
--------------------餘額不足異常------------------------
package com.atguigu.spring.tx.annotation.exception;
public class UserAccountException extends RuntimeException {
public UserAccountException() {
super();
// TODO Auto-generated constructor stub
}
public UserAccountException(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
// TODO Auto-generated constructor stub
}
public UserAccountException(String message, Throwable cause) {
super(message, cause);
// TODO Auto-generated constructor stub
}
public UserAccountException(String message) {
super(message);
// TODO Auto-generated constructor stub
}
public UserAccountException(Throwable cause) {
super(cause);
// TODO Auto-generated constructor stub
}
}
TEST.java
package com.atguigu.spring.tx.annotation.test;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.atguigu.spring.tx.annotation.dao.BookShopDao;
import com.atguigu.spring.tx.annotation.service.BookShopService;
import com.atguigu.spring.tx.annotation.service.Cashier;
public class TestTransaction {
private BookShopDao bookShopDao ;
private BookShopService bookShopService;
private Cashier cashier ;
@Before
public void init() {
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-tx.xml");
bookShopDao = ctx.getBean("bookShopDaoImpl",BookShopDao.class);
bookShopService = ctx.getBean("bookShopServiceImpl",BookShopService.class);
System.out.println(bookShopService.getClass().getName());
cashier = ctx.getBean("cashierImpl",Cashier.class);
}
@Test
public void testTx() {
bookShopService.buyBook("Tom", "1001");
}
@Test
public void testCheckOut() { // 兩本都買成功 買成功其中一本 兩本都沒買成功
List<String> isbns = new ArrayList<>();
isbns.add("1001");
isbns.add("1002");
cashier.checkOut("Tom", isbns);
}
}
註解驅動開發中的事務
2.事務回滾
2.1 預設回滾策略
@Transactional
public void rollback() throws SQLException {
// update db
throw new SQLException("exception");
}
上述程式碼事務會回滾嗎?不會的,就算丟擲SQLException了,但是之前的資料庫操作依然會提交,原因就是@Transactional預設情況下只回滾RuntimeException和Error。
2.2 指定回滾異常
因此,如果要指定哪些異常需要回滾,則通過配置@Transactional中rollbackFor,例如
@Transactional(rollbackFor = {SQLException.class})
public void rollback() throws SQLException {
// update db
throw new SQLException("exception");
}
按照上面例子,那指定的SQLException,當丟擲RuntimeException的時候,還會回滾嗎?
還是會回滾的。
2.3 事務巢狀的回滾
假設有下面的邏輯,事務會回滾嗎(或者說 updateA,updateB,updateC)哪些更新會提交
@Transactional
public void rollback() {
// updateA
try{
selfProxy.innelTransaction()
}catch(RuntimeException e){
//do nothing
}
//updateC
}
@Transactional
public void innelTransaction() throws SQLException {
// updateB
throw new RuntimeException("exception");
}
答案是會回滾,因為內部事務觸發回滾,當前事務被標記為 rollback-only,
當外部事務提交的時候,Spring丟擲以下異常,同時回滾外部事務
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
2.4 小結
所以,在需要事務回滾的時候,最好還是丟擲RuntimeException,並且不要在程式碼中捕獲此類異常
三、事務傳播性
@Transaction中的propagation的可以配置事務的傳播性,網上介紹的很多,就直接複製一段
PROPAGATION_REQUIRED--支援當前事務,如果當前沒有事務,就新建一個事務。這是最常見的選擇。 (也是預設策略)
PROPAGATION_SUPPORTS--支援當前事務,如果當前沒有事務,就以非事務方式執行。
PROPAGATION_MANDATORY--支援當前事務,如果當前沒有事務,就丟擲異常。
PROPAGATION_REQUIRES_NEW--新建事務,如果當前存在事務,把當前事務掛起。
PROPAGATION_NOT_SUPPORTED--以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
PROPAGATION_NEVER--以非事務方式執行,如果當前存在事務,則丟擲異常。
3.1 如何在事務中讀取最新配置
有時候需要在一個事務中,讀取最新資料(預設是讀取事務開始前的快照)。其實很簡單,只要使用上面PROPAGATION_NOT_SUPPORTED傳播性就可以了。
@Transactional
public void transaction() throws SQLException {
// do something
selfProxy.queryNewValue();
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void queryNewValue() throws SQLException {
//查詢資料中的最新值
}
四、內部呼叫事務方法
事務註解的實質就是在建立一個動態代理,在呼叫事務方法前開啟事務,在事務方法結束以後決定是事務提交還是回滾。
因此,直接在類內部中呼叫事務方法,是不會經過動態代理的
。 因此,如果要使方法B點事務生效,必須這樣
4.1 解決辦法
解決思路:需要在內部呼叫方法B的時候,找到當前類的代理類,用代理類去呼叫方法B
4.1.1 解決辦法1
@Service
public class MyService{
@Transactional
public void transaction(){
// do something
((MyService) AopContext.currentProxy()).queryNewValue();
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void queryNewValue(){
//查詢資料中的最新值
}
}
通過AopContext.currentProxy()可以拿到當前類的代理類,但是要使用這個時候,必須在啟動類上加上
@EnableAspectJAutoProxy(exposeProxy=true)
4.1.2 解決辦法2
既然是要拿到當前代理類,那其實直接在Spring的容器裡面去拿也可以啊。在spring中拿Bean的方法大致有2種
通過注入
@Service``public` `class` `MyService{`` ``@Autowired`` ``private` `MyService self;`` ``@Transactional`` ``public` `void` `transaction() {`` ``// do something`` ``self.queryNewValue();`` ``}`` ` ` ``@Transactional``(propagation = Propagation.NOT_SUPPORTED)`` ``public` `void` `queryNewValue() {`` ``//查詢資料中的最新值`` ``}``}
tips:spring現在對一些迴圈依賴是提供支援的,簡單來說,滿足:
- Bean是單例
- 注入的方式不是建構函式注入
通過BeanFactory
@Service
public class MyService{
@Autowired
private MyService self;
@Transactional
public void transaction() {
// do something
self.queryNewValue();
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void queryNewValue() {
//查詢資料中的最新值
}
}
4.2 需要注意的地方
- 使用@Transaction註解的方法,必須用public來修飾。
- 其實不止是@Transaction,其他類似@Cacheable,@Retryable等依賴spring proxy也必須使用上述方式達到內部呼叫。
- @Transactional,@Async放在同一個類中,如果使用Autowire注入會迴圈依賴,而使用BeanFactoryAware會使得@Transactional無效
事務原始碼
package com.atguigu.tx;
import java.beans.PropertyVetoException;
import javax.sql.DataSource;
import org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator;
import org.springframework.aop.framework.autoproxy.InfrastructureAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration;
import org.springframework.transaction.annotation.TransactionManagementConfigurationSelector;
import org.springframework.transaction.annotation.Transactional;
import com.mchange.v2.c3p0.ComboPooledDataSource;
/**
* 宣告式事務:
*
* 環境搭建:
* 1、匯入相關依賴
* 資料來源、資料庫驅動、Spring-jdbc模組
* 2、配置資料來源、JdbcTemplate(Spring提供的簡化資料庫操作的工具)運算元據
* 3、給方法上標註 @Transactional 表示當前方法是一個事務方法;
* 4、 @EnableTransactionManagement 開啟基於註解的事務管理功能;
* @EnableXXX
* 5、配置事務管理器來控制事務;
* @Bean
* public PlatformTransactionManager transactionManager()
*
*
* 原理:
* 1)、@EnableTransactionManagement
* 利用TransactionManagementConfigurationSelector給容器中會匯入元件
* 匯入兩個元件
* AutoProxyRegistrar
* ProxyTransactionManagementConfiguration
* 2)、AutoProxyRegistrar:
* 給容器中註冊一個 InfrastructureAdvisorAutoProxyCreator 元件;
* InfrastructureAdvisorAutoProxyCreator:?
* 利用後置處理器機制在物件建立以後,包裝物件,返回一個代理物件(增強器),代理物件執行方法利用攔截器鏈進行呼叫;
*
* 3)、ProxyTransactionManagementConfiguration 做了什麼?
* 1、給容器中註冊事務增強器;
* 1)、事務增強器要用事務註解的資訊,AnnotationTransactionAttributeSource解析事務註解
* 2)、事務攔截器:
* TransactionInterceptor;儲存了事務屬性資訊,事務管理器;
* 他是一個 MethodInterceptor;
* 在目標方法執行的時候;
* 執行攔截器鏈;
* 事務攔截器:
* 1)、先獲取事務相關的屬性
* 2)、再獲取PlatformTransactionManager,如果事先沒有新增指定任何transactionmanger
* 最終會從容器中按照型別獲取一個PlatformTransactionManager;
* 3)、執行目標方法
* 如果異常,獲取到事務管理器,利用事務管理回滾操作;
* 如果正常,利用事務管理器,提交事務
*/
@EnableTransactionManagement
@ComponentScan("com.atguigu.tx")
@Configuration
public class TxConfig {
//資料來源
@Bean
public DataSource dataSource() throws Exception{
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setUser("root");
dataSource.setPassword("123456");
dataSource.setDriverClass("com.mysql.jdbc.Driver");
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/test");
return dataSource;
}
@Bean
public JdbcTemplate jdbcTemplate() throws Exception{
//Spring對@Configuration類會特殊處理;給容器中加元件的方法,多次呼叫都只是從容器中找元件
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource());
return jdbcTemplate;
}
//註冊事務管理器在容器中
@Bean
public PlatformTransactionManager transactionManager() throws Exception{
return new DataSourceTransactionManager(dataSource());
}
}
TransactionManagementConfigurationSelector
public class TransactionManagementConfigurationSelector extends AdviceModeImportSelector<EnableTransactionManagement> {
@Override
protected String[] selectImports(AdviceMode adviceMode) {
switch (adviceMode) {
case PROXY://
return new String[] {AutoProxyRegistrar.class.getName(), //第一個元件
ProxyTransactionManagementConfiguration.class.getName()};//第二個元件,是一個配置類,
case ASPECTJ:
return new String[] {TransactionManagementConfigUtils.TRANSACTION_ASPECT_CONFIGURATION_CLASS_NAME};//"org.springframework.transaction.aspectj.AspectJTransactionManagementConfiguration";
default:
return null;
}
}
}
2)、AutoProxyRegistrar:
* 給容器中註冊一個 InfrastructureAdvisorAutoProxyCreator 元件;
* InfrastructureAdvisorAutoProxyCreator:?
* 利用後置處理器機制在物件建立以後,包裝物件,返回一個代理物件(增強器),代理物件執行方法利用攔截器鏈進行呼叫;
*
3)、ProxyTransactionManagementConfiguration 做了什麼?
* 1、給容器中註冊事務增強器;
* 1)、事務增強器要用事務註解的資訊,AnnotationTransactionAttributeSource解析事務註解
* 2)、事務攔截器:
* TransactionInterceptor;儲存了事務屬性資訊,事務管理器;
* 他是一個 MethodInterceptor;
* 在目標方法執行的時候;
* 執行攔截器鏈;
* 事務攔截器:
* 1)、先獲取事務相關的屬性
* 2)、再獲取PlatformTransactionManager,如果事先沒有新增指定任何transactionmanger
* 最終會從容器中按照型別獲取一個PlatformTransactionManager;
* 3)、執行目標方法
* 如果異常,獲取到事務管理器,利用事務管理回滾操作;
* 如果正常,利用事務管理器,提交事務
(1) AutoProxyRegistrar
public class AutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
/*
* 給容器中註冊一個 InfrastructureAdvisorAutoProxyCreator 元件;
* InfrastructureAdvisorAutoProxyCreator:?
* 利用後置處理器機制在物件建立以後,包裝物件,返回一個代理物件(增強器),代理物件執行方法利用攔截器鏈進行呼叫;
*/
private final Log logger = LogFactory.getLog(getClass());
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
boolean candidateFound = false;
Set<String> annoTypes = importingClassMetadata.getAnnotationTypes();
for (String annoType : annoTypes) {
AnnotationAttributes candidate = AnnotationConfigUtils.attributesFor(importingClassMetadata, annoType);
Object mode = candidate.get("mode");
Object proxyTargetClass = candidate.get("proxyTargetClass");
if (mode != null && proxyTargetClass != null && mode.getClass().equals(AdviceMode.class) &&
proxyTargetClass.getClass().equals(Boolean.class)) {
candidateFound = true;
if (mode == AdviceMode.PROXY) {//如果是PROXY
// 用工具註冊自帶代理建立器,註冊了一個InfrastructureAdvisorAutoProxyCreator
AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
/*
public static BeanDefinition registerAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry) {
return registerAutoProxyCreatorIfNecessary(registry, null);
}
public static BeanDefinition registerAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, Object source) {
return registerOrEscalateApcAsRequired(InfrastructureAdvisorAutoProxyCreator.class, registry, source);
}
*/
// 這個東西用於事務的傳播
if ((Boolean) proxyTargetClass) {
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
return;
}
}
}
}
if (!candidateFound) {
String name = getClass().getSimpleName();
logger.warn(String.format("%s was imported but no annotations were found " +
"having both 'mode' and 'proxyTargetClass' attributes of type " +
"AdviceMode and boolean respectively. This means that auto proxy " +
"creator registration and configuration may not have occured as " +
"intended, and components may not be proxied as expected. Check to " +
"ensure that %s has been @Import'ed on the same class where these " +
"annotations are declared; otherwise remove the import of %s " +
"altogether.", name, name, name));
}
}
}
(2).1 InfrastructureAdvisorAutoProxyCreator
public class InfrastructureAdvisorAutoProxyCreator extends AbstractAdvisorAutoProxyCreator {// 他也是一個BeanPostProcessor
// 利用後置處理器機制在物件建立以後,包裝物件,返回一個代理物件(增強器),代理物件執行方法利用攔截器鏈進行呼叫;
@Nullable
private ConfigurableListableBeanFactory beanFactory;
@Override
protected void initBeanFactory(ConfigurableListableBeanFactory beanFactory) {
super.initBeanFactory(beanFactory);
this.beanFactory = beanFactory;
}
@Override
protected boolean isEligibleAdvisorBean(String beanName) {
return (this.beanFactory != null && this.beanFactory.containsBeanDefinition(beanName) &&
this.beanFactory.getBeanDefinition(beanName).getRole() == BeanDefinition.ROLE_INFRASTRUCTURE);
}
}
(2) ProxyTransactionManagementConfiguration
這是一個配置類,匯入了很多元件
@Configuration
public class ProxyTransactionManagementConfiguration extends AbstractTransactionManagementConfiguration {
// 註冊 事務增強器
@Bean(name=TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor() {
BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor();
// 事務屬性,解析xml中的配置
advisor.setTransactionAttributeSource(transactionAttributeSource());//裡面有解析各種註解解析器,就是註解裡能寫的屬性
// 事務攔截器,儲存了事務屬性資訊,事務管理器 // TransactionInterceptor實現了MethodInterceptor
advisor.setAdvice(transactionInterceptor());
advisor.setOrder(this.enableTx.<Integer>getNumber("order"));
return advisor;
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public TransactionAttributeSource transactionAttributeSource() {
return new AnnotationTransactionAttributeSource();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public TransactionInterceptor transactionInterceptor() {
// new,該類實現了MethodInterceptor
TransactionInterceptor interceptor = new TransactionInterceptor();
interceptor.setTransactionAttributeSource(transactionAttributeSource());
if (this.txManager != null) {
interceptor.setTransactionManager(this.txManager);
}
return interceptor;
}
}
spring提供了事務相關介面:
- TransactionDefinition:事務定義:事務的隔離級別、事務的傳播行為
- TranscationAttribute:事務屬性:實現了對回滾規則的擴充套件(處理異常)
- PlatfromTransactionManager:事務管理器
- TransactionStatus:事務執行時狀態
配置說明
一、專案中spring+mybaits xml配置解析
一般我們會在datasource.xml中進行如下配置,但是其中每個配置項原理和用途是什麼,並不是那麼清楚,如果不清楚的話,在使用時候就很有可能會遇到坑,所以下面對這些配置項進行一一解說
(1)配置資料來源
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" />
<property name="url" value="${db_url}" />
<property name="username" value="$db_user}" />
<property name="password" value="${db_passwd}" />
<property name="maxWait" value="${db_maxWait}" />
<property name="maxActive" value="28" />
<property name="initialSize" value="2" />
<property name="minIdle" value="0" />
<property name="timeBetweenEvictionRunsMillis" value="db_time" />
</bean>
(2)建立sqlSessionFactory
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="mapperLocations" value="classpath*:com/**/mapper/*Mapper*.xml" />
<property name="dataSource" ref="dataSource" />
<property name="typeAliasesPackage" value="com.test.***.dal" />
</bean>
(3)配置掃描器,掃描指定路徑的mapper生成資料庫操作代理類
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="annotationClass" value="javax.annotation.Resource"></property>
<property name="basePackage" value="com.test.***.dal.***.mapper" />
<property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
(4)配置事務管理器
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
(5)宣告使用註解式事務
<tx:annotation-driven transaction-manager="transactionManager" />
(6)註冊各種beanfactory處理器
<context:annotation-config />
(7)該配置建立了一個TransactionInterceptor的bean,作為事務切面的執行方法
<tx:advice id="defaultTxAdvice">
<tx:attributes>
<tx:method name="*" rollback-for="Exception" />
</tx:attributes>
</tx:advice>
(8)該配置建立了一個DefaultBeanFactoryPointcutAdvisor的bean,該bean是一個advisor,裡面包含了pointcut和advice.前者說明切面加在哪裡,後者是執行邏輯。此處可以配多個advisor
<aop:config>
<aop:pointcut id="myCut" expression="(execution(* *..*BoImpl.*(..))) "/>
<aop:advisor pointcut-ref="myCut" advice-ref="defaultTxAdvice" />
</aop:config>
1.1 資料來源配置
(1)是資料來源配置,這個沒啥好說的。
1.2 配置SqlSessionFactory
(2) 作用是根據配置建立一個SqlSessionFactory,看下SqlSessionFactoryBean的程式碼知道它實現了FactoryBean和InitializingBean類,由於實現了InitializingBean,所以自然它的afterPropertiesSet方法,由於實現了FactoryBean類,所以自然會有getObject方法。下面看下時序圖:
從時序圖可知,SqlSessionFactoryBean類主要是通過屬性配置建立SqlSessionFactory例項,具體是解析配置中所有的mapper檔案放到configuration,然後作為建構函式引數例項化一個DefaultSqlSessionFactory作為SqlSessionFactory。
1.3 配置掃描器,掃描指定路徑的mapper生成資料庫操作代理類
MapperScannerConfigurer 實現了 BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware介面,所以會重寫一下方法:
1.3.1
//在bean註冊到ioc後建立例項前修改bean定義和新增bean註冊,這個是在context的refresh方法呼叫
void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException;
1.3.2
//在bean註冊到ioc後建立例項前修改bean定義或者屬性值
void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;
1.3.3
//set屬性設定後呼叫
void afterPropertiesSet() throws Exception;
1.3.4
//獲取IOC容器上下文,在context的prepareBeanFactory中呼叫
void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
1.3.5
//獲取bean在ioc容器中名字,在context的prepareBeanFactory中呼叫
void setBeanName(String name);
先上個掃描mapper生成代理類並註冊到ioc時序圖:
首先MapperScannerConfigurer實現的afterPropertiesSet方法用來確保屬性basePackage不為空
public void afterPropertiesSet() throws Exception {
notNull(this.basePackage, "Property 'basePackage' is required");
}
postProcessBeanFactory裡面啥都沒做,setBeanName獲取了bean的名字,setApplicationContext裡面獲取了ioc上下文。下面看重要的方法postProcessBeanDefinitionRegistry,由於mybais是執行時候才通過解析mapper檔案生成代理類注入到ioc,所以postProcessBeanDefinitionRegistry正好可以幹這個事情。
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}
//構造一個ClassPathMapperScanner查詢mapper
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
scanner.setAddToConfig(this.addToConfig);
//javax.annotation.Resource
scanner.setAnnotationClass(this.annotationClass);
scanner.setMarkerInterface(this.markerInterface);
//引用sqlSessionFactory
scanner.setSqlSessionFactory(this.sqlSessionFactory);
scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
//ioc上下文
scanner.setResourceLoader(this.applicationContext);
scanner.setBeanNameGenerator(this.nameGenerator);
scanner.registerFilters();
//basePackage=com.alibaba.***.dal.***.mapper,com.alibaba.rock.auth.mapper,com.alibaba.rock.workflow.dal.workflow.mapper
scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
下面重點看下scan方法:
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
//根據指定路徑去查詢對應mapper的介面類,並轉化為beandefination
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
} else {
//修改介面類bean的beandefination
processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
其中super.doScan(basePackages);根據指定路徑查詢mapper介面類,並生成bean的定義物件,物件中包含beanclassname,beanclass屬性,最後註冊該bean到ioc容器。下面看下最重要的processBeanDefinitions方法對bean定義的改造。
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
GenericBeanDefinition definition;
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (GenericBeanDefinition) holder.getBeanDefinition();
// 上面講的掃描後beanclass設定的為mapper介面類,但是這裡修改為MapperFactoryBean,MapperFactoryBean代理了mapper介面類,並且實際mapper介面類作為建構函式傳入了 definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName());
definition.setBeanClass(this.mapperFactoryBean.getClass());
definition.getPropertyValues().add("addToConfig", this.addToConfig);
//設定屬性配置中的sqlSessionFactory
boolean explicitFactoryUsed = false;
if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionFactory != null) {
definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
explicitFactoryUsed = true;
}
if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
if (explicitFactoryUsed) {
logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionTemplate != null) {
if (explicitFactoryUsed) {
logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
explicitFactoryUsed = true;
}
if (!explicitFactoryUsed) {
if (logger.isDebugEnabled()) {
logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
}
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
}
}
注:這裡修改了mapper介面類的beandefination中的beanclass為MapperFactoryBean,它則負責生產資料類操作代理類,實際mapper介面類作為建構函式傳入了 。由於只修改了beanclass,沒有修改beanname,所以我們從容器中獲取時候無感知的。
在上一個代理bean如何構造的時序圖:
screenshot.png
下面看下MapperFactoryBean是如何生成代理類的:
首先,上面程式碼設定了MapperFactoryBean的setSqlSessionFactory方法:
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
if (!this.externalSqlSession) {
this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);
}
}
上面方法建立了sqlSession,由於MapperFactoryBean為工廠bean所以例項化時候會呼叫getObject方法:
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
其實是呼叫了SqlSessionTemplate->getMapper,其中mapperInterface就是建立MapperFactoryBean時候的建構函式引數。
public <T> T getMapper(Class<T> type) {
return getConfiguration().getMapper(type, this);
}
這裡呼叫getConfiguration().getMapper(type, this);實際是DefaultSqlSessionFactory裡面的configration的getMapper方法:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//knownMappers是上面時序圖中步驟6設定進入的。
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
//代理回撥類為MapperProxy
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
在上一個實際執行sql時候呼叫代理類的序列圖:
screenshot.png
所以當呼叫實際的資料庫操作時候會呼叫MapperProxy的invoke方法:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
mapperMethod.execute(sqlSession, args);裡面實際是呼叫當前mapper對應的SqlSessionTemplate的資料庫操作,而它有委託給了代理類sqlSessionProxy,sqlSessionProxy是在SqlSessionTemplate的建構函式裡面建立的:
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
notNull(executorType, "Property 'executorType' is required");
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
this.sqlSessionProxy = (SqlSession) newProxyInstance(
SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class },
new SqlSessionInterceptor());
}
所以最終資料庫操作有被代理SqlSessionInterceptor執行:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//有TransactionSynchronizationManager管理
SqlSession sqlSession = getSqlSession(
SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType,
SqlSessionTemplate.this.exceptionTranslator);
try {
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
.....
}
}
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Creating a new SqlSession");
}
//這裡看到了使用sessionfactory熟悉的開啟了一個session
session = sessionFactory.openSession(executorType);
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
注意:這裡3裡面配置的掃描檔案在4的掃描檔案裡面一定要有,因為3給每個掃描檔案生成了一個代理,如果4裡面多了一個mapper,那麼在4中將找不到。
1.4 配置事務管理器
事務管理器作用見名知意,是用來管理事務的。
1.5 advice配置
作用是建立了一個TransactionInterceptor的bean,作為事務切面的執行方法。標籤解析的流程圖:
screenshot.png
由於是tx標籤,自然要查詢TxNamespaceHandler,程式碼如下:
public class TxNamespaceHandler extends NamespaceHandlerSupport {
static final String TRANSACTION_MANAGER_ATTRIBUTE = "transaction-manager";
static final String DEFAULT_TRANSACTION_MANAGER_BEAN_NAME = "transactionManager";
static String getTransactionManagerName(Element element) {
return (element.hasAttribute(TRANSACTION_MANAGER_ATTRIBUTE) ?
element.getAttribute(TRANSACTION_MANAGER_ATTRIBUTE) : DEFAULT_TRANSACTION_MANAGER_BEAN_NAME);
}
@Override
public void init() {
registerBeanDefinitionParser("advice", new TxAdviceBeanDefinitionParser());
registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser());
registerBeanDefinitionParser("jta-transaction-manager", new JtaTransactionManagerBeanDefinitionParser());
}
}
從init方法知道advice標籤需要TxAdviceBeanDefinitionParser這個解析類。
結合流程圖第一步設定了事務管理器的引用,我們看下引用的bean的名字:
static final String DEFAULT_TRANSACTION_MANAGER_BEAN_NAME = "transactionManager";
static String getTransactionManagerName(Element element) {
return (element.hasAttribute(TRANSACTION_MANAGER_ATTRIBUTE) ?
element.getAttribute(TRANSACTION_MANAGER_ATTRIBUTE) : DEFAULT_TRANSACTION_MANAGER_BEAN_NAME);
}
可以知道如果沒有配置這個屬性,那麼預設查詢依賴beanname=transactionManager。
然後parseAttributeSource主要迴圈解析我們配置的method標籤,和設定的方法的事務屬性。
另外程式碼:
protected Class<?> getBeanClass(Element element) {
return TransactionInterceptor.class;
}
可以知道這個advice標籤實際是創了TransactionInterceptor物件,並且通過呼叫setTransactionManager設定了事務管理器,通過setTransactionAttributeSources設定了事務屬性。
1.6 設定advisor
標籤<aop:config>
作用是建立了DefaultBeanFactoryPointcutAdvisor作為攔截器,把滿足切點的bean進行代理使用事務攔截器進行攔截。具體標籤邏輯先看流程圖:
screenshot.png
從標籤aop:config可知要查詢AopNamespaceHandler,程式碼如下:
public class AopNamespaceHandler extends NamespaceHandlerSupport {
@Override
public void init() {
registerBeanDefinitionParser("config", new ConfigBeanDefinitionParser());
registerBeanDefinitionParser("aspectj-autoproxy", new AspectJAutoProxyBeanDefinitionParser());
registerBeanDefinitionDecorator("scoped-proxy", new ScopedProxyBeanDefinitionDecorator());
registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
}
}
可知config標籤是ConfigBeanDefinitionParser來解析的,根據流程圖知configureAutoProxyCreator註冊了AspectJAwareAdvisorAutoProxyCreator類,然後createAdvisorBeanDefinition建立了DefaultBeanFactoryPointcutAdvisor,它是個advisor,並且設定引用了advice,這個adivce就是上面1.5講解的,然後createPointcutDefinition建立了切點AspectJExpressionPointcut,最後把切點設定到了advisor。
DefaultBeanFactoryPointcutAdvisor作用就是對滿足pointcut表示式的類的方法進行代理,並且使用advice進行攔截處理,而advice就是事務攔截器。
1.7 設定註解式事務
上面介紹完後就可以使用事務切面了,但是有時候還需要在具體類或者方法上進行註解行事務,那麼這就需要加 <tx:annotation-driven transaction-manager=“transactionManager” />配置
先上時序圖:
screenshot.png
!
同理1.6 不同是這裡建立了advisor,設定了advice(事務攔截器),但是好像沒有設定pointcut,看下BeanFactoryTransactionAttributeSourceAdvisor原始碼知道:
public class BeanFactoryTransactionAttributeSourceAdvisor extends AbstractBeanFactoryPointcutAdvisor {
private TransactionAttributeSource transactionAttributeSource;
private final TransactionAttributeSourcePointcut pointcut = new TransactionAttributeSourcePointcut() {
@Override
protected TransactionAttributeSource getTransactionAttributeSource() {
return transactionAttributeSource;
}
};
}
直接內建了pointcut,只不過1.6是AspectJExpressionPointcut表示式的切點,這裡是註解。
那麼這個BeanFactoryTransactionAttributeSourceAdvisor什麼時候被用來增強註解事務的類那,那是InfrastructureAdvisorAutoProxyCreator所做的事情,InfrastructureAdvisorAutoProxyCreator是個BeanPostProcessor,會在bean建立初始化後時候呼叫postProcessAfterInitialization,就是這個方法。
另外注意如果配置了多個註解式標籤在datasource.xml裡面時候只有第一個生效
public static void configureAutoProxyCreator(Element element, ParserContext parserContext) {
AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(parserContext, element);
String txAdvisorBeanName = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME;
//如果配置了多個註解式標籤在datasource.xml裡面時候只有第一個生效
if (!parserContext.getRegistry().containsBeanDefinition(txAdvisorBeanName)) {
Object eleSource = parserContext.extractSource(element);
// Create the TransactionAttributeSource definition.
RootBeanDefinition sourceDef = new RootBeanDefinition(
"org.springframework.transaction.annotation.AnnotationTransactionAttributeSource");
sourceDef.setSource(eleSource);
sourceDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
String sourceName = parserContext.getReaderContext().registerWithGeneratedName(sourceDef);
// Create the TransactionInterceptor definition.
RootBeanDefinition interceptorDef = new RootBeanDefinition(TransactionInterceptor.class);
interceptorDef.setSource(eleSource);
interceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registerTransactionManager(element, interceptorDef);
interceptorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName));
String interceptorName = parserContext.getReaderContext().registerWithGeneratedName(interceptorDef);
// Create the TransactionAttributeSourceAdvisor definition.
RootBeanDefinition advisorDef = new RootBeanDefinition(BeanFactoryTransactionAttributeSourceAdvisor.class);
advisorDef.setSource(eleSource);
advisorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
advisorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName));
advisorDef.getPropertyValues().add("adviceBeanName", interceptorName);
if (element.hasAttribute("order")) {
advisorDef.getPropertyValues().add("order", element.getAttribute("order"));
}
parserContext.getRegistry().registerBeanDefinition(txAdvisorBeanName, advisorDef);
CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), eleSource);
compositeDef.addNestedComponent(new BeanComponentDefinition(sourceDef, sourceName));
compositeDef.addNestedComponent(new BeanComponentDefinition(interceptorDef, interceptorName));
compositeDef.addNestedComponent(new BeanComponentDefinition(advisorDef, txAdvisorBeanName));
parserContext.registerComponent(compositeDef);
}
}
1.8 註冊各種beanfactory處理器
當我們需要使用BeanPostProcessor時,最直接的使用方法是在Spring配置檔案中定義這些Bean。單這些會顯得比較笨拙,
例如:使用@Autowired註解,必須事先在Spring容器中宣告
<bean class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor "/>
使用 @Required註解,就必須宣告:
<bean class="org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor"/>
通過標籤context:annotation-config/ ,我們可以同時自動註冊這些常用的beanfactory處理器,避免了我們一個個配置的繁瑣步驟:
public class ContextNamespaceHandler extends NamespaceHandlerSupport {
...
registerJava5DependentParser("annotation-config",
"org.springframework.context.annotation.AnnotationConfigBeanDefinitionParser");
....
}
public class AnnotationConfigBeanDefinitionParser implements BeanDefinitionParser {
public BeanDefinition parse(Element element, ParserContext parserContext) {
...
// Obtain bean definitions for all relevant BeanPostProcessors.
Set<BeanDefinitionHolder> processorDefinitions =
AnnotationConfigUtils.registerAnnotationConfigProcessors(parserContext.getRegistry(), source);
// Register component for the surrounding <context:annotation-config> element.
CompositeComponentDefinition compDefinition = new CompositeComponentDefinition(element.getTagName(), source);
parserContext.pushContainingComponent(compDefinition);
// Nest the concrete beans in the surrounding component.
for (BeanDefinitionHolder processorDefinition : processorDefinitions) {
parserContext.registerComponent(new BeanComponentDefinition(processorDefinition));
}
// Finally register the composite component.
parserContext.popAndRegisterContainingComponent();
return null;
}
}
public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(
BeanDefinitionRegistry registry, Object source) {
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<BeanDefinitionHolder>(4);
// Check for JPA support, and if present add the PersistenceAnnotationBeanPostProcessor.
if (jpaPresent && !registry.containsBeanDefinition(PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME)) {
RootBeanDefinition def = new RootBeanDefinition();
try {
ClassLoader cl = AnnotationConfigUtils.class.getClassLoader();
def.setBeanClass(cl.loadClass(PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME));
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Cannot load optional framework class: " + PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME, ex);
}
def.setSource(source);
def.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
beanDefinitions.add(registerBeanPostProcessor(registry, def, PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME));
}
// Check for JSR-250 support, and if present add the CommonAnnotationBeanPostProcessor.
if (jsr250Present && !registry.containsBeanDefinition(COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)) {
RootBeanDefinition def = new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class);
def.setSource(source);
def.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
beanDefinitions.add(registerBeanPostProcessor(registry, def, COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
}
if (!registry.containsBeanDefinition(AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) {
RootBeanDefinition def = new RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class);
def.setSource(source);
def.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
beanDefinitions.add(registerBeanPostProcessor(registry, def, AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
}
if (!registry.containsBeanDefinition(REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) {
RootBeanDefinition def = new RootBeanDefinition(RequiredAnnotationBeanPostProcessor.class);
def.setSource(source);
def.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
beanDefinitions.add(registerBeanPostProcessor(registry, def, REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
}
return beanDefinitions;
}
主要註冊常用的:
RequiredAnnotationBeanPostProcessor
AutowiredAnnotationBeanPostProcessor
CommonAnnotationBeanPostProcessor
平時我們使用autowired或者required之所以能生效,就是因為這個自動注入ioc已經。
相關文章
- Spring 事務原始碼解析Spring原始碼
- Spring原始碼剖析9:Spring事務原始碼剖析Spring原始碼
- Spring事務原始碼解讀Spring原始碼
- Spring原始碼剖析8:Spring事務概述Spring原始碼
- 【spring原始碼學習】spring的事務管理的原始碼解析Spring原始碼
- spring事務管理原始碼分析(二)事務處理流程分析Spring原始碼
- spring原始碼解析 (七) 事務底層原始碼實現Spring原始碼
- Spring事務原始碼分析專題(一)JdbcTemplate使用及原始碼分析Spring原始碼JDBC
- 《四 spring原始碼》spring的事務註解@Transactional 原理分析Spring原始碼
- Dubbo原始碼分析十一、服務路由原始碼路由
- spring事務管理原始碼分析(一)配置和事務增強代理的生成流程Spring原始碼
- 關於spring事務原始碼的一些小理解Spring原始碼
- Spring中@Transactional事務回滾例項及原始碼Spring原始碼
- Spring之事務原始碼Spring原始碼
- Spring Security原始碼分析十一:Spring Security OAuth2整合JWTSpring原始碼OAuthJWT
- 分散式事務 TCC-Transaction 原始碼分析 —— 事務恢復分散式原始碼
- spring 事務Spring
- spring事務Spring
- 分散式事務 TCC-Transaction 原始碼解析 —— 事務儲存器分散式原始碼
- mybaits原始碼分析--事務管理(八)AI原始碼
- React原始碼分析 - 元件更新與事務React原始碼元件
- PandasTA 原始碼解析(十一)AST原始碼
- 分散式事務之Spring事務與JMS事務(二)分散式Spring
- Spring 事務管理Spring
- Spring配置事務Spring
- Spring事務管理Spring
- 18個示例詳解 Spring 事務傳播機制(附測試原始碼)Spring原始碼
- springboot 事務建立流程原始碼分析Spring Boot原始碼
- diffusers-原始碼解析-十一-原始碼
- Spring事務專題(四)Spring中事務的使用、抽象機制及模擬Spring事務實現Spring抽象
- 使用Spring Boot + Kafka實現Saga分散式事務模式的原始碼 - vinsguruSpring BootKafka分散式模式原始碼
- 【原始碼講解】Spring事務是如何應用到你的業務場景中的?原始碼Spring
- 什麼是事務、事務特性、事務隔離級別、spring事務傳播特性?Spring
- Spring系列.事務管理Spring
- Spring事務筆記Spring筆記
- Spring系列-事務管理Spring
- Spring-AOP事務Spring
- Spring的事務管理Spring