什么是事务
数据库事务是指作为单个逻辑工作单元执行的一系列操作,这些操作要么一起成功,要么一起失败,是一个不可分割的工作单元。
举例:
张三要给李四转账 500 块钱,这里涉及到两个操作,从张三的账户上减去 500 块钱,给李四的账户添加 500 块钱,这两个操作要么同时成功要么同时失败,如何确保他们同时成功或者同时失败呢?答案就是事务。
1.1事务的四大特性(ACID)
原子性(Atomicity): 一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即,事务不可分割、不可约简。
一致性(Consistency): 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。
隔离性(Isolation): 数据库允许多个并发事务同时对其数据进行读写和修改,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括未提交读(Read Uncommitted)、提交读(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。
持久性(Durability): 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
Spring中的事务
2.1事务的用法
2.1.1 编程式事务
编程式事务的使用:
通过 PlatformTransactionManager 或者 TransactionTemplate 可以实现编程式事务。如果是在 Spring Boot 项目中,这两个对象 Spring Boot 会自动提供,我们直接使用即可。但是如果是在传统的 SSM 项目中,则需要我们通过配置来提供这两个对象,下面以SSM项目为例进行说明
PlatformTransactionManager实现编程式事务
在xml文件配置对象

代码如下:
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource" id="dataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///spring_tran?serverTimezone=Asia/Shanghai"/>
<property name="username" value="root"/>
<property name="password" value="123"/>
</bean>
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean class="org.springframework.transaction.support.TransactionTemplate" id="transactionTemplate">
<property name="transactionManager" ref="transactionManager"/>
</bean>
<bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
注入PlatformTransactionManager并使用

@Service
public class TransferService {
@Autowired
JdbcTemplate jdbcTemplate;
@Autowired
PlatformTransactionManager txManager;
public void transfer() {
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
TransactionStatus status = txManager.getTransaction(definition);
try {
jdbcTemplate.update("update user set account=account+100 where username='zhangsan'");
int i = 1 / 0;
jdbcTemplate.update("update user set account=account-100 where username='lisi'");
txManager.commit(status);
} catch (DataAccessException e) {
e.printStackTrace();
txManager.rollback(status);
}
}
}
在 try...catch... 中进行业务操作,没问题就 commit,有问题就 rollback。如果我们需要配置事务的隔离性、传播性等,可以在 DefaultTransactionDefinition 对象中进行配置。
使用transfer方法

TransactionTemplate实现编程式事务
TransactionTemplate 来实现编程式事务,如下:

@Service
public class TransferService {
@Autowired
JdbcTemplate jdbcTemplate;
@Autowired
TransactionTemplate tranTemplate;
public void transfer() {
tranTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
jdbcTemplate.update("update user set account=account+100 where username='zhangsan'");
int i = 1 / 0;
jdbcTemplate.update("update user set account=account-100 where username='lisi'");
} catch (DataAccessException e) {
status.setRollbackOnly();
e.printStackTrace();
}
}
});
}
}
直接注入 TransactionTemplate,然后在 execute 方法中添加回调写核心的业务即可,当抛出异常时,将当前事务标注为只能回滚即可。注意,execute 方法中,如果不需要获取事务执行的结果,则直接使用 TransactionCallbackWithoutResult 类即可,如果要获取事务执行结果,则使用 TransactionCallback 即可。
编程式事务由于代码入侵太严重了,因为在实际开发中使用的很少,我们在项目中更多的是使用声明式事务。
2.1.2 声明式事务
声明式事务如果使用 XML 配置,可以做到无侵入;如果使用 Java 配置,也只有一个 @Transactional 注解侵入而已,相对来说非常容易。
XML配置声明式事务
给如下方法使用XML的方式添加事务:

XML 配置声明式事务大致上可以分为三个步骤,如下:

配置事务管理器
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource" id="dataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///spring_tran?serverTimezone=Asia/Shanghai"/>
<property name="username" value="root"/>
<property name="password" value="123"/>
</bean>
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
配置事务通知
<tx:advice transaction-manager="transactionManager" id="txAdvice">
<tx:attributes>
<tx:method name="transfer*"/>
</tx:attributes>
</tx:advice>
配置 AOP
<aop:config>
<aop:pointcut id="pc1" expression="execution(* org.javaboy.demo.UserService.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pc1"/>
</aop:config>
第二步和第三步中定义出来的方法交集,就是我们要添加事务的方法。
配置完成后,org.javaboy.demo包下的UserService类中的transfer开头的方法就自动具备事务了,因此transfer3方法的事务就加上了:
Java配置声明式事务
使用 Java 配置来实现声明式事务:
创建Java的配置类


@Configuration
@ComponentScan
//开启事务注解支持
@EnableTransactionManagement
public class JavaConfig {
@Bean
DataSource dataSource() {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setPassword("123");
ds.setUsername("root");
ds.setUrl("jdbc:mysql:///test01?serverTimezone=Asia/Shanghai");
ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
return ds;
}
@Bean
JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
}
这里要配置的东西其实和 XML 中配置的都差不多,最最关键的就两个:
事务管理器 PlatformTransactionManager。
@EnableTransactionManagement 注解开启事务支持。
配置完成后,接下来,哪个方法需要事务就在哪个方法上添加 @Transactional 注解即可,向下面这样:

@Transactional()
public void transfer4() {
jdbcTemplate.update("update account set money = ? where username=?;", 998, "lisi");
int i = 1 / 0;
}
当然这个稍微有点代码入侵,不过问题不大,日常开发中这种方式使用较多。当@Transactional 注解加在类上面的时候,表示该类的所有方法都有事务,该注解加在方法上面的时候,表示该方法有事务。
使用transfer4方法

Java+XMl混合配置声明式事务
也可以 Java 代码和 XML 混合配置来实现声明式事务,就是一部分配置用 XML 来实现,一部分配置用 Java 代码来实现:
假设 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:tx="http://www.springframework.org/schema/tx"
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">
<!--
开启事务的注解配置,添加了这个配置,就可以直接在代码中通过 @Transactional 注解来开启事务了
-->
<tx:annotation-driven />
</beans>
那么 Java 代码中的配置如下:
@Configuration
@ComponentScan
@ImportResource(locations = "classpath:applicationContext3.xml")
public class JavaConfig {
@Bean
DataSource dataSource() {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setPassword("123");
ds.setUsername("root");
ds.setUrl("jdbc:mysql:///test01?serverTimezone=Asia/Shanghai");
ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
return ds;
}
@Bean
JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
}
Java 配置中通过 @ImportResource 注解导入了 XML 配置,XML 配置中的内容就是开启 @Transactional 注解的支持,所以 Java 配置中省略了 @EnableTransactionManagement 注解。
2.2 Spring中事务的三大基础设施
Spring 中对事务的支持提供了三大基础设施,我们先来了解下。
PlatformTransactionManager
TransactionDefinition
TransactionStatus
这三个核心类是 Spring 处理事务的核心类。
2.2.1 PlatformTransactionManager
PlatformTransactionManager 是事务处理的核心,它有诸多的实现类
PlatformTransactionManager 的定义如下:

PlatformTransactionManager 中主要有如下三个方法:
1.getTransaction()
getTransaction() 是根据传入的 TransactionDefinition 获取一个事务对象,TransactionDefinition 中定义了一些事务的基本规则,例如传播性、隔离级别等。
2.commit()
commit() 方法用来提交事务。
3.rollback()
rollback() 方法用来回滚事务。
可以看到 PlatformTransactionManager 中定义了基本的事务操作方法,这些事务操作方法都是平台无关的,具体的实现都是由不同的子类来实现的。
以 PlatformTransactionManager 为例,它有众多实现,如果你使用的是 JDBC 那么可以将 DataSourceTransactionManager 作为事务管理器;如果你使用的是 Hibernate,那么可以将 HibernateTransactionManager 作为事务管理器;如果你使用的是 JPA,那么可以将 JpaTransactionManager 作为事务管理器。DataSourceTransactionManager、HibernateTransactionManager 以及 JpaTransactionManager 都是 PlatformTransactionManager 的具体实现,但是我们并不需要掌握这些具体实现类的用法,我们只需要掌握好 PlatformTransactionManager 的用法即可。
2.2.2 TransactionDefinition
TransactionDefinition 用来描述事务的具体规则,也称作事务的属性。主要是五种属性:
隔离性
传播性
回滚规则
超时时间
是否只读
TransactionDefinition 类中一共有五个方法:
getIsolationLevel(),获取事务的隔离级别
getName(),获取事务的名称
getPropagationBehavior(),获取事务的传播性
getTimeout(),获取事务的超时时间
isReadOnly(),获取事务是否是只读事务
如果开发者使用了编程式事务的话,直接使用 DefaultTransactionDefinition 即可。
2.2.3 TransactionStatus
TransactionStatus 可以直接理解为事务本身
isNewTransaction() 方法获取当前事务是否是一个新事务。
hasSavepoint() 方法判断是否存在 savePoint()。
setRollbackOnly() 方法设置事务必须回滚。
isRollbackOnly() 方法获取事务只能回滚。
flush() 方法将底层会话中的修改刷新到数据库,一般用于 Hibernate/JPA 的会话,对如 JDBC 类型的事务无任何影响。
isCompleted() 方法用来获取是一个事务是否结束。
事务属性

3.1 隔离性
事务的隔离性,也就是事务的隔离级别。
MySQL 中有四种不同的隔离级别,这四种不同的隔离级别在 Spring 中都得到了很好的支持。Spring 中默认的事务隔离级别是 default,即数据库本身的隔离级别是啥就是啥,default 就能满足我们日常开发中的大部分场景。
不过如果项目有需要,我们也可以调整事务的隔离级别。
调整方式如下:
3.1.1 编程式事务隔离级别
如果是编程式事务,通过如下方式修改事务的隔离级别:
TransactionTemplate
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
TransactionDefinition 中定义了各种隔离级别。
PlatformTransactionManager
public void update2() {
//创建事务的默认配置
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
TransactionStatus status = platformTransactionManager.getTransaction(definition);
try {
jdbcTemplate.update("update account set money = ? where username=?;", 999, "zhangsan");
int i = 1 / 0;
//提交事务
platformTransactionManager.commit(status);
} catch (DataAccessException e) {
e.printStackTrace();
//回滚
platformTransactionManager.rollback(status);
}
}
这里是在 DefaultTransactionDefinition 对象中设置事务的隔离级别。
3.1.2 声明式事务隔离级别
如果是声明式事务通过如下方式修改隔离级别:
XML:
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!--以 add 开始的方法,添加事务-->
<tx:method name="add*"/>
<tx:method name="insert*" isolation="SERIALIZABLE"/>
</tx:attributes>
</tx:advice>
Java:
@Transactional(isolation = Isolation.SERIALIZABLE)
public void update4() {
jdbcTemplate.update("update account set money = ? where username=?;", 998, "lisi");
int i = 1 / 0;
}
3.2 传播性
事务的传播性:事务传播行为是为了解决业务层方法之间互相调用的事务问题,当一个事务方法被另一个事务方法调用时,事务该以何种状态存在?例如新方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行,等等,这些规则就涉及到事务的传播性。
传播性 | 描述 |
REQUIRED | 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务 |
SUPPORTS | 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行 |
MANDATORY | 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常 |
REQUIRES_NEW | 创建一个新的事务,如果当前存在事务,则把当前事务挂起 |
NOT_SUPPORTED | 以非事务方式运行,如果当前存在事务,则把当前事务挂起 |
NEVER | 以非事务方式运行,如果当前存在事务,则抛出异常 |
NESTED | 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED |
3.3 回滚规则
默认情况下,事务只有遇到运行期异常(RuntimeException 的子类)以及 Error 时才会回滚,在遇到检查型(Checked Exception)异常时不会回滚。
像 1/0,空指针这些是 RuntimeException,而 IOException 则算是 Checked Exception,换言之,默认情况下,如果发生 IOException 并不会导致事务回滚。
如果我们希望发生 IOException 时也能触发事务回滚,那么可以按照如下方式配置:
Java 配置:
@Transactional(rollbackFor = IOException.class)
public void handle2() {
jdbcTemplate.update("update user set money = ? where username=?;", 1, "zhangsan");
accountService.handle1();
}
XML 配置:
<tx:advice transaction-manager="transactionManager" id="txAdvice">
<tx:attributes>
<tx:method name="m3" rollback-for="java.io.IOException"/>
</tx:attributes>
</tx:advice>
另外,我们也可以指定在发生某些异常时不回滚,例如当系统抛出 ArithmeticException 异常并不要触发事务回滚,配置方式如下:
Java 配置:
@Transactional(noRollbackFor = ArithmeticException.class)
public void handle2() {
jdbcTemplate.update("update user set money = ? where username=?;", 1, "zhangsan");
accountService.handle1();
}
XML 配置:
<tx:advice transaction-manager="transactionManager" id="txAdvice">
<tx:attributes>
<tx:method name="m3" no-rollback-for="java.lang.ArithmeticException"/>
</tx:attributes>
</tx:advice>
3.4 是否只读
只读事务一般设置在查询方法上,但不是所有的查询方法都需要只读事务,要看具体情况。
一般来说,如果这个业务方法只有一个查询 SQL,那么就没必要添加事务,强行添加最终效果适得其反。
但是如果一个业务方法中有多个查询 SQL,情况就不一样了:多个查询 SQL,默认情况下,每个查询 SQL 都会开启一个独立的事务,这样,如果有并发操作修改了数据,那么多个查询 SQL 就会查到不一样的数据。此时,如果我们开启事务,并设置为只读事务,那么多个查询 SQL 将被置于同一个事务中,多条相同的 SQL 在该事务中执行将会获取到相同的查询结果。
设置事务只读的方式如下:
Java 配置:
@Transactional(readOnly = true)
XML 配置:
<tx:advice transaction-manager="transactionManager" id="txAdvice">
<tx:attributes>
<tx:method name="m3" read-only="true"/>
</tx:attributes>
</tx:advice>
3.5 超时时间
超时时间是说一个事务允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。
事务超时时间配置方式如下(单位为秒):
Java 配置:
@Transactional(timeout = 10)
XML 配置:
<tx:advice transaction-manager="transactionManager" id="txAdvice">
<tx:attributes>
<tx:method name="m3" read-only="true" timeout="10"/>
</tx:attributes>
</tx:advice>
在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒,默认值为-1。
4. 注意事项
事务只能应用到 public 方法上才会有效。
事务需要从外部调用,Spring 自调事务用会失效。即相同类里边,A 方法没有事务,B 方法有事务,A 方法调用 B 方法,则 B 方法的事务会失效,这点尤其要注意,因为代理模式只拦截通过代理传入的外部方法调用,所以自调用事务是不生效的。
建议事务注解 @Transactional 一般添加在实现类上,而不要定义在接口上,如果加在接口类或接口方法上时,只有配置基于接口的代理这个注解才会生效。