乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 Spring概述与IoC容器

    • Spring概述与IoC容器
    • Spring Framework 概述
    • IoC 与 DI 核心概念
    • @Configuration 详解
    • @Component 详解
    • @ComponentScan 详解
    • @Import 详解
    • @Profile 详解
    • @PropertySource 详解
    • @Service 详解
    • @Repository 详解
  • 第2章 Bean的定义与依赖注入

    • Bean的定义与依赖注入
    • @Bean 详解
    • @Autowired 详解
    • @Qualifier 详解
    • @Primary 详解
    • @Resource 详解
    • @Inject 详解
    • @Named 详解
    • @Value 详解
    • @Scope 详解
    • @Lazy 详解
  • 第3章 Bean生命周期与作用域

    • Bean生命周期与作用域
    • Bean生命周期概述
    • @PostConstruct
    • @PreDestroy
    • InitializingBean
    • DisposableBean
    • BeanPostProcessor
    • BeanFactoryPostProcessor
  • 第4章 AOP面向切面编程

    • AOP面向切面编程
    • AOP核心概念
    • @EnableAspectJAutoProxy
    • @Aspect
    • @Pointcut
    • @Before
    • @After
    • @AfterReturning
    • @AfterThrowing
    • @Around
  • 第5章 数据访问与事务管理

    • 数据访问与事务管理
    • 数据访问概述
    • @EnableTransactionManagement
    • @Transactional
    • @Transactional 的传播行为
    • @Transactional 的隔离级别
    • @Transactional 的回滚规则
    • @Transactional 的超时与只读属性
    • @TransactionalEventListener
  • 第6章 Spring Boot自动配置基础

    • Spring Boot自动配置基础
    • @SpringBootApplication 注解
    • @EnableAutoConfiguration 注解
    • @ConfigurationProperties 注解
    • @ConditionalOnClass 注解
    • @ConditionalOnMissingBean 注解
    • @ConditionalOnProperty 注解
  • 第7章 从容器到Web: Spring MVC导引

    • Spring MVC 导引
  • 第8章 扩展阅读

    • 扩展阅读
    • Spring 事件机制 — ApplicationEvent / ApplicationListener
    • @EventListener
    • SpEL — Spring 表达式语言
    • 校验 Validation — JSR-303 / JSR-380 Bean Validation
    • 类型转换与数据绑定 — Converter / DataBinder
  • 附录

    • Spring Framework 专业术语
    • Spring 核心知识点
    • Spring 面试高频考点
    • Spring 核心注解速查表

@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 完全忽略,不会生成事务代理。

常用属性概览

属性类型默认值说明
propagationPropagationREQUIRED事务传播行为
isolationIsolationDEFAULT事务隔离级别
timeoutint-1(无限制)事务超时时间(秒)
readOnlybooleanfalse是否为只读事务
rollbackForClass[]RuntimeException / Error触发回滚的异常类型
noRollbackForClass[]无不触发回滚的异常类型

本章后续文档将逐一深入 propagation、isolation、timeout、readOnly、rollbackFor 等属性。本文聚焦类级别与方法级别的基本用法以及声明式事务的底层代理原理。

核心原理:声明式事务的 AOP 代理流程

Spring 的声明式事务并非魔法,而是 AOP 代理的精确应用。当容器检测到 Bean 的方法上有 @Transactional 时,会为其生成代理对象,在方法调用链中插入事务管理逻辑。

事务代理的执行流程

代理创建的关键组件

关键理解:客户端代码注入的是代理对象,而非目标 Bean 本身。当调用代理对象的 public 方法时,TransactionInterceptor 先启动事务,再调用目标方法,最后根据方法执行结果决定提交或回滚。如果客户端直接调用目标对象(如同类内部 this.method()),则代理拦截器不会生效,事务随之失效。

完整示例:飞翔科技学生选课事务

场景简述

飞翔科技公司的 小崔 正在开发学生选课系统。选课操作涉及两个步骤:

  1. 从学生账户扣除课程费用;
  2. 向选课记录表插入一条选课记录。

这两个步骤必须原子执行:扣费成功但插记录失败时,费用必须回退;插记录成功但扣费失败时,记录必须撤销。白歌(架构师)要求小崔使用 @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_idnamebalance
2024001大翔3000.00
2024002白歌3000.00

course_enrollment 表新增记录:

idstudent_idcourse_namefeeenrolled_at
12024001Spring Core 进阶2000.002024-...

场景二(余额不足)控制台输出:

选课失败:余额不足,学号:2024002

场景二数据库状态:

student_idnamebalance
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 方法触发。

上一页
@EnableTransactionManagement
下一页
@Transactional 的传播行为