@Transactional
定义与作用
@Transactional 是 Spring 声明式事务管理的核心注解。它将方法或类的执行包裹在数据库事务的边界之内,确保一组操作要么全部成功提交(commit),要么在发生异常时全部回滚(rollback)。
在 Spring 5.x / Spring Boot 2.x 中,@Transactional 的实现依赖于 AOP 代理机制:Spring 为标注了该注解的 Bean 创建代理对象,在方法调用前后自动开启、提交或回滚事务。开发者无需在代码中手动编写 begin、commit、rollback,事务逻辑与业务逻辑完全解耦。
适用位置与常用属性
适用位置
| 位置 | 效果 | 优先级 |
|---|---|---|
| 类级别 | 该类所有 public 方法均具有事务性 | 方法级 > 类级 |
| 方法级别 | 仅该方法具有事务性 | 方法级 > 类级 |
| 接口方法 | 仅对基于接口的 JDK 代理生效,CGLIB 代理不继承接口注解 | 不推荐 |
重要限制:
@Transactional只能应用于public方法。非public方法(private、protected、包可见)上的该注解会被 Spring 完全忽略,不会生成事务代理。
常用属性概览
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
propagation | Propagation | REQUIRED | 事务传播行为 |
isolation | Isolation | DEFAULT | 事务隔离级别 |
timeout | int | -1(无限制) | 事务超时时间(秒) |
readOnly | boolean | false | 是否为只读事务 |
rollbackFor | Class[] | RuntimeException / Error | 触发回滚的异常类型 |
noRollbackFor | Class[] | 无 | 不触发回滚的异常类型 |
本章后续文档将逐一深入 propagation、isolation、timeout、readOnly、rollbackFor 等属性。本文聚焦类级别与方法级别的基本用法以及声明式事务的底层代理原理。
核心原理:声明式事务的 AOP 代理流程
Spring 的声明式事务并非魔法,而是 AOP 代理的精确应用。当容器检测到 Bean 的方法上有 @Transactional 时,会为其生成代理对象,在方法调用链中插入事务管理逻辑。
事务代理的执行流程
代理创建的关键组件
关键理解:客户端代码注入的是代理对象,而非目标 Bean 本身。当调用代理对象的 public 方法时,TransactionInterceptor 先启动事务,再调用目标方法,最后根据方法执行结果决定提交或回滚。如果客户端直接调用目标对象(如同类内部 this.method()),则代理拦截器不会生效,事务随之失效。
完整示例:飞翔科技学生选课事务
场景简述
飞翔科技公司的 小崔 正在开发学生选课系统。选课操作涉及两个步骤:
- 从学生账户扣除课程费用;
- 向选课记录表插入一条选课记录。
这两个步骤必须原子执行:扣费成功但插记录失败时,费用必须回退;插记录成功但扣费失败时,记录必须撤销。白歌(架构师)要求小崔使用 @Transactional 保证数据一致性。
操作前:数据库表结构与初始数据
-- 学生账户表
CREATE TABLE student_account (
student_id INT PRIMARY KEY,
name VARCHAR(50),
balance DECIMAL(10,2) NOT NULL
);
-- 选课记录表
CREATE TABLE course_enrollment (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
student_id INT NOT NULL,
course_name VARCHAR(100) NOT NULL,
fee DECIMAL(10,2) NOT NULL,
enrolled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 初始数据
INSERT INTO student_account (student_id, name, balance) VALUES
(2024001, '大翔', 5000.00),
(2024002, '白歌', 3000.00);
配置类:启用事务管理
package com.feixiang.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableTransactionManagement // 开启注解驱动的事务管理
public class TransactionConfig {
// DataSource 和 JdbcTemplate 配置见《数据访问概述》
}
DAO 层
package com.feixiang.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class EnrollmentDao {
private final JdbcTemplate jdbcTemplate;
@Autowired
public EnrollmentDao(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public int deductBalance(int studentId, double fee) {
String sql = "UPDATE student_account SET balance = balance - ? WHERE student_id = ? AND balance >= ?";
return jdbcTemplate.update(sql, fee, studentId, fee);
}
public int insertEnrollment(int studentId, String courseName, double fee) {
String sql = "INSERT INTO course_enrollment (student_id, course_name, fee) VALUES (?, ?, ?)";
return jdbcTemplate.update(sql, studentId, courseName, fee);
}
}
Service 层:类级别 @Transactional
package com.feixiang.service;
import com.feixiang.dao.EnrollmentDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional // 类级别:该类所有 public 方法均在事务中执行
public class EnrollmentService {
private final EnrollmentDao enrollmentDao;
@Autowired
public EnrollmentService(EnrollmentDao enrollmentDao) {
this.enrollmentDao = enrollmentDao;
}
// 选课方法:扣费 + 插记录
public void enrollCourse(int studentId, String courseName, double fee) {
int rows = enrollmentDao.deductBalance(studentId, fee);
if (rows == 0) {
throw new IllegalStateException("余额不足,学号:" + studentId);
}
enrollmentDao.insertEnrollment(studentId, courseName, fee);
System.out.println("[选课成功] " + courseName + " 费用:" + fee);
}
// 查询方法:不需要事务,方法级覆盖类级配置
@Transactional(readOnly = true, timeout = 5)
public double queryBalance(int studentId) {
String sql = "SELECT balance FROM student_account WHERE student_id = ?";
return enrollmentDao.getJdbcTemplate().queryForObject(sql, Double.class, studentId);
}
}
注:
queryBalance方法通过方法级@Transactional(readOnly = true)覆盖了类级别的默认配置,优化了只读查询的性能。
测试执行与结果
package com.feixiang;
import com.feixiang.service.EnrollmentService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class EnrollmentTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext("com.feixiang");
EnrollmentService service = ctx.getBean(EnrollmentService.class);
// 场景一:正常选课
try {
service.enrollCourse(2024001, "Spring Core 进阶", 2000.00);
} catch (Exception e) {
System.err.println("选课失败:" + e.getMessage());
}
// 场景二:余额不足,触发回滚
try {
service.enrollCourse(2024002, "JVM 性能调优", 5000.00);
} catch (Exception e) {
System.err.println("选课失败:" + e.getMessage());
}
ctx.close();
}
}
操作后:控制台输出与数据变化
场景一(正常选课)控制台输出:
[选课成功] Spring Core 进阶 费用:2000.0
场景一数据库状态:
| student_id | name | balance |
|---|---|---|
| 2024001 | 大翔 | 3000.00 |
| 2024002 | 白歌 | 3000.00 |
course_enrollment 表新增记录:
| id | student_id | course_name | fee | enrolled_at |
|---|---|---|---|---|
| 1 | 2024001 | Spring Core 进阶 | 2000.00 | 2024-... |
场景二(余额不足)控制台输出:
选课失败:余额不足,学号:2024002
场景二数据库状态:
| student_id | name | balance |
|---|---|---|
| 2024001 | 大翔 | 3000.00 |
| 2024002 | 白歌 | 3000.00(未变化) |
course_enrollment 表:无新增记录。事务回滚生效,扣费和插记录两个操作都被撤销。
易错场景与面试考点
反例一:@Transactional 标注 private 方法
@Service
public class EnrollmentService {
@Autowired
private EnrollmentDao enrollmentDao;
// 错误:private 方法上的 @Transactional 完全无效
@Transactional
private void deductAndEnroll(int studentId, String courseName, double fee) {
enrollmentDao.deductBalance(studentId, fee);
enrollmentDao.insertEnrollment(studentId, courseName, fee);
}
public void enrollCourse(int studentId, String courseName, double fee) {
// 调用的是 this.deductAndEnroll(),事务不生效
deductAndEnroll(studentId, courseName, fee);
}
}
问题分析:Spring AOP 代理基于代理对象的方法调用拦截。无论是 JDK 动态代理还是 CGLIB 代理,都无法拦截 private 方法。@Transactional 在 private 方法上会被静默忽略,方法以非事务方式执行。若此时发生异常,数据库操作不会回滚。
正确写法:将方法改为 public,或通过代理对象调用。
反例二:同类内部自调用导致事务失效
@Service
public class EnrollmentService {
@Autowired
private EnrollmentDao enrollmentDao;
// 外部调用此方法,事务生效
@Transactional
public void enrollCourse(int studentId, String courseName, double fee) {
deductBalance(studentId, fee);
enrollmentDao.insertEnrollment(studentId, courseName, fee);
}
// 同类内部调用,事务失效
public void deductBalance(int studentId, double fee) {
enrollmentDao.deductBalance(studentId, fee);
if (true) {
throw new RuntimeException("模拟异常");
}
}
}
问题分析:enrollCourse 方法上的 @Transactional 会生效,因为它由外部客户端通过代理对象调用。但 deductBalance 方法内部的异常发生在非事务方法中(或即使 deductBalance 有 @Transactional,通过 this.deductBalance() 调用也不会经过代理)。更隐蔽的陷阱是:如果 enrollCourse 调用同类另一个有 @Transactional 的方法,被调方法的事务配置也会被忽略。
正确写法:
// 方案一:注入自身代理
@Service
public class EnrollmentService {
@Autowired
private EnrollmentDao enrollmentDao;
@Autowired
private EnrollmentService self; // 注入自身代理
@Transactional
public void enrollCourse(int studentId, String courseName, double fee) {
self.deductBalance(studentId, fee); // 通过代理调用
enrollmentDao.insertEnrollment(studentId, courseName, fee);
}
@Transactional
public void deductBalance(int studentId, double fee) {
// ...
}
}
// 方案二:使用 AopContext(需在 @EnableTransactionManagement 开启 exposeProxy = true)
@Transactional
public void enrollCourse(int studentId, String courseName, double fee) {
((EnrollmentService) AopContext.currentProxy()).deductBalance(studentId, fee);
}
反例三:异常被 catch 未抛出,事务不回滚
@Service
public class EnrollmentService {
@Autowired
private EnrollmentDao enrollmentDao;
@Transactional
public void enrollCourse(int studentId, String courseName, double fee) {
enrollmentDao.deductBalance(studentId, fee);
try {
enrollmentDao.insertEnrollment(studentId, courseName, fee);
} catch (DataAccessException e) {
// 错误:吞掉异常,Spring 认为方法正常返回,执行 commit
System.err.println("插入选课记录失败:" + e.getMessage());
}
}
}
问题分析:Spring 的事务拦截器在方法返回后检查是否存在未捕获的异常。如果异常在方法内部被 catch 且未重新抛出,拦截器看到的是正常返回,于是执行 commit。此时扣费已提交,但选课记录未插入,数据不一致。
正确写法:
@Transactional
public void enrollCourse(int studentId, String courseName, double fee) {
enrollmentDao.deductBalance(studentId, fee);
try {
enrollmentDao.insertEnrollment(studentId, courseName, fee);
} catch (DataAccessException e) {
// 记录日志后,必须重新抛出异常以触发回滚
System.err.println("插入选课记录失败:" + e.getMessage());
throw new IllegalStateException("选课事务失败", e);
}
}
反例四:@Transactional 与 @Async 的冲突
@Service
public class EnrollmentService {
@Transactional
@Async // 错误:两个注解同时标注
public void enrollCourse(int studentId, String courseName, double fee) {
// ...
}
}
问题分析:@Async 会使方法在另一个线程中异步执行,而 @Transactional 的事务上下文绑定到当前线程(通过 ThreadLocal 实现)。当方法切换到异步线程时,原线程的事务上下文不会自动传递,导致事务注解失效。此外,异步方法的返回值为 void 或 Future,调用方无法直接感知异常,进一步增加了事务管理的复杂度。
正确写法:将事务操作与异步操作分离:
@Service
public class EnrollmentService {
@Autowired
private EnrollmentDao enrollmentDao;
@Autowired
private NotificationService notificationService;
// 同步执行事务
@Transactional
public void enrollCourse(int studentId, String courseName, double fee) {
enrollmentDao.deductBalance(studentId, fee);
enrollmentDao.insertEnrollment(studentId, courseName, fee);
// 事务提交后,再异步发送通知
notificationService.sendEnrollmentEmail(studentId, courseName);
}
}
@Service
public class NotificationService {
@Async
public void sendEnrollmentEmail(int studentId, String courseName) {
// 发送邮件,与事务无关
}
}
面试高频考点
Q1:@Transactional 对 private 方法有效吗?为什么?
无效。Spring 声明式事务基于 AOP 代理实现,无论是 JDK 动态代理(基于接口)还是 CGLIB 代理(基于子类继承),都无法拦截
private方法。CGLIB 通过生成子类重写方法实现代理,而子类不能重写父类的private方法。因此@Transactional在private方法上会被静默忽略。
Q2:同类内部方法调用为什么会导致 @Transactional 失效?
因为
this.method()调用的是目标对象本身,而非代理对象。Spring 的事务拦截器嵌入在代理对象中,只有通过代理对象调用的方法才会经过TransactionInterceptor。同类内部调用绕过了代理层,导致事务注解不被处理。
Q3:方法内部 catch 了异常,为什么事务没有回滚?
Spring 的事务拦截器在方法返回后检查是否存在未捕获的
RuntimeException或Error。如果异常在方法内部被吞掉(catch 后未重新抛出),拦截器认为方法正常完成,执行commit。解决方案:catch 后记录日志,然后重新抛出异常;或改用编程式事务管理TransactionTemplate。
Q4:@Transactional 和 @Async 一起使用有什么问题?
二者冲突的根本原因是事务上下文与线程绑定。
@Transactional通过ThreadLocal在当前线程维护事务状态,@Async将方法切换到新线程执行,导致事务上下文丢失。正确做法是将事务操作保留在同步方法中,异步操作(如发送通知)在事务提交后通过另一个@Async方法触发。