Spring事务
事务在实际开发中,重要性不言而喻。假设没有合理的事务控制,A向B发起了100元转账,A账户减100,B账户加100,但是转账中途因网络等因素导致程序异常(B账户更新记录没有成功,A账户更新成功),这就导致A账户无缘无故损失100元。。。这就是事务的一个简单例子,何时提交事务、何时事务回滚、合理设置事务的超时时间也是程序设计非常重要的一部分。
1、事务相关概念回顾
1.1、什么是事务
事务:数据库事务是指一组操作逻辑单元(不可分割的整体,这些操作要么一起成功,要么一起失败),使数据从一种状态变换到另一种状态。
在我们日常工作中,涉及到事务的场景非常多,一个service
中往往需要调用不同的dao
层的方法,为了确保数据库中的数据的一致性,这些方法要么同时成功要么同时失败。因此在service
层中我们一定要确保这一点。
事务的四大属性(ACID):
- 原子性(
Atomicity
) 原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。 - 一致性(
Consistency
) 事务必须使数据库从一个一致性状态变换到另外一个一致性状态。 - 隔离性(
Isolation
) 事务的隔离性是指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,也就是设置不同个隔离级别包括未提交读(Read Uncommitted)、提交读(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。 - 持久性(
Durability
) 持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响。
1.2、事务转账案例实现(原生JDBC)
AA给BB转账100元,以MySQL
数据库为例
/*
事务:一组逻辑操作单元,是数据从一种状态变换到另一种状态
> 一组逻辑操作单元:一个或多个DML操作
数据一旦提交,就不可回滚
那些操作会导致数据自动提交?
> DDL一旦执行,都会自动提交 set autocommit = false 对DDL操作失效
> DML默认情况下,一旦执行,就会自动提交,但是我们通过 set autocommit = false 的方式取消DML操作的自动提交
> 默认在关闭连接时,会自动的提交数据
*/
public class TransactionTest {
@Test // 未考虑数据库事务的情况的转账操作
public void transferTest1(){
/*
针对user_table来说:
AA用于给BB用户转账100
update user_table set balance = balance - 100 where user = 'AA';
update user_table set balance = balance + 100 where user = 'BB';
*/
String sql1 = "update user_table set balance = balance - 100 where user = ?;";
update1_0(sql1,"AA");
// 模拟网络异常
System.out.println(10/0);
String sql2 = "update user_table set balance = balance + 100 where user = ?;";
update1_0(sql2,"BB");
System.out.println("转账成功!");
}
// 通用的[增删改]操作 --version 1.0(未考虑事务)
public int update1_0(String sql,Object ...args) { // 注意:这里要求SQL中的占位符(?)个数要与参数一致
Connection connection = null;
PreparedStatement preparedStatement = null;
try {
// 1. 获取数据库连接
connection = JDBCUtils.getConnection();
// 2. 预编译SQL语句,返回PreparedStatement的实例
preparedStatement = connection.prepareStatement(sql);
// 3. 填充占位符
for (int i = 0; i < args.length; i++) {
preparedStatement.setObject(i + 1, args[i]);
}
// 4. 执行
// 方式2
// preparedStatement.execute();
/*
preparedStatement.execute() 方法:
如果执行的是查询操作,有返回结果,则此方法返回true
如果执行的增、删、改操作,没有返回结果,则此方法返回false
*/
// 方式1
return preparedStatement.executeUpdate();
} catch(Exception e){
e.printStackTrace();
} finally {
// 5. 关闭连接和PreparedStatement
JDBCUtils.closeResource(connection,preparedStatement);
}
return 0;
}
@Test // 考虑数据库事务的转账操作
public void transactionTest2() {
Connection connection = null;
try {
connection = JDBCUtils.getConnection();
// 1.取消数据的自动提交
connection.setAutoCommit(false);
String sql1 = "update user_table set balance = balance - 100 where user = ?;";
update2_0(connection,sql1,"AA");
String sql2 = "update user_table set balance = balance + 100 where user = ?;";
update2_0(connection, sql2,"BB");
System.out.println("转账成功!");
// 2.提交数据
connection.commit();
} catch (Exception e) {
e.printStackTrace();
// 3.回滚数据
try {
connection.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}finally{
if(connection != null){
JDBCUtils.closeResource(connection,null);
}
}
}
// 通用的[增删改]操作 --version 2.0(考虑事务)
/*
2.0 版本说明:考虑事务就是共用一个数据库连接,因为数据库连接每次断开连接时,都会自动提交数据,且1.0版本
每次执行完一条SQL语句,就会自动断开连接,并且提交数据,然后继续执行下一条SQL语句
*/
public int update2_0(Connection connection,String sql,Object ...args) { // 注意:这里要求SQL中的占位符(?)个数要与参数一致
PreparedStatement preparedStatement = null;
try {
// 1. 预编译SQL语句,返回PreparedStatement的实例
preparedStatement = connection.prepareStatement(sql);
// 2. 填充占位符
for (int i = 0; i < args.length; i++) {
preparedStatement.setObject(i + 1, args[i]);
}
// 3. 执行
// 方式2
// preparedStatement.execute();
/*
preparedStatement.execute() 方法:
如果执行的是查询操作,有返回结果,则此方法返回true
如果执行的增、删、改操作,没有返回结果,则此方法返回false
*/
// 方式1
return preparedStatement.executeUpdate();
} catch(Exception e){
e.printStackTrace();
} finally {
// 4. 关闭连接和PreparedStatement
JDBCUtils.closeResource(null,preparedStatement);
}
return 0;
}
}
2、Spring中的事务
转账案例中我们发现可以手动控制事务的提交、是否自动提交、事务回滚等为我们开发带来了诸多便利,可灵活处理事务操作。然而Spring
的事务管理模块就是做这些事情(只是设计的更加高级),一切事务都由Spring
来管理,因此想要了解Spring
中的事务就得从它设计的类或接口开始,分别是PlatformTransactionManager
、TransactionDefinition
、TransactionStatus
。
2.1、三大基础组件
1、PlatformTransactionManager
:事务处理的核心接口,定义了事务处理的三个最基本方法。规范事务处理的行为,具体实现细节都由各种子类实现,例如JDBC的DataSourceTransactionManager
,hibernate的HibernateTransactionManager
,jpa的JpaTransactionManager
等等
// 获取
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
// 提交
void commit(TransactionStatus status) throws TransactionException;
// 回滚
void rollback(TransactionStatus status) throws TransactionException;
2、TransactionDefinition
:也是一个接口,规范了事务的一些属性,例如事务的隔离级别(Isolation)、事务的传播行为(Propagation Behavior)、事务的超时时间(Timeout)、是否为只读事务(Readonly)。
// 支持当前事务,如果事务不存在,创建一个新的事务
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;
// 如果存在当前事务,则在嵌套事务中执行,否则其行为与PROPAGATION_REQUIRED类似。EJB中没有类似的特性。
int PROPAGATION_NESTED = 6;
// 使用数据库默认的隔离级别
int ISOLATION_DEFAULT = -1;
// 表示可能发生脏读、不可重复读和幻象读。
// 此级别允许一个事务更改的行在提交该行的任何更改之前被另一个事务读取(“脏读”)。如果回滚了任何更改,则第二个事务将检索到无效行。
int ISOLATION_READ_UNCOMMITTED = 1;
// 表示防止脏读;
// 可能发生不可重复读取和幻象读取。此级别仅禁止事务读取包含未提交更改的行。
int ISOLATION_READ_COMMITTED = 2;
// 指示阻止脏读和不可重复读;可能发生幻象读取。
// 此级别禁止事务读取包含未提交更改的行,还禁止一个事务读取行,第二个事务更改行,第一个事务重新读取行,第二次获得不同的值(“不可重复读取”)。
int ISOLATION_REPEATABLE_READ = 4;
// 表示阻止脏读、不可重复读和幻象读。
// 此级别包括隔离可重复读取的禁止,并进一步禁止以下情况:一个事务读取满足where条件的所有行,第二个事务插入满足where条件的行,第一个事务重新读取相同条件的行,检索第二次读取中的附加“幻影”行。
int ISOLATION_SERIALIZABLE = 8;
// 使用数据库默认的事务超时时间,或者没有超时时间
int TIMEOUT_DEFAULT = -1;
3、TransactionStatus
:也是一个接口,规范一个事务的状态(也可以说是事务本身)。接口提供了控制事务执行和查询事务状态的方法。比如当前调用栈中是否已经存在了一个事务,就是通过该接口来判断的。该接口还可以管理和控制事务的执行,比如检查事务是否为一个新事务,或者是否只读,初始化回滚操作等。
// 判断事务是否存在
boolean hasSavepoint();
// 将操作刷新至数据库
void flush();
2.2、Spring中的编程式事务
在Spring
中,事务管理提供了两种实现方式,分别是编程式事务和声明式事务。
编程式事务言外之意就是在业务功能代码中嵌入事务管理的代码,手动控制事务的各种操作,属于侵入性事务管理。在Spring
中为了支持和简化编程式事务,专门提供了一个类TransactionTemplate
,在它的execute()
方法中就能实现事务的功能。
优点:
1、可以有效避免由于SpringAOP
的问题导致事务失效问题
2、能够更小粒度控制事务的范围,更直观更灵活
缺点:
1、每次都要单独实现,业务量大且功能复杂时,开发繁琐维护成本高
2、与业务代码耦合度高
说了这么多,如何使用编程式事务呢?
1、使用maven
搭建一个Spring
工程(略)
2、编写spring-dao.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!--
1.加载数据库配置信息(引入外部属性文件)
2.database.properties 文件中定义了数据库的连接信息的例如 spring.datasource.driver-class-name=com.mysql.jdbc.Driver
-->
<context:property-placeholder location="classpath:properties/database.properties"/>
<!-- 配置数据源(这里使用jdbc默认的数据源也可以使用druid等但是要引入相关依赖) -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${spring.datasource.driver-class-name}"/>
<property name="url" value="${spring.datasource.url}"/>
<property name="username" value="${spring.datasource.username}"/>
<property name="password" value="${spring.datasource.password}"/>
</bean>
<!-- 提供一个事务管理器(要想让spring管理事务,无论是是编程式事务管理还是声明式事务管理都要给spring容器提供一个事务管理器) -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 为了简化编程式事务操作,可以用JdbcTemplate封装好方法实现,当然也可以使用DataSourceTransactionManager来实现事务的一些操作 -->
<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="transactionManager"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
3、编写测试代码进行测试
第一种使用PlatformTransactionManager
的实现类实现编程式事务:
/**
* @description:
* @author: laizhenghua
* @date: 2022/5/28 11:32
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(value = {"classpath:applicationContext.xml"})
public class Test {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private PlatformTransactionManager transactionManager;
@Autowired
private TransactionTemplate transactionTemplate;
@org.junit.Test
public void transactionManagerTest() {
// 1.定义默认的事务属性
TransactionDefinition definition = new DefaultTransactionDefinition();
// 2.获取事务
TransactionStatus transaction = transactionManager.getTransaction(definition);
try {
int affectedRows = jdbcTemplate.update("UPDATE `TEST`.`TB_USER` U SET U.MONEY = U.MONEY - 100 WHERE U.USERNAME = ?", "AA");
// int i = 1 / 0; 模拟异常
// 3.提交事务
transactionManager.commit(transaction);
System.out.println("update success");
} catch (Exception e) {
// 4.出现异常则回滚事务
transactionManager.rollback(transaction);
e.printStackTrace();
}
}
}
第二种使用TransactionTemplate
实现编程式事务:
@org.junit.Test
public void transactionTemplateTest() {
// 1.有返回值
Object result = transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
int affectedRows = jdbcTemplate.update("UPDATE `TEST`.`TB_USER` U SET U.MONEY = U.MONEY - 100 WHERE U.USERNAME = ?", "AA");
} catch (Exception e) {
status.setRollbackOnly();
e.printStackTrace();
}
}
});
System.out.println(result);
// 2.无返回值
transactionTemplate.executeWithoutResult(new Consumer<TransactionStatus>() {
@Override
public void accept(TransactionStatus transactionStatus) {
try {
int affectedRows = jdbcTemplate.update("UPDATE `TEST`.`TB_USER` U SET U.MONEY = U.MONEY - 100 WHERE U.USERNAME = ?", "AA");
int i = 1 / 0; // 模拟异常
} catch (Exception e) {
transactionStatus.setRollbackOnly();
e.printStackTrace();
}
}
});
}
可以看到编程式事务代码入侵性太强了,与业务功能代码耦合度非常高,实际开发中使用较少。Spring
也一直倡导非侵入性的开发方式
2.3、Spring中的声明式事务
声明式事务基于AOP
实现,笔者也非常喜欢AOP
编程。对目标方法执行前执行后进行拦截处理事务的逻辑,即在目标方法执行前创建或者加入一个事务,在目标方法执行之后根据执行情况提交或者回滚事务。
从业务功能角度来说,事务处理属于附属业务,不应该掺杂在业务功能代码中。像这种附属业务应该抽离出来让切面去实现,来降低程序之间的耦合度。因此事务管理就是一个非常典型的横切逻辑,正是AOP的用武之地,Spring
团队也注意到了这一点,为声明式事务提供了简单而强大的支持,可以通过XML
配置文件或@Transaction
注解告诉Spring
帮忙管理事务,然后开发者只需专注业务代码的编写,无需关心事务的相关处理。
优点:
1、简单易维护,节省了很多代码让开发者从事务管理中解放出来
2、对业务代码没有侵入性,将事务管理与业务功能代码分离,降低程序之间的耦合度,开发者只需关注业务功能代码的编写
缺点:
1、最小粒度只能作用于方法上,如果想要给一部分代码块增加事务的话,只能把这部分代码块独立出来作为一个方法。
2、存在事务失效的风险,例如
/*
1. @Transactional 应⽤在⾮ public 修饰的⽅法上
2. @Transactional 注解属性 propagation 设置错误
3. @Transactional 注解属性 rollbackFor 设置错误
4. 同⼀个类中⽅法调⽤,导致@Transactional失效
5. 异常被catch捕获导致@Transactional失效
6. 数据库引擎不⽀持事务
这几种场景都有可能导致事务失效,而编程式事务很多都是可以避免的,因为编程式事务什么地方开启事务、什么地方提交、什么地方回滚比较清晰。
*/
3、数据一致性问题,例如一个方法中包含RPC远程调用、消息发送、缓存更新、文件写入等操作。这些操作自身是无法回滚事务的,这就会导致数据不一致问题。如RPC调用成功了但是本地事务回滚了,可是RPC调用却没有回滚。
4、事务时长问题,例如一个事务方法中有远程调用,就会拉长整个事务。就会导致本事务的数据库连接一直被占用!如果类似操作很多,有可能会耗尽数据库连接池。
总结下来就是声明式事务对开发者要求也很高,不能过度使用声明式事务,应当合理选择使用那种事务管理。
那么声明式事务管理在实际开发中又如何使用呢?
2.3.1、使用XML配置文件实现
1、事务管理器配置(想要Spring
管理事务都离不开事务管理器 )
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
2、配置事务通知
<!-- 配置事务通知 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 配置事务属性(name支持通配符配置),可以配置propagation/isolation/timeout/read-only/rollback-for等属性-->
<tx:method name="save*"/>
<tx:method name="add*"/>
<tx:method name="delete*"/>
<tx:method name="update*"/>
</tx:attributes>
</tx:advice>
3、配置AOP(切入点)
注意切入点配置属于AOP知识
<!--事务切入点配置 -->
<aop:config>
<aop:pointcut id="updateMoney" expression="execution(* com.laizhenghua.ehcache.service.impl.UserServiceImpl.updateMoney(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="updateMoney"/>
</aop:config>
完整spring-dao.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.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--
1.加载数据库配置信息(引入外部属性文件)
2.database.properties 文件中定义了数据库的连接信息的例如 spring.datasource.driver-class-name=com.mysql.jdbc.Driver
-->
<context:property-placeholder location="classpath:properties/database.properties"/>
<!-- 配置数据源(这里使用jdbc默认的数据源也可以使用druid等但是要引入相关依赖) -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${spring.datasource.driver-class-name}"/>
<property name="url" value="${spring.datasource.url}"/>
<property name="username" value="${spring.datasource.username}"/>
<property name="password" value="${spring.datasource.password}"/>
</bean>
<!-- 提供一个事务管理器(要想让spring管理事务,无论是是编程式事务管理还是声明式事务管理都要给spring容器提供一个事务管理器) -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置事务通知 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 配置事务属性(name支持通配符配置),可以配置propagation/isolation/timeout/read-only/rollback-for等属性-->
<tx:method name="save*"/>
<tx:method name="add*"/>
<tx:method name="delete*"/>
<tx:method name="update*"/>
</tx:attributes>
</tx:advice>
<!--事务切入点配置 -->
<aop:config>
<aop:pointcut id="updateMoney" expression="execution(* com.laizhenghua.ehcache.service.impl.UserServiceImpl.updateMoney(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="updateMoney"/>
</aop:config>
</beans>
4、编写测试代码进行测试
模拟业务代码
/**
* @description:
* @author: laizhenghua
* @date: 2022/5/28 14:41
*/
@Service(value = "userService")
public class UserServiceImpl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public Integer updateMoney(String name, Integer money) {
int affectedRows = jdbcTemplate.update("UPDATE `TEST`.`TB_USER` U SET U.MONEY = U.MONEY - ? WHERE U.USERNAME = ?", money, name);
int i = 1 / 0; // 模拟异常
System.out.println(affectedRows);
return affectedRows;
}
}
模拟执行
/**
* @description:
* @author: laizhenghua
* @date: 2022/5/28 11:32
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(value = {"classpath:applicationContext.xml"})
public class Test {
@Autowired
private UserService userService;
@org.junit.Test
public void transactionTest() {
// 执行业务代码
Integer affectedRows = userService.updateMoney("AA", 100); // 测试此方法声明式事务有没有生效
System.out.println(affectedRows);
}
}
执行前AA
的money是200,我们看Spring
有没有帮我们自动管理事务。
执行后会抛出java.lang.ArithmeticException: / by zero
异常,但是money还是200,说明我们配置的声明式事务已经生效了,可自行体验一下。
2.3.2、使用配置类实现
上面方式我们都是基于XML
配置文件实现,包括数据库、事务管理器、事务通知等。如果你觉得编写这些配置文件比较繁琐,还可以完全通过配置类实现,例如:
/**
* @description:
* @author: laizhenghua
* @date: 2022/7/2 10:32
*/
@Configuration
@EnableTransactionManagement // 开启事务注解支持
@ComponentScan(basePackages = "com.laizhenghua.ehcache")
@PropertySource(value = "classpath:properties/database.properties") // 引入SQL连接信息配置文件
public class DaoConfig {
@Autowired
private ApplicationContext applicationContext;
@Bean("dataSource")
public DataSource getDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
Environment environment = applicationContext.getEnvironment();
dataSource.setDriverClassName(Objects.requireNonNull(environment.getProperty("spring.datasource.driver-class-name")));
dataSource.setUrl(environment.getProperty("spring.datasource.url"));
dataSource.setUsername(environment.getProperty("spring.datasource.username"));
dataSource.setPassword(environment.getProperty("spring.datasource.password"));
return dataSource;
}
@Bean("transactionManager")
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(getDataSource());
}
@Bean("jdbcTemplate")
public JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(getDataSource());
}
}
但是一般实际开发中我们不这样整。使用配置类实现声明式事务管理,就是使用注解@Transactional
实现,因此我们只需使用Java配置类配置事务管理器即可,例如
1、spring-dao.xml
去掉事务相关配置,只留数据库连接配置和jdbcTemplate
<?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.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--
1.加载数据库配置信息(引入外部属性文件)
2.database.properties 文件中定义了数据库的连接信息的例如 spring.datasource.driver-class-name=com.mysql.jdbc.Driver
-->
<context:property-placeholder location="classpath:properties/database.properties"/>
<!-- 配置数据源(这里使用jdbc默认的数据源也可以使用druid等但是要引入相关依赖) -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${spring.datasource.driver-class-name}"/>
<property name="url" value="${spring.datasource.url}"/>
<property name="username" value="${spring.datasource.username}"/>
<property name="password" value="${spring.datasource.password}"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
2、使用Java
类配置事务管理器
/**
* @description:
* @author: laizhenghua
* @date: 2022/7/2 11:24
*/
@Configuration
@EnableTransactionManagement // 开启事务注解支持
public class TransactionManagerConfiguration {
@Autowired
private DataSource dataSource;
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource);
}
}
3、编程测试代码,加上@Transactional
注解
/**
* @description:
* @author: laizhenghua
* @date: 2022/5/14 11:50
*/
@Service(value = "testService ")
public class TestServiceImpl implements TestService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
@Transactional
public Integer test(String name, Integer money) {
int affectedRows = jdbcTemplate.update("UPDATE `TEST`.`TB_USER` U SET U.MONEY = U.MONEY - ? WHERE U.USERNAME = ?", money, name);
int i = 1 / 0; // 模拟异常,测试事务回滚
return affectedRows;
}
}
4、执行测试代码,观察数据是否保持一致性即事务是否回滚。
/**
* @description:
* @author: laizhenghua
* @date: 2022/7/2 11:03
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(value = {"classpath:applicationContext.xml"})
public class TransactionTest {
@Autowired
private TestService testService;
@Test
public void transactionalTest() {
Integer affectedRows = testService.test("AA", 100);
System.out.println(affectedRows);
}
}
OK,测试没有问题。test()
抛出异常后,事务回滚了。数据一致性得以保持,比起XML
配置这种方式更加简洁,只需要一个注解就能轻松管理事务。
2.3.3、配置类+XML混合配置实现
Spring
也支持Java配置类和XML同时配置事务相关的内容。这种配置方式称为混合配置(其实就是注解+XML配置)。这种混合使用的好处就是事务管理器通过XML
配置,而事务控制则是使用注解开启,不同的方法可以设置不同的事务属性,灵活控制事务属性(超时、隔离级别、回滚异常),使代码看起来更加直观、简洁。
例如spring-dao.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.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--
1.加载数据库配置信息(引入外部属性文件)
2.database.properties 文件中定义了数据库的连接信息的例如 spring.datasource.driver-class-name=com.mysql.jdbc.Driver
-->
<context:property-placeholder location="classpath:properties/database.properties"/>
<!-- 1.配置包扫描路径 -->
<context:component-scan base-package="com.laizhenghua.ehcache"/>
<!-- 2.开启事务注解支持-->
<tx:annotation-driven/>
<!-- 3.配置数据源(这里使用jdbc默认的数据源也可以使用druid等但是要引入相关依赖) -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${spring.datasource.driver-class-name}"/>
<property name="url" value="${spring.datasource.url}"/>
<property name="username" value="${spring.datasource.username}"/>
<property name="password" value="${spring.datasource.password}"/>
</bean>
<!-- 4.事务管理器配置 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 5.jdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
在方法中开启事务就使用注解@Transactional
开启,不同的方法根据自身业务需求,灵活指定事务的属性。例如:
1、想让方法以非事务方式执行。
@Override
@Transactional(propagation = Propagation.NOT_SUPPORTED) // 非事务方式执行
public Integer test(String name, Integer money) {
int affectedRows = jdbcTemplate.update("UPDATE `TEST`.`TB_USER` U SET U.MONEY = U.MONEY - ? WHERE U.USERNAME = ?", money, name);
// int i = 1 / 0; // 模拟异常,测试事务回滚
return affectedRows;
}
2、指定事务的异常回滚
@Override
@Transactional(rollbackFor = {ArithmeticException.class}) // 抛出异常类型为ArithmeticException则回滚
public Integer updateMoney(String name, Integer money) {
int affectedRows = jdbcTemplate.update("UPDATE `TEST`.`TB_USER` U SET U.MONEY = U.MONEY - ? WHERE U.USERNAME = ?", money, name);
int i = 1 / 0; // 模拟异常
System.out.println(affectedRows);
return affectedRows;
}
3、指定事务超时时间
@Override
@Transactional(timeout = 30) // 超过指定超时时间,自动回滚事务,单位是秒,默认超时时间是数据库默认的超时时间即-1
public Integer updateMoney(String name, Integer money) {
int affectedRows = jdbcTemplate.update("UPDATE `TEST`.`TB_USER` U SET U.MONEY = U.MONEY - ? WHERE U.USERNAME = ?", money, name);
int i = 1 / 0; // 模拟异常
System.out.println(affectedRows);
return affectedRows;
}
4、设置只读事务
@Override
@Transactional(readOnly = true) // 只读事务,主要用于一次执行多条查询语句,避免更改而导致数据不一致问题
public Integer updateMoney(String name, Integer money) {
int affectedRows = jdbcTemplate.update("UPDATE `TEST`.`TB_USER` U SET U.MONEY = U.MONEY - ? WHERE U.USERNAME = ?", money, name);
int i = 1 / 0; // 模拟异常
System.out.println(affectedRows);
return affectedRows;
}
5、等等,详见@Transactional
注解属性
2.4、事务的隔离级别
隔离级别:主要解决多个同时运行且访问数据库数据相同的事务带来的并发问题。在数据库中事务的隔离性是这样定义的:数据库系统必须具有隔离并发运行各个事务的能力,使他们不会互相影响,避免产生并发问题。
隔离级别也因此而产生,一个事务与其他事务的隔离程度称为隔离级别,数据库规定了多种事务隔离级别,如读未提交数据、读已提交、可重复读、串行化等。不同隔离级别对应不同的干扰程度即隔离级别越高、数据一致性就越好,相应的性能就越弱。
隔离级别 | 描述 |
| 允许事务读取未被其他事务提交的变更。这种隔离级别脏读、不可重复读和幻读的问题都会出现 |
| 只允许事务读取已经被其他事务提交的变更。可以避免脏读、但不可重复读和幻读问题任然可能会出现 |
| 确保事务可以多次从一个字段中读取相同的值,在这个事务期间,禁止其他事务对这个字段进行更新,可以避免脏读和不可重复读,但是幻读的问题依然存在 |
| 确保事务可以从一个表中读取相同的行,这个事务持续期间,禁止其他事务对该表执行插入、更新和删除操作,所有并发问题都可以避免但是性能十分低下 |
补充:
1、MySQL
支持4种事务的隔离级别且默认的隔离级别为READ COMMITED
(读已提交数据)。
2、ORACLE
支持2种隔离级别。分别是READ COMMITED
(读已提交数据)和SERIALIZABLE
(串行化),且默认的隔离级别为READ COMMITED
(读已提交数据)。
1、编程式事务设置隔离级别
@Test
public void transactionalTest() {
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
// 串行化事务设置
definition.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
TransactionStatus transaction = transactionManager.getTransaction(definition);
try {
// 业务代码...
transactionManager.commit(transaction); // 事务提交
} catch (Exception e) {
e.printStackTrace();
transactionManager.rollback(transaction); // 事务回滚
}
}
@Test
public void transactionTemplateTest() {
// 串行化事务设置
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
transactionTemplate.executeWithoutResult(transactionStatus -> {
try {
// 业务代码...
} catch (Exception e) {
e.printStackTrace();
transactionStatus.setRollbackOnly(); // 事务回滚
}
});
}
2、声明式事务设置隔离级别
// 注解
@Transactional(isolation = Isolation.SERIALIZABLE)
// XML配置文件
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 配置事务属性(name支持通配符配置),可以配置propagation/isolation/timeout/read-only/rollback-for等属性-->
<tx:method name="save*" isolation="SERIALIZABLE"/>
</tx:attributes>
</tx:advice>
后面设置事务属性包括事务超时时间、传播性等也是一样的。
2.5、事务的传播性
事务的传播性:又称事务的传播机制,主要描述当前service
调用另外一个service
事务的行为是什么样子的。也就是业务层事务方法互相调用时,被调用者的事务以何种状态存在。
1、int PROPAGATION_REQUIRED = 0;
事务的默认传播机制,使用当前事务,如果当前事务不存在则新建一个事务。
// testService下有个update方法
@Service(value = "testService")
public class TestServiceImpl implements TestService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
@Transactional(propagation = Propagation.REQUIRED)
public Integer update() {
int affectedRows = jdbcTemplate.update("UPDATE `TEST`.`TB_USER` U SET U.MONEY = U.MONEY - ? WHERE U.USERNAME = ?", 100, "AA");
return affectedRows;
}
}
// 其他service调用testService方法下update方法
@Service(value = "userService")
public class UserServiceImpl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private TestService testService;
@Override
@Transactional
public Integer updateMoney(String name, Integer money) {
int affectedRows = jdbcTemplate.update("UPDATE `TEST`.`TB_USER` U SET U.MONEY = U.MONEY - ? WHERE U.USERNAME = ?", money, name);
testService.update(); // 调用testService方法下update方法
return affectedRows;
}
}
// 当执行到 testService.update(); 这一行代码时会使用当前的事务。update内部不会自己开启事务,除非当前事务不存在
// 如何验证只开启了一个事务 lgging.level.root=debug 开启此配置项观察日志输出情况即可
2、int PROPAGATION_REQUIRES_NEW = 3;
无论当前事务存不存在都开启一个新的事务,如果当前事务存在,则把当前事务挂起。
// userService.update()
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void update() {
// BB加一百块钱
jdbcTemplate.update("UPDATE `TEST`.`TB_USER` U SET U.MONEY = U.MONEY + ? WHERE U.USERNAME = ?", 100, "BB");
}
// helloService.test()
@Override
@Transactional
public void test() {
// AA减一百块钱
jdbcTemplate.update("UPDATE `TEST`.`TB_USER` U SET U.MONEY = U.MONEY - ? WHERE U.USERNAME = ?", 100, "AA");
userService.update(); // 调用 userService.update()
int i = 1 / 0;
}
注意点:如果测试代码抛出如下异常
com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
// MySQL表锁和行锁问题
/*
MySQL InnoDB 存储引擎的特性。可用 SHOW VARIABLES LIKE '%STORAGE_ENGINE%'; 查看
因为我们没有给操作的字段添加索引,InnoDB并没有做到行锁,走的是表锁。当前事务挂起后其实将整个
表锁住了,嵌套执行的方法要单独开启事务,就会一直等待释放资源最后就抛出 Lock wait timeout 异常
解决办法:给USERNAME添加索引即可,例如
ALTER TABLE `TEST`.TB_USER ADD INDEX IDX1_USERNAME(USERNAME);
*/
然后我们执行helloService.test()
方法,观察日志输出情况,例如(截取了部分日志):
// 为当前方法开启新事务
[nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager : Creating new transaction with name [com.laizhenghua.cache.service.impl.HelloServiceImpl.test]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
// 挂起当前事务并为UserServiceImpl.update()方法开启新事务
[nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager : Suspending current transaction, creating new transaction with name [com.laizhenghua.cache.service.impl.UserServiceImpl.update]
[nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager : Acquired Connection [HikariProxyConnection@1323770487 wrapping com.mysql.jdbc.JDBC4Connection@219d09e9] for JDBC transaction
[nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager : Switching JDBC Connection [HikariProxyConnection@1323770487 wrapping com.mysql.jdbc.JDBC4Connection@219d09e9] to manual commit
[nio-8080-exec-1] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL update
总结:里边的事务不受外边影响,即使外边抛出异常里面也能正常提交。实际开发中最常用的也是上面这两个。其他传播行为这里就不一一举例子了。遇到了具体分析下选择合适的传播性即可。
3、int PROPAGATION_NESTED = 6;
如果当前事务存在,则创建一个事务作为当前事务的嵌套事务来运行。如果当前事务不存在则等价于PROPAGATION_REQUIRED
。如果当前事务回滚,嵌套事务(子事务)也会回滚,这就是与PROPAGATION_REQUIRES_NEW
的区别。
4、int PROPAGATION_SUPPORTS = 1;
如果当前事务存在,则加入当前事务。如果当前事务不存在,则以非事务的方式继续运行。
5、int PROPAGATION_NOT_SUPPORTED = 4;
以非事务方式运行,如果当前事务存在,则挂起。
6、int PROPAGATION_NEVER = 5;
以非事务方式运行,如果当前事务存在,则抛出异常。
7、int PROPAGATION_MANDATORY = 2;
支持当前事务,如果事务不存在则抛出异常。
2.6、事务回滚规则
1、并不是所有的异常都会导致事务回滚!!!例如IOException
就不会导致事务回滚。
2、运行时异常RunTimeException
及其子类会导致事务回滚。
事务回滚规则,牢记以上两点即可。当然我们也可以指定异常回滚,例如:
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = IOException.class)
public void update() {
jdbcTemplate.update("UPDATE `TEST`.`TB_USER` U SET U.MONEY = U.MONEY + ? WHERE U.USERNAME = ?", 100, "BB");
}
指定异常不会回滚:
@Override
@Transactional(noRollbackFor = {ArithmeticException.class})
public void test() {
jdbcTemplate.update("UPDATE `TEST`.`TB_USER` U SET U.MONEY = U.MONEY - ? WHERE U.USERNAME = ?", 100, "AA");
userService.update();
int i = 1 / 0;
}
2.7、事务超时时间
就是指定事务的回滚时间,从事务开启节点开始计算,超过指定时间则自动回滚,单位是秒。默认超时时间是数据库自己的超时时间。
@Override
@Transactional(timeout = 3000) // 30s自动回滚
public void test() {
// 业务代码
}
2.8、只读事务
1、只读事务一般用在有多条查询语句的方法上,也就是说方法上只有一个SELECT
语句就没有必要加上只读事务。
2、主要解决查询数据不一致现象,也就是读一致性。例如给方法上加上@Transactional
,多查询只读事务执行时间为a
到a+10
(a为事务开始时间,a+10为事务结束时间)在事务执行过程中,其他事务对数据的更改会被只读事务忽视。只读事务查询出来的数据任是a
时间的数据,这样就保证了数据整体一致性。
3、只读事务设置方式
@Override
@Transactional(readOnly = true)
public void query() {
// 查询1
// 查询2
// 查询3
}
2.9、事务注意事项
1、混合事务配置的时候,xml中aop配置高于注解配置。
2、事务只有是public
方法才生效
3、相同服务内,没有事务的方法调用有事务的方法会导致事务失效。(原理是AOP)
4、注解加到实现类中,不要加到接口上(接口上的事务,只有配置基于接口的代理才会生效,会出现意料外的问题)