事务在实际开发中,重要性不言而喻。假设没有合理的事务控制,A向B发起了100元转账,A账户减100,B账户加100,但是转账中途因网络等因素导致程序异常(B账户更新记录没有成功,A账户更新成功),这就导致A账户无缘无故损失100元。。。这就是事务的一个简单例子,何时提交事务、何时事务回滚、合理设置事务的超时时间也是程序设计非常重要的一部分。
事务:数据库事务是指一组操作逻辑单元(不可分割的整体,这些操作要么一起成功,要么一起失败),使数据从一种状态变换到另一种状态。
在我们日常工作中,涉及到事务的场景非常多,一个service中往往需要调用不同的dao层的方法,为了确保数据库中的数据的一致性,这些方法要么同时成功要么同时失败。因此在service层中我们一定要确保这一点。
事务的四大属性(ACID):
原子性(Atomicity) 原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
一致性(Consistency) 事务必须使数据库从一个一致性状态变换到另外一个一致性状态。
隔离性(Isolation) 事务的隔离性是指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,也就是设置不同个隔离级别包括未提交读(Read Uncommitted)、提交读(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。
持久性(Durability) 持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响。

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;
}
}
转账案例中我们发现可以手动控制事务的提交、是否自动提交、事务回滚等为我们开发带来了诸多便利,可灵活处理事务操作。然而Spring的事务管理模块就是做这些事情(只是设计的更加高级),一切事务都由Spring来管理,因此想要了解Spring中的事务就得从它设计的类或接口开始,分别是PlatformTransactionManager、TransactionDefinition、TransactionStatus。
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;
// 如果存在当前事务,则在嵌套事务中执行,否则其行为将与所需的行为类似。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();
在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、编写测试代码进行测试
/**
* @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 transactionTest() {
// 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");
// 3.提交事务
transactionManager.commit(transaction);
System.out.println("update success");
} catch (Exception e) {
// 4.出现异常则回滚事务
transactionManager.rollback(transaction);
e.printStackTrace();
}
}
}