Spring 事物介紹(二)之 事物的傳播機(jī)制
Spring中對事物的支持
Spring 事物相關(guān)API:
spring事物是在數(shù)據(jù)庫事物的基礎(chǔ)上進(jìn)行封裝擴(kuò)展嘹害,其主要特性如下:
- 支持原有的數(shù)據(jù)事物的隔離級別
- 加入了事物傳播的概念慎玖,提供多個事物的合并和隔離的功能
- 提供聲明式事物咽弦,讓業(yè)務(wù)代碼與事物分離赡译,事物更易用
spring提供了三個接口用來使用事物:
- TransactionDefinition :事物定義
- PlatformTransactionManager :事物的管理
- TransactionStatus : 事物運(yùn)行狀態(tài)
TransactionDefinition.java
package org.springframework.transaction;
public interface TransactionDefinition {
int PROPAGATION_REQUIRED = 0;
int PROPAGATION_SUPPORTS = 1;
int PROPAGATION_MANDATORY = 2;
int PROPAGATION_REQUIRES_NEW = 3;
int PROPAGATION_NOT_SUPPORTED = 4;
int PROPAGATION_NEVER = 5;
int PROPAGATION_NESTED = 6;
int ISOLATION_DEFAULT = -1;
int ISOLATION_READ_UNCOMMITTED = 1;
int ISOLATION_READ_COMMITTED = 2;
int ISOLATION_REPEATABLE_READ = 4;
int ISOLATION_SERIALIZABLE = 8;
int TIMEOUT_DEFAULT = -1;
int getPropagationBehavior();
int getIsolationLevel();
int getTimeout();
boolean isReadOnly();
String getName();
}
PlatformTransactionManager.java
package org.springframework.transaction;
public interface PlatformTransactionManager {
TransactionStatus getTransaction(TransactionDefinition var1) throws TransactionException;
void commit(TransactionStatus var1) throws TransactionException;
void rollback(TransactionStatus var1) throws TransactionException;
}
TransactionStatus.java
package org.springframework.transaction;
import java.io.Flushable;
public interface TransactionStatus extends SavepointManager, Flushable {
boolean isNewTransaction();
boolean hasSavepoint();
void setRollbackOnly();
boolean isRollbackOnly();
void flush();
boolean isCompleted();
}
API應(yīng)用demo:
pom.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.demo.spring</groupId>
<artifactId>spring-tx-test</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<spring.version>4.3.8.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.35</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>4.0.4.RELEASE</version>
</dependency>
</dependencies>
</project>
SpringTransactionTest.java
package com.demo.spring;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* com.demo.spring
*
* @author Zyy
* @date 2019/2/13 20:51
*/
public class SpringTransactionTest {
private static String jdbcUrl = "jdbc:mysql://192.168.5.104:3306/spring";
private static String userName = "root";
private static String password = "root";
public static Connection openConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(jdbcUrl, userName, password);
return conn;
}
public static void main(String[] args) {
final DataSource dataSource = new DriverManagerDataSource(jdbcUrl, userName, password);
TransactionTemplate template = new TransactionTemplate();
template.setTransactionManager(new DataSourceTransactionManager(dataSource));
template.execute(new TransactionCallback<Object>() {
public Object doInTransaction(TransactionStatus transactionStatus) {
Connection connection = DataSourceUtils.getConnection(dataSource);
Object savePoint = null;
try {
{
PreparedStatement preparedStatement = connection.prepareStatement(
"insert into account(accountName,user,money) VALUES (?,?,?)");
preparedStatement.setString(1,"111");
preparedStatement.setString(2,"a");
preparedStatement.setInt(3,100);
preparedStatement.executeUpdate();
}
savePoint = transactionStatus.createSavepoint();
{
PreparedStatement preparedStatement = connection.prepareStatement(
"insert into account(accountName,user,money) VALUES (?,?,?)");
preparedStatement.setString(1,"222");
preparedStatement.setString(2,"b");
preparedStatement.setInt(3,100);
preparedStatement.executeUpdate();
}
{
PreparedStatement preparedStatement = connection.prepareStatement(
"update account set money= money+1 where user=?");
preparedStatement.setString(1,"333");
//手動設(shè)置異常
int i = 1/0;
}
} catch (SQLException e) {
e.printStackTrace();
} catch (Exception e) {
System.out.println("update failed");
if (savePoint != null) {
transactionStatus.rollbackToSavepoint(savePoint);
} else {
transactionStatus.setRollbackOnly();
}
}
return null;
}
});
}
}
執(zhí)行結(jié)果:
update failed
但是savePoint之前的插入是成功的狸相。
這是因?yàn)?/p>
public Object doInTransaction(TransactionStatus transactionStatus) {
Connection connection = DataSourceUtils.getConnection(dataSource);
...
}
TransactionStatus transactionStatus中的Connection與Connection與DataSourceUtils.getConnection(dataSource)返回的是同一個Connection矮男。
相關(guān)源碼:
org.springframework.transaction.support.TransactionSynchronizationManager#bindResource
public static void bindResource(Object key, Object value) throws IllegalStateException {
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
Assert.notNull(value, "Value must not be null");
Map<Object, Object> map = (Map)resources.get();
if (map == null) {
map = new HashMap();
resources.set(map);
}
Object oldValue = ((Map)map).put(actualKey, value);
if (oldValue instanceof ResourceHolder && ((ResourceHolder)oldValue).isVoid()) {
oldValue = null;
}
if (oldValue != null) {
throw new IllegalStateException("Already value [" + oldValue + "] for key [" + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]");
} else {
if (logger.isTraceEnabled()) {
logger.trace("Bound value [" + value + "] for key [" + actualKey + "] to thread [" + Thread.currentThread().getName() + "]");
}
}
}
其中((Map)map).put(actualKey, value)中的
actualKey
org.springframework.jdbc.datasource.DriverManagerDataSource
value
org.springframework.jdbc.datasource.ConnectionHolder
這個map已經(jīng)放入到resources中了槐秧,resources是一個ThreadLocal,取的時(shí)候也去這里面取
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal("Transactional resources");
get時(shí)的相關(guān)源碼
org.springframework.transaction.support.TransactionSynchronizationManager#doGetResource
private static Object doGetResource(Object actualKey) {
Map<Object, Object> map = (Map)resources.get();
if (map == null) {
return null;
} else {
Object value = map.get(actualKey);
if (value instanceof ResourceHolder && ((ResourceHolder)value).isVoid()) {
map.remove(actualKey);
if (map.isEmpty()) {
resources.remove();
}
value = null;
}
return value;
}
}
所以
if (savePoint != null) {
transactionStatus.rollbackToSavepoint(savePoint);
} else {
transactionStatus.setRollbackOnly();
}
在執(zhí)行回滾時(shí)是同一個Connection.rollback()掖蛤。
聲明式事物
如果僅僅只是提供API杀捻,在進(jìn)行數(shù)據(jù)庫操作時(shí),仍很麻煩蚓庭,所以Spring還提出了聲明式事物致讥,@Transactional注解仅仆。
聲明式事物demo。
pom.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.demo.spring</groupId>
<artifactId>spring-tx-test</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<spring.version>4.3.8.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.35</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>4.0.4.RELEASE</version>
</dependency>
</dependencies>
</project>
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: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/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.demo.spring.*"/>
<aop:aspectj-autoproxy expose-proxy="true"/>
<bean class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<constructor-arg name="url" value="jdbc:mysql://192.168.5.104/spring"/>
<constructor-arg name="username" value="root"/>
<constructor-arg name="password" value="root"/>
</bean>
<tx:annotation-driven transaction-manager="txManager"/>
</beans>
Account.java
package com.demo.spring.service;
import java.io.Serializable;
/**
* com.demo.spring.service
*
* @author Zyy
* @date 2019/2/14 22:00
*/
public class Account implements Serializable {
private static final long serialVersionUID = 1L;
private int id;
private String accountName;
private String user;
private String money;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getAccountName() {
return accountName;
}
public void setAccountName(String accountName) {
this.accountName = accountName;
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
public String getMoney() {
return money;
}
public void setMoney(String money) {
this.money = money;
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", accountName='" + accountName + '\'' +
", user='" + user + '\'' +
", money='" + money + '\'' +
'}';
}
}
AccountService.java
package com.demo.spring.service;
import java.util.List;
import java.util.Map;
/**
* com.demo.spring.service
*
* @author Zyy
* @date 2019/2/14 21:54
*/
public interface AccountService {
void addAccount(String name, int money);
int updateAccount(String name, int money);
List<Map<String, Object>> queryAccount(String name);
}
AccountServiceImpl.java
package com.demo.spring.service;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* com.demo.spring.service
*
* @author Zyy
* @date 2019/2/14 21:58
*/
@Service
public class AccountServiceImpl implements AccountService{
@Resource
private JdbcTemplate jdbcTemplate;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addAccount(String name, int money) {
String accountid = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());
jdbcTemplate.update("insert into account (accountname,user,money) values (?,?,?)", accountid, name, money);
}
@Transactional
public List<Map<String, Object>> queryAccount(String name) {
List<Map<String, Object>> list = jdbcTemplate.queryForList("select * from account where user = ?", name);
return list;
}
@Transactional
public int updateAccount(String name, int money) {
return jdbcTemplate.update("update account set money = money + ? where user = ?", money, name);
}
}
AccountServiceTest.java
package com.demo.spring;
import com.demo.spring.service.AccountService;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* com.demo.spring
*
* @author Zyy
* @date 2019/2/14 22:15
*/
public class AccountServiceTest {
@Test
public void add() {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("spring-tx.xml");
AccountService accountService = context.getBean(AccountService.class);
accountService.addAccount("ayang",100);
}
@Test
public void update() {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("spring-tx.xml");
AccountService accountService = context.getBean(AccountService.class);
accountService.updateAccount("ayang",110);
}
@Test
public void select() {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("spring-tx.xml");
AccountService accountService = context.getBean(AccountService.class);
List<Map<String, Object>> list = accountService.queryAccount("ayang");
System.out.println(Arrays.toString(list.toArray()));
}
}
Spring事物默認(rèn)的傳播機(jī)制:
@Transactional(propagation = Propagation.REQUIRES_NEW)
下面介紹一下spring事物的傳播機(jī)制垢袱。
Spring事物傳播機(jī)制
類別 | 事物傳播類型 | 說明 |
---|---|---|
支持當(dāng)前事物 | PROPAGATION_REQUIRED(必須的) | 如果當(dāng)前沒有事物墓拜,就新建一個事物,如果已經(jīng)存在一個事物中请契,加入到這個事物中咳榜。這是最常見的選擇。 |
支持當(dāng)前事物 | PROPAGATION_SUPPORTS(支持) | 支持當(dāng)前事物姚糊,如果當(dāng)前沒有事物,就以非事物方式執(zhí)行授舟。 |
支持當(dāng)前事物 | PROPAGATION_MANDATORY(強(qiáng)制) | 使用當(dāng)前的事物救恨,如果當(dāng)前沒有事物,就拋出異常释树。 |
不支持當(dāng)前事物 | PROPAGATION_REQUIRES_NEW(隔離) | 新建事物肠槽,如果當(dāng)前存在事物,把當(dāng)前事物掛起奢啥。 |
不支持當(dāng)前事物 | PROPAGATION_NOT_SUPPORTED(不支持) | 以非事物方式執(zhí)行操作秸仙,如果當(dāng)前存在事物,就把當(dāng)前事物掛起桩盲。 |
不支持當(dāng)前事物 | PROPAGATION_NEVER(強(qiáng)制非事物) | 以非事物方式執(zhí)行寂纪,如果當(dāng)前存在事物,則拋出異常赌结。 |
嵌套事物 | PROPAGATION_NESTED(嵌套事物) | 如果當(dāng)前存在事物捞蛋,則在嵌套事物內(nèi)執(zhí)行。如果當(dāng)前沒有事物柬姚,則執(zhí)行與PROPAGATION_REQUIRED類似的操作拟杉。 |
常用的事物傳播機(jī)制:
- PROPAGATION_REQUIRED
如果當(dāng)前沒有事物,就新建一個事物量承,如果已經(jīng)存在一個事物中搬设,加入到這個事物中這個是默認(rèn)傳播機(jī)制。
- PROPAGATION_NOT_SUPPORTED
以非事物方式執(zhí)行操作撕捍,如果當(dāng)前存在事物拿穴,就把當(dāng)前事物掛起∮欠纾可以用于發(fā)送提示信息贞言,站內(nèi)信,郵件提示燈阀蒂。不屬于并且不應(yīng)該影響主體業(yè)務(wù)邏輯该窗,即時(shí)發(fā)送失敗也不應(yīng)該對主題業(yè)務(wù)邏輯回滾弟蚀。
- PROPAGATION_REQUIRED_NEW
新建一個事物,如果存在當(dāng)前事物酗失,則將事物掛起义钉。總是新啟一個事物规肴,這個傳播機(jī)制適用于不受父類方法事物影響的操作捶闸,比如某些業(yè)務(wù)場景下需要記錄業(yè)務(wù)日志,用于異步反查拖刃,那么不管主體業(yè)務(wù)邏輯是否完成删壮,日志都需要記錄下來,不能因?yàn)橹黧w業(yè)務(wù)邏輯報(bào)錯而丟失日志兑牡。
測試:
表結(jié)構(gòu) | 服務(wù)類 | 功能描述 |
---|---|---|
user | userService | 創(chuàng)建用戶央碟,并添加賬號 |
account | accountService | 添加賬號 |
相關(guān)代碼:
package com.demo.spring;
import com.demo.spring.service.UserService;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* com.demo.spring
*
* @author Zyy
* @date 2019/2/14 22:15
*/
public class UserServiceTest {
public static void main(String[] args) {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("spring-tx.xml");
UserService userService = context.getBean(UserService.class);
userService.addUser("ayang");
}
}
@Transactional(propagation = Propagation.REQUIRED)
public void addUser(String name) {
jdbcTemplate.update("insert into user (name) values(?)", name);
accountService.addAccount(name, 100);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addAccount(String name, int money) {
String accountid = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());
jdbcTemplate.update("insert into account (accountname,user,money) values (?,?,?)", accountid, name, money);
int i = 1/0;
}
場景:
場景 | createUser | addAccount(Exception) | 結(jié)果 |
---|---|---|---|
1 | 無事物 | required | user成功,account失敗 |
2 | required | 無事物 | user失敗均函,account失敗 |
3 | required | not_supported | user失敗亿虽,account成功 |
4 | required | required_new | user失敗,account失敗 |
5 | required(異常移至create末尾) | required_new | user失敗苞也,account成功 |
6 | required(異常移至create末尾)洛勉,add方法至當(dāng)前類 | required_new | user失敗,account失敗 |
場景5和6為何會出現(xiàn)不一致如迟,required_new沒有起到作用收毫?
spring 聲明示事物使用動態(tài)代理實(shí)現(xiàn)的。
代理模擬:
TransactionalTest.java
com.demo.spring.TransactionalTest
package com.demo.spring;
import com.demo.spring.service.UserService;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* com.demo.spring
*
* @author Zyy
* @date 2019/2/18 23:05
*/
public class TransactionalTest {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-tx.xml");
final UserService userService = context.getBean(UserService.class);
UserService proxyUserService = (UserService) Proxy.newProxyInstance(
TransactionalTest.class.getClassLoader(), new Class[]{UserService.class}, new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
System.out.println("開啟事物:" + method.getName());
return method.invoke(userService,args);
} finally {
System.out.println("關(guān)閉事物:" + method.getName());
}
}
});
proxyUserService.addUser("ayang");
}
}
UserServiceImpl.java
package com.demo.spring.service;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* com.demo.spring.service
*
* @author Zyy
* @date 2019/2/14 21:53
*/
@Service
public class UserServiceImpl implements UserService{
@Resource
private AccountService accountService;
@Resource
private JdbcTemplate jdbcTemplate;
@Transactional(propagation = Propagation.REQUIRED)
public void addUser(String name) {
jdbcTemplate.update("insert into user (name) values(?)", name);
this.addAccount(name, 100);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addAccount(String name, int money) {
String accountid = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());
jdbcTemplate.update("insert into account (accountname,user,money) values (?,?,?)", accountid, name, money);
//int i = 1/0;
}
}
結(jié)果:
開啟事物:addUser
關(guān)閉事物 : addUser
當(dāng)我們調(diào)用addUser方法時(shí)殷勘,僅打印addUser的事物開啟和關(guān)閉牛哺,并沒打印addAccount的事物開啟和關(guān)閉,所以addAccount的事物的失效的劳吠。
原因在于spring 聲明示事物使用動態(tài)代理實(shí)現(xiàn)引润,而當(dāng)調(diào)用同一個類的方法時(shí),是不會走代理邏輯的痒玩,自然事物的配置也會失效淳附。
如果遇到這種情況如何處理?
在配置文件中添加:
<!-- 配置暴露proxy -->
<aop:aspectj-autoproxy expose-proxy="true"/>
在spring xml中配置 暴露proxy 對象蠢古,然后在代碼中用AopContext.currentProxy() 就可以獲當(dāng)前代理對象奴曙,然后基于代理對象調(diào)用創(chuàng)建帳戶
((UserSerivce) AopContext.currentProxy()).addAccount(name, 10000);
當(dāng)復(fù)現(xiàn)場景6,發(fā)現(xiàn)事物的配置又生效了草讶,與場景5結(jié)果一致洽糟,addUser失敗,addAcount成功。
github : https://github.com/zhaoyybalabala/spring-test
歡迎留言交流:)