Appearance
Spring教程 - 11 注解方式实现声明式事务
事务是一组数据库操作的集合,这些操作要么 全部成功,要么 全部失败。
例如张三给李四转账100,要张三的账户减去100,李四的账户加上100,这是一组操作,必须全部成功,不能张三账户减去100成功,李四账户加上100失败,这会导致数据错误。
在前面介绍 JdbcTemplate 的时候,我们是没有处理事务的,默认情况下每条 SQL 都是一个单独的事务(即自动提交模式,autoCommit=true)。你执行一条更新的 SQL,它会立即提交到数据库。
关于事务的知识,可以查看 SQL 教程 - 事务 。
下面就来模拟一下张三给李四转账100的操作。
11.1 不处理事务
我们还是按照 Controller --> Service --> Dao 的方式,分层调用来实现。

1 实现Dao
接口:IUserDao.java
添加两个方法,一个是查询用户,一个是修改用户的余额。
java
package com.foooor.hellospring.dao;
import com.foooor.hellospring.pojo.User;
public interface IUserDao {
/**
* 根据id查询用户
*/
User selectById(int userId);
/**
* 修改用户余额
*/
void updateBalance(int userId, int balance);
}实现类:UserDaoImpl.java
记得添加注解。
java
package com.foooor.hellospring.dao.impl;
import com.foooor.hellospring.dao.IUserDao;
import com.foooor.hellospring.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class UserDaoImpl implements IUserDao {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 根据id获取用户
*/
@Override
public User selectById(int userId) {
// 1.准备sql
String sql = "select balance from tb_user where id = ?";
// 2.执行查询
List<User> userList = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class), userId);
// 3.返回结果
return userList.isEmpty() ? null : userList.get(0);
}
/**
* 修改用户余额
*/
public void updateBalance(int userId, int balance) {
// 1.准备sql
String sql = "update tb_user set balance = balance + ? where id = ?";
// 2.执行更新
jdbcTemplate.update(sql, balance, userId);
}
}- 上面添加了两个方法,一个是查询用户,这里我为了不让查询不到的时候报错,所以使用了列表的方式查询。
2 实现Service
接口:IUserService.java
我们是在 Service 中实现业务逻辑,所以也就在这一层实现转账操作。
java
package com.foooor.hellospring.service;
public interface IUserService {
/**
* 转账
*/
void transfer(int fromUserId, int toUserId, int amount);
}实现类:UserServiceImpl.java
记得添加注解。
java
package com.foooor.hellospring.service.impl;
import com.foooor.hellospring.dao.IUserDao;
import com.foooor.hellospring.pojo.User;
import com.foooor.hellospring.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements IUserService {
@Autowired
private IUserDao userDao;
/**
* 转账
*/
@Override
public void transfer(int fromUserId, int toUserId, int amount) {
// 1.转账前的检查
if (amount <= 0) {
throw new IllegalArgumentException("转账金额必须大于0");
}
if (fromUserId == toUserId) {
throw new IllegalArgumentException("转账方和收款方不能相同");
}
User fromUser = userDao.selectById(fromUserId);
if (fromUser == null) {
throw new IllegalArgumentException("转账方不存在");
}
if (userDao.selectById(toUserId) == null) {
throw new IllegalArgumentException("收款方不存在");
}
// 2.检查转账方是否有足够的余额
if (fromUser.getBalance() < amount) {
throw new IllegalArgumentException("转账方余额不足");
}
// 3.更新转账方和收款方的余额
userDao.updateBalance(fromUserId, -amount); // 转账人余额减少
userDao.updateBalance(toUserId, amount); // 收款人余额增加
}
}- 在转账前先做一些检查,判断用户是否存在等;
- 然后将转账人的余额减去指定的金额,将收款人加上指定的金额;
3 实现Controller
UserController.java
记得添加注解。
java
package com.foooor.hellospring.controller;
import com.foooor.hellospring.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
@Autowired
private IUserService userService;
/**
* 转账
*/
public void transfer(int fromUserId, int toUserId, int amount) {
userService.transfer(fromUserId, toUserId, amount);
}
}- Controller 是接口层,是提供给前端调用的,所以调用 Service 就可以了。
4 测试
编写测试类,注入 Controller,然后进行测试即可:
java
package com.foooor.hellospring;
import com.foooor.hellospring.config.SpringConfig;
import com.foooor.hellospring.controller.UserController;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@SpringJUnitConfig(classes = {SpringConfig.class}) // 指定 Spring 配置文件
public class JdbcTest {
@Autowired
private UserController userController;
/**
* 测试转账功能
*/
@Test
public void testTransfer() {
userController.transfer(2, 3, 100);
}
}- 直接运行测试方法,然后查看数据库的时候,发现可以转账成功。
项目结构如下:

11.2 存在的问题
在上面的代码中,是没有处理事务的,所以每一句 SQL 操作都是在自己独立的事务中的,执行完成就提交了。
在实际的开发中,我们的代码可能会报错,下面模拟一下在两个转账之间的代码报错:
java
// 3.更新转账方和收款方的余额
userDao.updateBalance(fromUserId, -amount); // 转账人余额减少
// TODO 模拟报错
int a = 1/0;
userDao.updateBalance(toUserId, amount); // 收款人余额增加此时再执行转账的操作,出问题了!转账人的钱减少了,但是收款人的前没有增加!第一句 SQL 执行完成就提交了,不管后面的代码有没有报错。
这样可不行,所以得让操作在一个事务中才行,如果出现错误,需要回滚事务。
在以前学习 JDBC 的时候,我们通过编码的方式,手动开启和提交事务,在报错的时候,捕获异常,并回滚事务,这种方式编程式事务实现起来比较麻烦,每次都要自己封装,很麻烦。
11.3 声明式事务
在 Spring 中帮我们封装了声明式事务,Spring使用 AOP,对写有 @Transactional 注解的类(方法)创建代理对象,在执行方法的时候,会在切面中开启事务,然后再执行目标方法,根据方法执行情况,决定提交还是回滚事务。
下面就来介绍 Spring 声明式事务的使用。
1 开启事务管理
首先在配置类上开启声明式事务:
java
package com.foooor.hellospring.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
@Configuration // 配置类
@EnableTransactionManagement // 开启事务管理
@ComponentScan("com.foooor.hellospring") // 开启组件扫描,并指定扫描的包
@PropertySource("classpath:jdbc.properties") // 加载外部配置文件
public class SpringConfig {
// ...其他配置
/**
* 配置事务管理器
*/
@Bean
public DataSourceTransactionManager transactionManager(DataSource dataSource) {
DataSourceTransactionManager txManager = new DataSourceTransactionManager();
txManager.setDataSource(dataSource);
return txManager;
}
}- 首先在类上添加
@EnableTransactionManagement注解开启事务管理; - 然后创建事务管理器
DataSourceTransactionManager。
2 配置事务
使用起来非常的简单,我们只需要在 Service 或 Service 的方法上添加 @Transactional 注解即可。
- 加在类上,是对 Service 中所有的方法生效;
- 加在方法上,只对该方法生效;
java
@Transactional // 开启事务
@Service
public class UserServiceImpl implements IUserService {
// ...略
}这样再次运行测试方法,会发现之前的问题不存在了,方法中的操作是在一个事务中完成的。
11.4 事务的属性
下面介绍一下事务@Transactional 的一些属性。
11.4.1 事务传播行为
在项目中,经常存在一个 Service 会调用另一个 Service,如果两个 Service 都开启了事务,那么这个事务是如何传播的呢?
例如: ServiceA 中的 methodA() 调用 ServiceB 的 methodB() 方法,两个 Service 都开启了事务。
Spring 一共定义了 7 种传播行为(在 Propagation 枚举里),最常用的是前 2 个:
| 值 | 含义 |
|---|---|
| REQUIRED(默认) | 如果当前有事务就加入,没有就新建一个事务。 methodA() 调用 methodB() 的时候,methodA() 已经开启了事务,那么 methodB 就不会开启新的事务了,直接在 methodA 的事务中进行。 |
| REQUIRES_NEW | 不管有没有事务,都新建一个事务,原事务挂起。 methodA() 调用 methodB() 的时候,methodB() 不管有没有事务,都会开启新的事务,methodB()执行完成,会提交事务,如果 methodA() 在后续的执行过程中报错,不影响methodB 事务的提交,只有 methodA 的执行会回滚。 |
| SUPPORTS | 如果有事务就加入,没有就非事务方式运行 |
| NOT_SUPPORTED | 始终以非事务方式运行,如果有事务则挂起 |
| MANDATORY | 必须在事务中运行,否则抛异常 |
| NEVER | 必须在非事务中运行,否则抛异常 |
| NESTED | 如果有事务就在事务中嵌套一个子事务(依赖数据库 savepoint) |
举个栗子:
如果我们创建了一个新的 TransferServiceImpl,在这个 Service 中多次调用之前 UserService 中的转账:
java
package com.foooor.hellospring.service.impl;
import com.foooor.hellospring.service.ITransferService;
import com.foooor.hellospring.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Transactional
@Service
public class TransferServiceImpl implements ITransferService {
@Autowired
private IUserService userService;
/**
* 多次转账
*/
@Override
public void multiTransfer(int fromUserId, int toUserId, int amount) {
// 转账1
userService.transfer(fromUserId, toUserId, amount);
// TODO 模拟报错
int a = 1 / 0;
// 转账2
userService.transfer(fromUserId, toUserId, amount);
}
}- 然后修改 Controller 调用 TransferService。
此时我们没有修改事务的传播行为,默认是 REQUIRED ,所以两次转账都没有开启新的事务,都是在 TransferServiceImpl 的事务中。当上面抛出异常后,整个事务都回滚了,所以没有进行转账。
如果修改 UserServiceImpl 事务的传递方式,修改为 Propagation.REQUIRES_NEW ,如下:
java
@Transactional(propagation = Propagation.REQUIRES_NEW) // 开启事务
@Service
public class UserServiceImpl implements IUserService {
// ...略
}那么在 TransferServiceImpl 多次调用 UserServiceImpl 转账的方法,都会开启一个新的事务,所以在 int a = 1 / 0; 报错的时候,第一次转账已经执行完成,并提交了,所以只有第二次转账没有成功。
需要注意:上面调用 UserServiceImpl 中的方法,都是从外部调用的,所以其实是通过代理对象来调用的,在调用目标方法之前,Spring 会通过切面开启事务。
但是如果你的 @Transactional 是添加在 Service 的方法上的,例如一个 Service 中有两个方法,methodA() 和methodB() ,如果只有 methodB 添加了 @Transactional ,此时通过外部调用 methodA() 不会进入事务切面,显然不会开启事务,然后通过 methodA 调用 methodB,即使 methodB 添加了 @Transactional ,此时也不会开启事务,因为不会经过切面了。
11.4.2 回滚策略
默认情况下,不是抛出所有异常,事务都会回滚的。
在Spring 声明式事务(@Transactional)中,默认事务的回滚条件是:
- 运行时异常(RuntimeException) 或 Error → 会触发回滚。
- 受检异常(Checked Exception) → 不会回滚。
例如:
RuntimeException,NullPointerException,IllegalArgumentException→ 默认回滚。IOException,SQLException→ 默认 不回滚。
通过指定事务的回滚策略,可以定义哪些异常触发回滚。
1 指定异常回滚
举个栗子:
下面设置遇到 Exception 异常就回滚:
java
@Transactional(rollbackFor = Exception.class)- 通过上面的设置,出现任何错误或异常,都会触发事务的回滚。
也可以指定某个异常:
java
@Transactional(rollbackFor = SQLException.class)除了通过 rollbackFor 指定,还可以通过 rollbackForClassName 指定:
java
@Transactional(rollbackForClassName = "java.io.IOException")rollbackForClassName是按照异常类名匹配,和rollbackFor类似,但用字符串写全类名。
但是需要注意,如果异常被捕获处理了,是不会回滚的,除非把异常重新抛出来:
java
// 错误示例:异常被"吞掉",事务不会回滚
@Transactional(rollbackFor = Exception.class)
public void exampleMethod() {
try {
// ... 数据库操作
} catch (Exception e) {
e.printStackTrace(); // 仅仅打印日志,没有再次抛出
}
}
// 正确示例:将异常抛出
@Transactional(rollbackFor = Exception.class)
public void exampleMethod() throws Exception {
try {
// ... 数据库操作
} catch (Exception e) {
// ... 可以记录日志
throw e; // 或者抛出新的业务异常
}
}2 指定异常不会滚
还可以指定哪些异常不会滚:
java
@Transactional(noRollbackFor = BusinessException.class)3 回滚策略混用
回滚和不会滚可以混合使用:
java
@Transactional(
rollbackFor = {SQLException.class, IOException.class},
noRollbackFor = {BusinessException.class}
)11.4.3 隔离级别
事务的隔离级别主要用来解决脏读、不可重复读和幻读问题。
脏读(Dirty Read)
- 事务 A 读到了事务 B 尚未提交的数据。
- 如果 B 回滚,A 读到的数据就是不存在的 → 脏数据。
不可重复读(Non-Repeatable Read)
- 事务 A 读取一条数据,然后事务 B 对该条数据进行了修改并提交了数据。
- 事务 A 再次读取数据,发现两次读到的结果不同。
幻读(Phantom Read)
- 事务 A 根据某个条件查询了多行数据。
- 中间事务 B 插入了一些符合条件的新数据。
- A 再次查询时,发现数据“凭空多了几条”。
详细的隔离级别说明,见: SQL 教程 - 事务 。
隔离级别有四种:
| 隔离级别 (Isolation Level) | 脏读 (Dirty Read) | 不可重复读 (Non-Repeatable Read) | 幻读 (Phantom Read) |
|---|---|---|---|
| READ UNCOMMITTED(读未提交) | ❌ 可能发生 | ❌ 可能发生 | ❌ 可能发生 |
| READ COMMITTED(读已提交) | ✔️ 避免 | ❌ 可能发生 | ❌ 可能发生 |
| REPEATABLE READ(可重复读) | ✔️ 避免 | ✔️ 避免 | ❌ 可能发生* |
| SERIALIZABLE(可串行化) | ✔️ 避免 | ✔️ 避免 | ✔️ 避免 |
需要注意,隔离级别越高,性能越低,但数据一致性越强。
MySQL 默认的隔离级别是:REPEATABLE READ;Oracle 默认的隔离级别是:READ COMMITTED。
- 按照 SQL 标准,REPEATABLE READ 仍可能幻读。
- 但在 MySQL InnoDB 中,使用了 间隙锁(Gap Lock),实际上连幻读也避免了,因此比标准更强。
设置 Spring 事务的隔离级别的方式如下:
java
@Transactional(isolation = Isolation.DEFAULT) // 默认隔离级别
@Transactional(isolation = Isolation.READ_UNCOMMITTED) // 读未提交隔离级别
@Transactional(isolation = Isolation.READ_COMMITTED) // 读已提交隔离级别
@Transactional(isolation = Isolation.REPEATABLE_READ) // 可重复读隔离级别
@Transactional(isolation = Isolation.SERIALIZABLE) // 串行化隔离级别根据需要设置即可, 一般采用默认数据的隔离级别就可以了。
11.4.4 只读
对于查询方法而言,显然是不会修改数据库的数据的,那么我们可以把它设置为只读,这样可以告诉数据库当前的操作不涉及写操作,数据库可以针对查询操作来进行优化。
举个栗子:
java
@Transactional(readOnly = true)这样就设置了当前的操作为只读操作,如果进行了写操作,那么会报错!
11.4.5 超时
如果操作出现异常,导致卡住超时,那么我们可以设置事务的超时时间,
我们还可以设置事务的超时时间(单位秒),如果操作出现异常,导致超时,那么可以触发回滚。
java
@Transactional(timeout = 5) // 5 秒11.5 总结
完整的配置类如下:
java
package com.foooor.hellospring.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
@Configuration // 配置类
@EnableTransactionManagement // 开启事务管理
@ComponentScan("com.foooor.hellospring") // 开启组件扫描,并指定扫描的包
@PropertySource("classpath:jdbc.properties") // 加载外部配置文件
public class SpringConfig {
@Value("${jdbc.driver}") // 读取从配置文件注入的属性值。
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
/**
* 配置 Druid 数据源
*/
@Bean
public DataSource dataSource() {
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(username);
ds.setPassword(password);
return ds;
}
/**
* 配置 JdbcTemplate
* 方法的参数,Spring IoC 容器会自动注入对应的 Bean
*/
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
/**
* 配置事务管理器
*/
@Bean
public DataSourceTransactionManager transactionManager(DataSource dataSource) {
DataSourceTransactionManager txManager = new DataSourceTransactionManager();
txManager.setDataSource(dataSource);
return txManager;
}
}而事务的配置,只需要在 Service 上添加注解即可:
java
@Transactional(rollbackFor = Exception.class)到这里 Spring 的重点已经都讲完了!
内容未完......