Spring事務管理:ACID的概念,Spring事務管理核心介面,基於XML方式的宣告式事務、基於註解(Annotation)方式的宣告式事務
一、事務的概念可以描述為具有以下四個關鍵屬性,也就是ACID
-
原子性(Atomicity):事務應該當作一個單獨單元的操作,這意味著整個序列操作要麼是成功,要麼是失敗;
-
一致性(Consistency):這表示資料庫的引用完整性的一致性,表中唯一的主鍵等;
-
隔離性(Isolation):可能同時處理很多有相同的資料集的事務,每個事務應該與其他事務隔離,以防止資料損壞;
- 永續性(Durability):一個事務一旦完成全部操作,這個事務的結果必須是永久性的,不能因系統故障而從資料庫中刪除。
二、Spring 事務管理的核心介面有三個
1. PlatformTransactionManager:平臺事務管理器,主要具有以下三個方法
- TransactionStatus getTransaction(TransactionDefinition definition); 用於獲取事務狀態資訊
- void commit(TransactionStatus status); 用於提交事務
- void rollback(TransactionStatus status); 用於回滾事務
PlatformTransactionManager介面只是代表事務管理的介面,常見的幾個實現類如下:
- org.springframework.jdbc.datasource.DataSourceTransactionManager 用於配置JDBC資料來源的事務管理器
- org.springframework.orm.hibernate4.HibernateTransactionManager 用於配置Hibernate的事務管理器
- org.springframework.transaction.jta.JtaTransactionManager 用於配置全域性事務管理器
2. TransactionDefinition:事務定義
TransactionDefinition介面是事務定義(描述)的物件,該物件中定義了事務規則,並提供了獲取事務相關資訊的方法,具體如下表所示:
方法 | 說明 |
String getName( ); | 獲取事務物件名稱 |
int getIsolationLevel( ); | 獲取事務的隔離級別 |
int getPropagationBehavior( ); | 獲取事務的傳播行為 |
int getTimeout( ); | 獲取事務的超時時間 |
boolean isReadOnly( ); | 獲取事務是否只讀 |
上述方法中,事務的傳播行為是指在同一個方法中,不同操作前後所使用的事務傳播行為有很多種,具體如下圖所示:
在事務管理過程中,傳播行為可以控制是否需要建立事務以及如何建立事務,通常情況下,資料的查詢不會影響原資料的改變,所以不需要進行事務管理,而對於資料的插入、更新和刪除操作,必須進行事務管理。如果沒有指定事務的傳播行為,Spring預設傳播行為是REQUIRED。
3. TransactionStatus:事務狀態
它描述某一時間點上事務在狀態資訊,具體如下圖所示:
三、Spring 支援兩種型別的事務管理
- 程式設計式事務管理 :這意味著你在程式設計中管理事務,它給你極大的靈活性,但卻很難維護。本篇不對此種方式的實現展開討論。
- 宣告式事務管理 :這意味著你可以從業務程式碼中分離事務管理,它可以使用XML方式或註解方式來管理事務。本篇僅對XML方式和註解方式實現事務展開討論。
四、基於XML方式的宣告式事務
1. 建立表(MySQL資料庫)
create table account(id int primary key auto_increment,username varchar(50),balance double);
2. 建立實體類
package com.itheima.jdbc; public class Account { private Integer id; // 賬戶id private String username; // 使用者名稱 private Double balance; // 賬戶餘額 public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public Double getBalance() { return balance; } public void setBalance(Double balance) { this.balance = balance; } public String toString() { return "Account [id=" + id + ", " + "username=" + username + ", balance=" + balance + "]"; } }
3. 建立介面
package com.itheima.jdbc; import java.util.List; public interface AccountDao { // 新增 public int addAccount(Account account); // 更新 public int updateAccount(Account account); // 刪除 public int deleteAccount(int id); // 通過id查詢 public Account findAccountById(int id); // 查詢所有賬戶 public List<Account> findAllAccount(); // 轉賬 public void transfer(String outUser,String inUser,Double money); }
4. 建立實現類
package com.itheima.jdbc; import java.util.List; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; public class AccountDaoImpl implements AccountDao { // 宣告JdbcTemplate屬性及其setter方法 private JdbcTemplate jdbcTemplate; public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } // 新增賬戶 public int addAccount(Account account) { // 定義SQL String sql = "insert into account(username,balance) value(?,?)"; // 定義陣列來存放SQL語句中的引數 Object[] obj = new Object[] { account.getUsername(), account.getBalance() }; // 執行新增操作,返回的是受SQL語句影響的記錄條數 int num = this.jdbcTemplate.update(sql, obj); return num; } // 更新賬戶 public int updateAccount(Account account) { // 定義SQL String sql = "update account set username=?,balance=? where id = ?"; // 定義陣列來存放SQL語句中的引數 Object[] params = new Object[] { account.getUsername(), account.getBalance(), account.getId() }; // 執行新增操作,返回的是受SQL語句影響的記錄條數 int num = this.jdbcTemplate.update(sql, params); return num; } // 刪除賬戶 public int deleteAccount(int id) { // 定義SQL String sql = "delete from account where id = ? "; // 執行新增操作,返回的是受SQL語句影響的記錄條數 int num = this.jdbcTemplate.update(sql, id); return num; } // 通過id查詢賬戶資料資訊 public Account findAccountById(int id) { // 定義SQL語句 String sql = "select * from account where id = ?"; // 建立一個新的BeanPropertyRowMapper物件 RowMapper<Account> rowMapper = new BeanPropertyRowMapper<Account>(Account.class); // 將id繫結到SQL語句中,並通過RowMapper返回一個Object型別的單行記錄 return this.jdbcTemplate.queryForObject(sql, rowMapper, id); } // 查詢所有賬戶資訊 public List<Account> findAllAccount() { // 定義SQL語句 String sql = "select * from account"; // 建立一個新的BeanPropertyRowMapper物件 RowMapper<Account> rowMapper = new BeanPropertyRowMapper<Account>(Account.class); // 執行靜態的SQL查詢,並通過RowMapper返回結果 return this.jdbcTemplate.query(sql, rowMapper); } /** * 轉賬 * inUser:收款人 * outUser:匯款人 * money:收款金額 */ public void transfer(String outUser, String inUser, Double money) { // 收款時,收款使用者的餘額=現有餘額+所匯金額 this.jdbcTemplate.update("update account set balance = balance +? " + "where username = ?",money, inUser); // 模擬系統執行時的突發性問題 int i = 1/0; // 匯款時,匯款使用者的餘額=現有餘額-所匯金額 this.jdbcTemplate.update("update account set balance = balance-? " + "where username = ?",money, outUser); } }
5. 建立配置檔案(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-4.3.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd"> <!-- 1.配置資料來源 --> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <!--資料庫驅動 --> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> <!--連線資料庫的url --> <property name="url" value="jdbc:mysql://localhost:3306/xuejia" /> <!--連線資料庫的使用者名稱 --> <property name="username" value="root" /> <!--連線資料庫的密碼 --> <property name="password" value="admin" /> </bean> <!-- 2.配置JDBC模板 --> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <!-- 預設必須使用資料來源 --> <property name="dataSource" ref="dataSource" /> </bean> <!--3.定義id為accountDao的Bean --> <bean id="accountDao" class="com.itheima.jdbc.AccountDaoImpl"> <!-- 將jdbcTemplate注入到AccountDao例項中 --> <property name="jdbcTemplate" ref="jdbcTemplate" /> </bean> <!-- 4.事務管理器,依賴於資料來源 --> <bean id="transactionManager" class= "org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> <!-- 5.編寫通知:對事務進行增強(通知),需要編寫對切入點和具體執行事務細節 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!-- name:*表示任意方法名稱 --> <tx:method name="*" propagation="REQUIRED" isolation="DEFAULT" read-only="false" /> </tx:attributes> </tx:advice> <!-- 6.編寫aop,讓spring自動對目標生成代理,需要使用AspectJ的表示式 --> <aop:config> <!-- 切入點 --> <aop:pointcut expression="execution(* com.itheima.jdbc.*.*(..))" id="txPointCut" /> <!-- 切面:將切入點與通知整合 --> <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut" /> </aop:config> </beans>
6. 建立測試程式
package com.itheima.jdbc; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; //測試類 public class TransactionTest { @Test public void xmlTest() { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); // 獲取AccountDao例項 AccountDao accountDao = (AccountDao) applicationContext.getBean("accountDao"); // 增加兩個使用者 Account account1 = new Account(); account1.setUsername("Jack"); account1.setBalance(1000.00); int num1 = accountDao.addAccount(account1); Account account2 = new Account(); account2.setUsername("Rose"); account2.setBalance(500.00); int num2 = accountDao.addAccount(account2); // 呼叫例項中的轉賬方法 accountDao.transfer("Jack", "Rose", 100.0); // 輸出提示資訊 System.out.println("轉賬成功!"); } }
7. 執行
系統扔出異常:java.lang.ArithmeticException: / by zero
8. 修改程式,刪除實現類中的如下程式碼
// 模擬系統執行時的突發性問題 int i = 1/0;
9. 再次執行,結果如下
轉賬成功!
10. 查詢資料庫表中的資料
注意,表中出現兩個Jack,兩個Rose,並且他們的餘額都發生改變。因為addAccount執行了兩次,每次增加兩個人。
五、基於註解宣告式的事務
對上面的程式做如下修改:
1. 修改介面實現類
package com.itheima.jdbc; import java.util.List; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; public class AccountDaoImpl implements AccountDao { // 宣告JdbcTemplate屬性及其setter方法 private JdbcTemplate jdbcTemplate; public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } // 新增賬戶 public int addAccount(Account account) { // 定義SQL String sql = "insert into account(username,balance) value(?,?)"; // 定義陣列來存放SQL語句中的引數 Object[] obj = new Object[] { account.getUsername(), account.getBalance() }; // 執行新增操作,返回的是受SQL語句影響的記錄條數 int num = this.jdbcTemplate.update(sql, obj); return num; } // 更新賬戶 public int updateAccount(Account account) { // 定義SQL String sql = "update account set username=?,balance=? where id = ?"; // 定義陣列來存放SQL語句中的引數 Object[] params = new Object[] { account.getUsername(), account.getBalance(), account.getId() }; // 執行新增操作,返回的是受SQL語句影響的記錄條數 int num = this.jdbcTemplate.update(sql, params); return num; } // 刪除賬戶 public int deleteAccount(int id) { // 定義SQL String sql = "delete from account where id = ? "; // 執行新增操作,返回的是受SQL語句影響的記錄條數 int num = this.jdbcTemplate.update(sql, id); return num; } // 通過id查詢賬戶資料資訊 public Account findAccountById(int id) { // 定義SQL語句 String sql = "select * from account where id = ?"; // 建立一個新的BeanPropertyRowMapper物件 RowMapper<Account> rowMapper = new BeanPropertyRowMapper<Account>(Account.class); // 將id繫結到SQL語句中,並通過RowMapper返回一個Object型別的單行記錄 return this.jdbcTemplate.queryForObject(sql, rowMapper, id); } // 查詢所有賬戶資訊 public List<Account> findAllAccount() { // 定義SQL語句 String sql = "select * from account"; // 建立一個新的BeanPropertyRowMapper物件 RowMapper<Account> rowMapper = new BeanPropertyRowMapper<Account>(Account.class); // 執行靜態的SQL查詢,並通過RowMapper返回結果 return this.jdbcTemplate.query(sql, rowMapper); } @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, readOnly = false) public void transfer(String outUser, String inUser, Double money) { // 收款時,收款使用者的餘額=現有餘額+所匯金額 this.jdbcTemplate.update("update account set balance = balance +? " + "where username = ?",money, inUser); // 模擬系統執行時的突發性問題 int i = 1/0; // 匯款時,匯款使用者的餘額=現有餘額-所匯金額 this.jdbcTemplate.update("update account set balance = balance-? " + "where username = ?",money, outUser); } }
2. 新建配置檔案(applicationContext-annotation.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-4.3.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd"> <!-- 1.配置資料來源 --> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <!--資料庫驅動 --> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> <!--連線資料庫的url --> <property name="url" value="jdbc:mysql://localhost:3306/xuejia" /> <!--連線資料庫的使用者名稱 --> <property name="username" value="root" /> <!--連線資料庫的密碼 --> <property name="password" value="admin" /> </bean> <!-- 2.配置JDBC模板 --> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <!-- 預設必須使用資料來源 --> <property name="dataSource" ref="dataSource" /> </bean> <!--3.定義id為accountDao的Bean --> <bean id="accountDao" class="com.itheima.jdbc.AccountDaoImpl"> <!-- 將jdbcTemplate注入到AccountDao例項中 --> <property name="jdbcTemplate" ref="jdbcTemplate" /> </bean> <!-- 4.事務管理器,依賴於資料來源 --> <bean id="transactionManager" class= "org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> <!-- 5.註冊事務管理器驅動 --> <tx:annotation-driven transaction-manager="transactionManager"/> </beans>
3. 修改測試程式
package com.itheima.jdbc; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; //測試類 public class TransactionTest { @Test public void annotationTest() { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext-annotation.xml"); // 獲取AccountDao例項 AccountDao accountDao = (AccountDao) applicationContext.getBean("accountDao"); // 呼叫例項中的轉賬方法 accountDao.transfer("Jack", "Rose", 100.0); // 輸出提示資訊 System.out.println("轉賬成功!"); } }
4.執行
系統扔出異常:java.lang.ArithmeticException: / by zero
5. 修改程式,刪除實現類中的如下程式碼
// 模擬系統執行時的突發性問題 int i = 1/0;
6. 再次執行,結果如下
轉賬成功!
7. 查詢資料庫表中的資料
注意,表中出現兩個Jack,兩個Rose,並且他們的餘額再次發生改變。
本文參考:《Java EE企業級應用開發教程》