spring事務管理之一:程式設計式事務管理
概述
事務是一組邏輯上的操作,這組操作,要麼全部成功,要麼全部失敗。不會存在一部分操作失敗,一部分操作成功的情形。
事務特性
事務的四個屬性:原子性、一致性、隔離性、永續性。
- 原子性:是指事務是一個不可分割的工作單位,事務中的操作,要麼都發生,要麼都不發生。
- 一致性:是指事務前後,資料的完整性必須保持一致。
- 隔離性:是指多個使用者併發訪問資料庫時,一個使用者的事務,不能被其他使用者的事務所幹擾,多個併發事務之間資料要相互隔離。
- 永續性:是指一個事務,一旦被提交,他對資料庫中的資料的改變是永久性的,即使資料庫發生故障也不應該對其有任何影響。
spring對事務的管理,主要的API在spring-tx-version.jar中,spring提供了三個主要的介面:
- PlatformTransactionManager:事務管理器
- TransactionDefinition:事務定義資訊,包括隔離(isolation),傳播(propagation),只讀(readonly)等
- TransactionStatus:事務執行狀態
spring為不同的持久化框架,提供了不同PlatformTransactionManager介面實現。
- 如果我們使用spring-jdbc,他為我們提供的事務管理類是DataSourceTransactionManager。
- 如果我們使用hibernate,他為我們提供的事務管理器類是HibernateTransactionManager。
隔離性是為了解決髒讀,不可重複讀,幻影讀問題的。
- 髒讀:一個事務讀取了另一個事務改寫但還未提交的資料,如果這些資料被回滾,則讀到的資料是無效的。
- 不可重複讀:同一個事務中,多次讀取同一資料返回的結果有所不同,與髒讀的區別是,它側重事務提交。
- 幻影讀:一個事務讀取了幾行記錄,另一個事務插入一些記錄,幻讀就發生了,再後來的查詢中,第一個事務就會發現有些原來沒有的記錄,和不可重複讀的區別是:不可重複讀是資料發生了修改,導致不一樣,側重修改,幻讀是資料多了或者少了,側重新增和刪除。
隔離級別分為四種(DEFAULT是衍生出來的):
- DEFAULT:後端資料庫預設的隔離級別,mysql和oracle預設隔離級別分別是REPEATABLE_READ和READ_COMMITTED。
- READ_UNCOMMITED:容許你讀取還未提交的改變了的資料。會導致隔離級別的三類問題。
- READ_COMMITED:容許在併發事務已經提交後讀取,可防止髒讀,但幻讀和不可重複讀仍會發生。
- REPEATTABLE_READ:對相同欄位多次讀取結果是一樣的,除非資料被事務本身改變,可防止髒讀,不可重複讀,但幻讀還是會發生,通過鎖定特定的記錄行來實現。
- SERIALIZABLE:最嚴格的隔離級別,完全服從acid原則,確保隔離級別的任何問題都不會發生。它通過鎖整張表來實現。
傳播行為主要是用來解決多個業務之間方法相互呼叫時事務管理的問題。
事務的傳播行為分為以下幾種行為:
- PROPAGATION_REQUIRED:支援當前事務,如果不存在,就新建一個。
- PROPAGATION_SUPPORTS:支援當前事務,如果不存在,就不使用事務。
- PROPAGATION_MANDATORY:支援當前事務,如果不存在,就丟擲異常。
- PROPAGATION_REQUIRES_NEW:如果有事務存在,則掛起事務,新建一個事務。
- PROPAGATION_NOT_SUPPORTED:以非事務方式執行,如果有事務存在,掛起當前事務。
- PROPAGATION_NEVER:以非事務方式執行,如果有事務存在,就丟擲異常。
- PROPAGATION_NESTED:如果當前事務存在,則巢狀事務執行。
程式設計式事務管理是比較貼近jdbc時代,手動控制事務,例如:conn.setAutoCommit(false),conn.rollback()等。這種方式在開發中已經很少使用了,至於原因,看過這個示例,你就能明白。需要事務的地方,需要手動編寫事務程式碼,雖然有可用的模板,使用方式也固定,但是隨著業務方法的增加,這種頻繁編寫事務的程式碼,實在是不敢想象。
示例
這裡使用maven工程,工程結構如下:
pom.xml
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>4.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
</dependencies>
jdbc.properties
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql:///shop?useUnicode=true&useSSL=false
jdbc.username=hadoop
jdbc.password=hadoop
spring.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"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-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
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">
<context:property-placeholder location="classpath:jdbc.properties"/>
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<bean id="accountDao" class="com.xxx.springtransaction.dao.impl.AccountDaoImpl">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="accountService"
class="com.xxx.springtransaction.service.impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao"></property>
<property name="transactionTemplate" ref="transactionTemplate" />
</bean>
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="transactionTemplate"
class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="transactionManager" />
</bean>
</beans>
AccountDao.java
package com.xxx.springtransaction.dao;
public interface AccountDao {
public void transferIn(String id,Double money);
public void transferOut(String id,Double money);
public Double findById(String id);
}
AccountDaoService.java
package com.xxx.springtransaction.dao.impl;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
import com.xxx.springtransaction.dao.AccountDao;
public class AccountDaoImpl extends JdbcDaoSupport implements AccountDao {
@Override
public void transferIn(String id, Double money) {
String sql = "update account set money = money + ? where id = ?";
getJdbcTemplate().update(sql, money,id);
}
@Override
public void transferOut(String id, Double money) {
String sql = "update account set money = money - ? where id = ?";
getJdbcTemplate().update(sql, money,id);
}
@Override
public Double findById(String id){
return null;
}
}
AccountService.java
package com.xxx.springtransaction.service;
public interface AccountService {
public void transfer(String out,String in,Double money);
public Double findById(String id);
}
AccountServiceImpl.java
package com.xxx.springtransaction.service.impl;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import com.xxx.springtransaction.dao.AccountDao;
import com.xxx.springtransaction.service.AccountService;
public class AccountServiceImpl implements AccountService {
private TransactionTemplate transactionTemplate;
public void setTransactionTemplate(TransactionTemplate transactionTemplate) {
this.transactionTemplate = transactionTemplate;
}
private AccountDao accountDao;
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
@Override
public void transfer(final String out, final String in, final Double money) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus
transactionStatus) {
accountDao.transferOut(out, money);
//int i = 1/0;
//System.out.println(i);
accountDao.transferIn(in, money);
}
});
}
@Override
public Double findById(String id){
return accountDao.findById(id);
}
}
單元測試類:SpringTransactionTest.java
package com.xxx.springtransaction;
import javax.annotation.Resource;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.xxx.springtransaction.service.AccountService;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring.xml")
public class SpringTransactionTest {
@Resource
private AccountService accountService;
@Test
public void demo1(){
accountService.transfer("1", "2", 200d);
}
}
準備資料,這裡假設有一張賬戶表account,裡面有三個賬戶,分別是aaa,bbb,ccc,均有1000元,這裡模擬aaa給bbb轉賬200元。
mysql> select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | aaa | 1000 |
| 2 | bbb | 1000 |
| 3 | ccc | 1000 |
+----+------+-------+
3 rows in set
測試驗證方法:
1、在沒有事務管理或者正常的情況下,aaa給bbb直接轉賬,會成功,aaa的賬戶變為800,bbb的賬戶變為1200
mysql> select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | aaa | 800 |
| 2 | bbb | 1200 |
| 3 | ccc | 1000 |
+----+------+-------+
3 rows in set
2、在沒有事務管理的情況下,開啟AccountServiceImpl.java中以下程式碼註釋,讓業務方法丟擲異常。
int i = 1/0;
System.out.println(i);
這時候轉賬,aaa的賬戶會轉出200,但是bbb的賬戶因為異常,不會增加200.
mysql> select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | aaa | 600 |
| 2 | bbb | 1200 |
| 3 | ccc | 1000 |
+----+------+-------+
3 rows in set
3、給業務程式碼加上事務管理程式碼,如下所示,再次轉賬,丟擲異常,事務回滾,aaa的賬戶錢數不會減少,bbb的不會增加:
@Override
public void transfer(final String out, final String in, final Double money) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus
transactionStatus) {
accountDao.transferOut(out, money);
int i = 1/0;
System.out.println(i);
accountDao.transferIn(in, money);
}
});
}
轉賬結果:
mysql> select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | aaa | 600 |
| 2 | bbb | 1200 |
| 3 | ccc | 1000 |
+----+------+-------+
3 rows in set
這個結果說明事務管理生效了。