Spring Transaction 聲明式事務管理
參考資料:https://lfvepclr.gitbooks.io/spring-framework-5-doc-cn/content/
1. Spring 事務特性
事務 : 是邏輯上一組操作,要麼全都成功,要麼全都失敗。
事務特性 : ACID
- 原子性 : 事務不可分割
- 一致性 : 事務執行的前後,數據完整性保持一致
- 隔離性 : 一個事務執行的時候,不應該受到其他事務的打擾
- 持久性 : 一旦結束,數據就永久的保存到數據庫
如果不考慮隔離性:
髒讀 : 一個事務讀到另一個事務未提交數據
不可重複讀 : 一個事務讀到另一個事務已經提交數據(update)導致一個事務多次查詢結果不一致
虛讀 : 一個事務讀到另一個事務已經提交數據(insert)導致一個事務多次查詢結果不一致
事務的隔離級別:
- 未提交讀 : 以上情況都有可能發生。
- 已提交讀 : 避免髒讀,但不可重複讀,虛讀是有可能發生。
- 可重複讀 : 避免髒讀,不可重複讀,但是虛讀有可能發生。
- 串行的 : 避免以上所有情況。
mySQL 數據庫存儲引擎:innoDB(支持事務)、MyISAM(不支持事務)
2. Spring 事務API
-
PlatformTransactionManager : 平臺事務管理器
- getTransaction(TransactionDefinition definition)
- rollback(TransactionStatus status)
- commit(TransactionStatus status)
-
TransactionDefinition : 事務定義
- ISOLation_XXX:事務隔離級別(isolation)
- ISOLATION_DEFAULT : 默認級別,Mysql -->
repeatable_read
| Oracle -->>read_commited
- ISOLATION_READ_UNCOMMITTED
- ISOLATION_READ_COMMITTED
- ISOLATION_REPEATABLE_READ
- ISOLATION_SERIALIZABLE
- ISOLATION_DEFAULT : 默認級別,Mysql -->
- PROPAGATION_XXX:事務的傳播行爲
- ISOLation_XXX:事務隔離級別(isolation)
-
TransactionStatus : 事務狀態
是否有保存點
是否是一個新的事務
事務是否已經提交
關係 :
PlatformTransactionManager 通過 TransactionDefinition 設置事務相關信息管理事務,管理事務過程中,產生一些事務狀態,狀態由 TransactionStatus 記錄。
API詳解:
PlatformTransactionManager : 接口
Spring爲不同的持久化框架提供了不同 PlatformTransactionManager 接口實現。
使用Spring JDBC或iBatis 進行持久化數據時使用(★):
- org.springframework.jdbc.datasource.DataSourceTransactionManager
使用Hibernate進行持久化數據時使用:
- org.springframework.orm.hibernate.HibernateTransactionManager
使用JPA進行持久化時使用:
- org.springframework.orm.jpa.JpaTransactionManager
當持久化機制是Jdo時使用:
- org.springframework.jdo.JdoTransactionManager
使用一個JTA實現來管理事務,在一個事務跨越多個資源時必須使用:
- org.springframework.transaction.jta.JtaTransactionManager
3. Spring 事務傳播行爲
不是JDBC事務管理,用來解決實際開發的問題。
傳播行爲:解決業務層之間調用的事務的關係
(ServiceA.m() 方法與 ServiceB.m() 方法)
-
PROPAGATION_REQUIRED
: 支持當前事務,如果不存在 就新建一個A,B——如果A有事務,B使用A的事務,如果A沒有事務,B就開啓一個新的事務(A,B是在一個事務中)
-
PROPAGATION_SUPPORTS : 支持當前事務,如果不存在,就不使用事務
A,B——如果A有事務,B使用A的事務,如果A沒有事務,B就不使用事務
-
PROPAGATION_MANDATORY : 支持當前事務,如果不存在,拋出異常
A,B——如果A有事務,B使用A的事務,如果A沒有事務,拋出異常
-
PROPAGATION_REQUIRES_NEW
: 如果有事務存在,掛起當前事務,創建一個新的事務A,B——如果A有事務,B將A的事務掛起,重新創建一個新的事務(A,B不在一個事務中,事務互不影響)
-
PROPAGATION_NOT_SUPPORTED : 以非事務方式運行,如果有事務存在,掛起當前事務
A,B——非事務的方式運行,A有事務,就會掛起當前的事務
-
PROPAGATION_NEVER : 以非事務方式運行,如果有事務存在,拋出異常
-
PROPAGATION_NESTED
: 如果當前事務存在,則嵌套事務執行。基於 SavePoint 技術A,B——A有事務,A執行之後,將A事務執行之後的內容保存到SavePoint;B事務有異常的話,用戶需要自己設置事務提交還是回滾。
-
常用:
PROPAGATION_REQUIRED
PROPAGATION_REQUIRES_NEW
PROPAGATION_NESTED
4. Spring 事務分類
-
Spring 事務管理
分層開發:事務處在Service層。 -
Spring 事務管理分成兩類
-
編程式事務管理 : 手動編寫代碼完成事務管理 (xml配置 + transactionTemplate.execute())
-
聲明式事務管理
: 不需要手動編寫代碼和配置 (★ @Transactional 註解)
-
4.1 編程式事務
- TransactionTemplate
在 doIntransaction 裏處理邏輯。如果出異常了,就執行 isRollbackOnly 方法進行回滾。
@Autowired
TransactionTemplate transactionTemplate;
transactionTemplate.execute((TransactionStatus transactionStatus) -> {
try {
//...
} catch (Exception e) {
transactionStatus.isRollbackOnly();
throw e;
}
return null;
});
- TransactionManager
手動 commit,異常就 rollback
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
userRepository.save(user);
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
e.printStackTrace();
}
4.2 聲明式事務
Spring 事務的本質是數據庫對事務的支持。
支持方式:
@EnableTransactionManagement
註解方式,開啓對事務註解的解析<tx:annotation-driven />
XML配置方式,開啓 spring 對註解事務的支持
5. Spring 事務操作轉賬
5.1 無事務操作
- 創建表
CREATE TABLE `account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL,
`money` double DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
INSERT INTO `account` VALUES ('1', 'aaa', '1000');
INSERT INTO `account` VALUES ('2', 'bbb', '1000');
- 創建項目,導入依賴
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!-- Spring 事務依賴 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.9</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.32</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
- 創建接口和類 Dao & Service
// 1.AccountDao
public interface AccountDao {
// 加錢
void increaseMoney(Integer id, Double money);
// 減錢
void decreaseMoney(Integer id, Double money);
}
// 2.AccountDaoImpl
// 繼承 JdbcDaoSupport 可以直接調用父類方法(含了1個 jdbcTemplate)
public class AccountDaoImpl extends JdbcDaoSupport implements AccountDao {
@Override
public void increaseMoney(Integer id, Double money) {
getJdbcTemplate().update("update account set money = money+? where id = ? ", money,id);
}
@Override
public void decreaseMoney(Integer id, Double money) {
getJdbcTemplate().update("update account set money = money-? where id = ? ", money,id);
}
}
// 3.AccountService
public interface AccountService {
//轉賬方法
void transfer(Integer from,Integer to,Double money);
}
// 4.AccountServiceImpl
public class AccountServiceImpl implements AccountService {
private AccountDao accountDao;
@Override
public void transfer(Integer from, Integer to, Double money) {
// 減錢
accountDao.decreaseMoney(from, money);
// int i = 1 / 0;// 如果發生異常數據(錢)會丟失,數據不一致
// 加錢
accountDao.increaseMoney(to, money);
}
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
}
- 創建 applicationContext.xml 引入事務
tx
約束
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans" 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-4.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.2.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.2.xsd ">
<bean name="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="jdbc:mysql://localhost:3306/數據庫名"></property>
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="username" value="root"></property>
<property name="password" value="root"></property>
</bean>
<bean name="accountDao" class="com.AccountDaoImpl">
<property name="dataSource" ref="dataSource"></property>
</bean>
<bean name="accountService" class="com.AccountServiceImpl">
<property name="accountDao" ref="accountDao"></property>
</bean>
</beans>
5.測試
public class Demo {
@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
AccountService accountService = (AccountService)context.getBean("accountService");
accountService.transfer(1, 2, 100d);
}
}
5.2 XML 配置事務
無事務操作時,如果在轉賬方法中出現異常後,數據前後會產生不一致,此時,我們需要用 Spring 的事務管理來解決這一問題。
手動編碼的方式完成事務管理,缺點 : 代碼量增加,代碼有侵入性。
// 修改AccountServiceImpl
public class AccountServiceImpl implements AccountService {
private AccountDao accountDao;
// TransactionTemplate 事務模板類
private TransactionTemplate transactionTemplate;
@Override
public void transfer(Integer from, Integer to, Double money) {
// 事務模板調用 execute 來自動開啓、關閉、以及異常時自動回滾操作
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
// 減錢
accountDao.decreaseMoney(from, money);
int i = 1 / 0; // 如果發生異常數據(錢)不會丟失,數據保持一致
// 加錢
accountDao.increaseMoney(to, money);
}
});
}
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
public void setTransactionTemplate(TransactionTemplate transactionTemplate) {
this.transactionTemplate = transactionTemplate;
}
}
<!-- applicationContext.xml -->
<bean name="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="jdbc:mysql://localhost:3306/mydb" />
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="username" value="root"></property>
<property name="password" value="root"></property>
</bean>
<!-- TransactionManager 事務核心管理器,封裝了所有事務操作,依賴於連接池 -->
<bean name="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- TransactionTemplate 事務模板對象,依賴於事務核心管理器 -->
<bean name="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="transactionManager" />
</bean>
<bean name="accountDao" class="com.AccountDaoImpl">
<property name="dataSource" ref="dataSource" />
</bean>
<bean name="accountService" class="com.AccountServiceImpl">
<property name="accountDao" ref="accountDao" />
<!-- 爲 accountService 注入事務模板對象 -->
<property name="transactionTemplate" ref="transactionTemplate" />
</bean>
此時調用測試類測試,在轉賬出現異常時,Spring 事務會自動進行回滾,讓數據保持一致。
5.3 配置事務通知
修改 applicationContext.xml,配置事務通知是需注意:
企業中配置CRUD方法一般使用 方法名+通配符*
的形式配置通知,此時類中的方法名要和配置的方法名一致。
使用 <tx:advice> <tx:attributes> <tx:method ... />
配置指定方法的事務通知。
- 以方法爲單位,指定方法應用什麼事務屬性:
- isolation:用於指定事務的
隔離級別
。默認值是 DEFAULT,表示使用數據庫的默認隔離級別。 - propagation:用於指定事務的
傳播行爲
。默認值是 REQUIRED,表示一定會有事務,增刪改的選擇。查詢方法可以選擇 SUPPORTS。 - read-only:用於指定事務是否
只讀
。只有查詢方法才能設置爲true。默認值是false,表示讀寫。 - timeout:用於指定事務的
超時時間
,默認值是-1,表示永不超時。如果指定了數值,以秒爲單位。 - rollback-for:用於
指定一個異常,決定是否回滾
,當產生該異常時,事務回滾,產生其他異常時,事務不回滾。沒有默認值。表示任何異常都回滾。 - no-rollback-for:用於
指定一個異常,決定是否不回滾
,當產生該異常時,事務不回滾,產生其他異常時事務回滾。沒有默認值。表示任何異常都回滾。
- isolation:用於指定事務的
<bean name="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="jdbc:mysql://localhost:3306/mydb"></property>
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="username" value="root"></property>
<property name="password" value="root"></property>
</bean>
<!-- 事務核心管理器,封裝了所有事務操作. 依賴於連接池 -->
<bean name="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置事務通知 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="save*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false" />
<tx:method name="persist*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false" />
<tx:method name="update*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false" />
<tx:method name="modify*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false" />
<tx:method name="delete*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false" />
<tx:method name="remove*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false" />
<tx:method name="get*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="true" />
<tx:method name="find*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="true" />
<tx:method name="transfer" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false" />
</tx:attributes>
</tx:advice>
<!-- 配置織入 -->
<aop:config>
<!-- 配置切點表達式 -->
<aop:pointcut expression="execution(* com.qf.service.*ServiceImpl.*(..))" id="txPc" />
<!-- 配置切面 : 通知+切點 advice-ref:通知的名稱 pointcut-ref:切點的名稱 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPc" />
</aop:config>
<bean name="accountDao" class="com.AccountDaoImpl">
<property name="dataSource" ref="dataSource"></property>
</bean>
<bean name="accountService" class="com.AccountServiceImpl">
<property name="accountDao" ref="accountDao"></property>
</bean>
5.4 註解 配置事務
註解配置(aop)的方式完成事務管理,修改 applicationContext.xml :
<tx:annotation-driven />
開啓 spring 對註解事務的支持。
<bean name="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="jdbc:mysql://localhost:3306/mydb" />
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="username" value="root" />
<property name="password" value="root" />
</bean>
<!-- 事務核心管理器,封裝了所有事務操作. 依賴於連接池 -->
<bean name="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 開啓 spring 對註解事務的支持-->
<tx:annotation-driven />
<bean name="accountDao" class="com.AccountDaoImpl">
<property name="dataSource" ref="dataSource" />
</bean>
<bean name="accountService" class="com.AccountServiceImpl">
<property name="accountDao" ref="accountDao" />
</bean>
@Transactional 註解
屬性 | 類型 | 描述 |
---|---|---|
value | String | 事務管理器 |
propagation | Propagation | 傳播級別 |
isolation | Isolation | 隔離級別 |
readOnly | boolean | 讀/寫與只讀事務 |
timeout | int | 事務超時(秒) |
rollbackFor | Class | 觸發事務回滾的類,默認只對未檢查異常有效 |
noRollbackFor | Class | 設置不需要進行回滾的異常類數組 |
- @Transactional 特性
- 類上添加 @Transactional,在每個方法單開一個事務,管理方式相同。
- @Transactional 註解只在 public 方法上起作用。
- 默認只對未檢查異常回滾
- 只讀事務只在事務啓動時應用,否則即使配置也會被忽略。
修改 AccountServiceImpl :
// 註解添加聲明式事務
@Transactional(isolation=Isolation.REPEATABLE_READ,propagation=Propagation.REQUIRED,readOnly=true)
public class AccountServiceImpl implements AccountService {
private AccountDao accountDao;
@Override
//如果該方法與類名上的配置不同,可以單獨在這個方法上配置註解,單獨生效自己的
@Transactional(isolation= Isolation.REPEATABLE_READ,propagation= Propagation.REQUIRED,readOnly=false)
public void transfer(Integer from, Integer to, Double money) {
// 減錢
accountDao.decreaseMoney(from, money);
int i = 1 / 0;// 如果發生異常數據(錢)不會丟失,數據保持一致
// 加錢
accountDao.increaseMoney(to, money);
}
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
}