spring中@Transactional注解和事务的实战

 更新时间:2025年07月08日 09:14:06   作者:GJCTYU  
本文主要介绍了spring中@Transactional注解和事务的实战,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

前言

在开发过程中,遇到多个数据库操作的时候往往只知道加上@transactional注解(spring框架)但是没有系统学习数据库的事务知识以及各种用法,本文会举例介绍数据库中事务的概念以及用法

一、事务是什么?

在实际的项目开发过程中会涉及很多不可分割的数据库操作,如转账业务的进账和出帐,要么全部执行成功,要么全部失败回滚。这样一些的对数据库操作的集合就叫事务。现在假设数据库中有一个表accounts

balanceuser_id
10001
1002

在数据库中执行一段事务的sql如下:

-- 显式事务示例
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;
COMMIT; -- 或 ROLLBACK 回滚

很多人会觉得这里的COMMIT或者ROLLBACK之前,数据库操作只是在内存中,并没有更改表中的数据,需要注意的是,这里的COMMIT或者ROLLBACK 之前,数据已经是持久化了,也就是写入了磁盘(数据库表中)。

二、事务的特性

根据对事务的理解,可以归纳出事务应该具有的特性
原子性(Atomicity):事务被视为不可分割的最小单元,要么全部提交成功,要么全部失败回滚。
一致性(Consistency):事务执行前后,数据库从一个一致状态转变为另一个一致状态。
隔离性(Isolation):多个事务并发执行时,一个事务的操作不应影响其他事务。
持久性(Durability):事务提交后,其对数据的修改是永久性的。

2.1隔离性

在上述特性中比较难理解的应该是隔离性,可以想象这样一个场景:
因为网络延迟或者条件异常,事务A未提交还没来得及回滚

-- 事务A
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;

事务B开始查询事务,查询的结果就是balance = balance - 100

-- 事务B
BEGIN TRANSACTION;
select balance WHERE user_id = 1 from accounts;
commit;

事务A回滚,此刻a,b两个事务间就发生了干扰,a的事务的提交影响了b事务对数据库的正常读取。

-- 事务A
ROLLBACK ;--回滚

所以隔离性就是指的的是a事务对与b事务的互不影响的程度。

2.2事务的隔离级别

事务的隔离级别也可以叫做事务的互不影响程度的级别。
读未提交(Read Uncommitted):影响程度最高,即使事务没有commit,另外的事务也可以读到,可能导致脏读。

-- ----------------------
-- 窗口1(事务A)
-- ----------------------
-- 设置隔离级别为读未提交
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;

-- 修改A账户余额(未提交)
UPDATE account SET balance = 800 WHERE name = 'A账户';
SELECT * FROM account;  -- 查看修改后结果:A账户800

-- ----------------------
-- 窗口2(事务B)
-- ----------------------
-- 设置隔离级别为读未提交
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;

-- 查询A账户余额(读到未提交的数据)
SELECT * FROM account;  -- 结果:A账户800(脏读)

-- 窗口1(事务A)回滚
ROLLBACK;

-- 窗口2再次查询
SELECT * FROM account;  -- 结果:A账户恢复1000(脏读数据消失)

读已提交(Read Committed):只能读取已提交的数据,避免脏读但可能出现不可重复读(一个事务内的多次查询结果不一致)。即b开启事务
进行第一次查询,此时a事务开启事务,a更改完数据后,事务提交,b开启第二次查询,查询结果不一致。

-- ----------------------
-- 窗口1(事务A)
-- ----------------------
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION; --设置读已提交的隔离级别

-- 修改A账户余额并提交
UPDATE account SET balance = 800 WHERE name = 'A账户';
COMMIT;  -- 提交事务

-- ----------------------
-- 窗口2(事务B)
-- ----------------------
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;

-- 第一次查询(事务A未提交时)
SELECT * FROM account;  -- 结果:A账户1000

-- 此时事务A已提交...

-- 第二次查询(事务A已提交)
SELECT * FROM account;  -- 结果:A账户800(不可重复读)

ROLLBACK;

可重复读(Repeatable Read):确保同一事务多次读取结果一致(相当于事务开始时候进行了一次数据库快照,事务内的查询的是查询开启事务时刻的数据库快照的数据),避免不可重复读但可能出现幻读(同一事务内多次查询返回不同行数)。

-- ----------------------
-- 窗口1(事务A)
-- ----------------------
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;

-- 修改A账户余额并提交
UPDATE account SET balance = 800 WHERE name = 'A账户';
COMMIT;

-- ----------------------
-- 窗口2(事务B)
-- ----------------------
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;

-- 第一次查询
SELECT * FROM account;  -- 结果:A账户1000

-- 此时事务A已提交...

-- 第二次查询(结果与第一次一致)
SELECT * FROM account;  -- 结果:A账户1000(可重复读)

-- 提交事务B后,才能看到A的修改
COMMIT;
SELECT * FROM account;  -- 结果:A账户800

这里需要强调下为什么会出现幻读同一事务内多次查询返回不同行数)。
虽然不可重复读读的是快照读,但是使用update/insert/delete等时,是会先当前读,也就是说的是已提交的数据,此刻的快照读会更新为当前使用update/insert/delete等时数据库的快照,所以再之后的普通读select中的快照和最开始的快照的版本是不一样的了,读的数据也就不一样了。幻读例子如下:

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;

-- 插入新记录
INSERT INTO account(name, balance) VALUES ('C账户', 500);
COMMIT;
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;

-- 第一次查询(返回2条记录)
SELECT COUNT(*) FROM account;  

-- 此时事务A插入新记录并提交...

-- 第二次查询(仍返回2条记录)
SELECT COUNT(*) FROM account;  

-- 执行更新操作后再次查询(出现幻读)
UPDATE account SET balance = balance+100 WHERE name = 'A账户';
SELECT COUNT(*) FROM account;  -- 返回3条记录
COMMIT;

串行化(Serializable):最高隔离级别,完全禁止并发问题,所有事务完全同步性能最低(按照一定的顺序执行)。事务A插入数据,事务B在查询时被阻塞,避免幻读。

-- 事务A
-- ----------------------
-- 窗口1(事务A)
-- ----------------------
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;

-- 查询当前数据
SELECT * FROM account;  -- 结果:A账户1000,B账户1000

-- ----------------------
-- 窗口2(事务B)
-- ----------------------
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;

-- 插入新数据(会被事务A阻塞,直到A提交或回滚)
INSERT INTO account (name, balance) VALUES ('C账户', 1000);

-- 窗口1提交事务
COMMIT;

-- 窗口2的插入操作继续执行(此时事务A已提交)
-- 提交后,窗口1再次查询会看到新数据(幻读解决)

三、@Transactional注解

@Transactional注解简介

@Transactional是Spring框架提供的声明式事务管理注解,用于简化数据库事务操作。通过在方法或类上添加该注解,可以自动管理事务的开启、提交、回滚等操作,无需手动编写事务代码。

基本用法

在Spring Boot项目中,需要在启动类或配置类上添加@EnableTransactionManagement以启用事务管理功能。随后可以在服务层方法上使用@Transactional注解:

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    @Transactional
    public void createUser(User user) {
        userRepository.save(user);
    }
}

常用属性配置

@Transactional提供多个属性用于定制事务行为:

#使用注解时候默认配置如下
@Transactional(
    propagation = Propagation.REQUIRED,
    isolation = Isolation.DEFAULT,
    timeout = -1,
    readOnly = false,
    rollbackFor = {RuntimeException.class, Error.class}
)

自定义注解配置

@Transactional(
    propagation = Propagation.REQUIRED, --事务传播行为
    isolation = Isolation.DEFAULT, --隔离级别,数据库默认不可重读
    timeout = 30, --定义超时时间,超过自动给回滚
    readOnly = false, --不只读,可修改
    rollbackFor = {SQLException.class}, --遇到异常捕获后必须回滚
    noRollbackFor = {NullPointerException.class} --遇到NullPointerException异常时不回滚事务
)
public void updateUser(User user) {
    // 业务逻辑
}

事务传播行为

Spring定义了7种事务传播行为,常用选项包括:

  • REQUIRED:默认值,当前有事务则加入,没有则新建
  • REQUIRES_NEW:总是新建事务,暂停当前事务
  • NESTED:在当前事务中嵌套子事务
  • SUPPORTS:有事务则加入,没有则以非事务方式执行
  • NOT_SUPPORTED:以非事务方式执行,暂停当前事务
  • MANDATORY:必须在事务中调用,否则抛出异常
  • NEVER:不能在事务中调用,否则抛出异常

事务隔离级别

隔离级别控制事务间的可见性:

  • DEFAULT:使用数据库默认级别
  • READ_UNCOMMITTED:读未提交
  • READ_COMMITTED:读已提交
  • REPEATABLE_READ:可重复读
  • SERIALIZABLE:串行化

异常处理与回滚

默认只在运行时异常和Error时回滚,可通过以下属性调整:

  • rollbackFor:指定触发回滚的异常类型
  • noRollbackFor:指定不触发回滚的异常类型
  • rollbackForClassName:通过类名指定回滚异常
  • noRollbackForClassName:通过类名指定不回滚异常

性能优化建议

对于只读操作,建议明确设置readOnly=true

@Transactional(readOnly = true)
public User getUser(Long id) {
    return userRepository.findById(id);
}

避免在事务方法中进行远程调用或耗时操作,防止事务长时间占用连接。

四、 事务不生效的可能原因

在很多面试题会问事务不生效的原因, @Transactional 是基于Spring AOP实现的事务切面,所以失效本质上可以归因于AOP代理未生效或切面逻辑被阻断。Spring通过 @EnableTransactionManagement 开启事务支持,底层使用 TransactionInterceptor 拦截目标方法,生成代理对象(JDK动态代理或CGLIB代理)。只有通过代理对象调用方法时,事务切面才会生效;若直接调用目标对象(如 this.方法() ),会绕过代理,导致事务失效。

方法访问权限非public

Spring事务代理要求目标方法必须是public,若定义为private/protected/default,代理无法拦截方法调用。

@Transactional  // 失效
private void saveData() {
    // 操作数据库
}

自调用问题

同类中非事务方法调用事务方法,因直接调用this而非代理对象,导致拦截失效。

public class OrderService {
    public void createOrder() {
        this.saveOrder();  // 自调用,事务失效
    }
    
    @Transactional
    public void saveOrder() {
        // 保存订单
    }
}

异常被捕获未抛出

spring默认仅对RuntimeException和Error回滚,若捕获异常且未重新抛出,事务管理器无法感知异常。示例:

@Transactional
public void update() {
    try {
        jdbcTemplate.update("...");
    } catch (DataAccessException e) {
        // 捕获后未抛出,事务不会回滚
    }
}

通过@Transactional注解指定需回滚的异常:

@Transactional(rollbackFor = IOException.class)

将受检异常转换为事务管理器可识别的类型:

catch (IOException e) {
   throw new RuntimeException("Wrapped exception", e);
}

数据库引擎不支持事务

如MySQL的MyISAM引擎不支持事务,需改用InnoDB。配置示例:

CREATE TABLE test (
    id INT PRIMARY KEY
) ENGINE=InnoDB;  // 必须为InnoDB

未启用事务管理

Spring Boot需配置@EnableTransactionManagement(默认自动开启),传统项目遗漏此注解会导致事务失效。示例缺失:

@SpringBootApplication
// 若手动配置需添加@EnableTransactionManagement
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

特殊场景:final/static方法

代理无法增强final/static方法,导致事务失效。示例:

@Transactional
public final void process() {  // final方法失效
    // 业务逻辑
}

五、分布式事务考虑

对于跨服务的事务操作,@Transactional只能管理本地事务。分布式场景可考虑:

  1. Seata框架
  2. 消息队列最终一致性
  3. TCC模式
  4. SAGA模式

总结

在开发过程中遇到过在同一条件下查询出不同的数据结果,最后发现是对事务的理解使用欠缺导致。这个问题在开发过程中通过还挺常见的,不仅仅是在面对事务的时候加个@Transactional注解就完事了。

到此这篇关于spring中@Transactional注解和事务的实战的文章就介绍到这了,更多相关spring @Transactional内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • IDEA代码热部署和热加载的三种实现方案

    IDEA代码热部署和热加载的三种实现方案

    在日常开发中,我们需要经常修改 Java 代码,手动重启项目,查看修改后的效果,如果在项目小时,重启速度比较快,等待的时间是较短的,我们可以使用代码热加载和热部署解决该问题,本文给大家介绍了三种实现方案,需要的朋友可以参考下
    2023-11-11
  • Java并发编程之ThreadLocal详解

    Java并发编程之ThreadLocal详解

    今天给大家带来的是Java并发编程的相关知识,文中对ThreadLocal做了非常详细的分析及介绍,对小伙伴们很有帮助,需要的朋友可以参考下
    2021-06-06
  • 阿里的一道Java并发面试题详解

    阿里的一道Java并发面试题详解

    这篇文章主要介绍了阿里的一道Java并发面试题详解,网络、并发相关的知识,相对其他一些编程知识点更难一些,主要是不好调试并且涉及内容太多 !,需要的朋友可以参考下
    2019-06-06
  • JAVA保证HashMap线程安全的几种方式

    JAVA保证HashMap线程安全的几种方式

    HashMap是线程不安全的,这意味着如果多个线程并发地访问和修改同一个HashMap实例,可能会导致数据不一致和其他线程安全问题,本文主要介绍了JAVA保证HashMap线程安全的几种方式,具有一定的参考价值,感兴趣的可以了解一下
    2025-04-04
  • JavaOOP封装实例解读

    JavaOOP封装实例解读

    封装通过private限制属性访问,提供get/set方法控制数据读写,确保值合法,示例中Student类属性私有,Test类需调用set方法赋值并验证,get方法获取值,实现数据隐藏与安全操作
    2025-09-09
  • MyBatisPlus项目的创建和使用

    MyBatisPlus项目的创建和使用

    本文介绍了MyBatis-Plus的基本使用方法,包括项目的创建和配置、增删查改操作、日志打印以及条件构造器的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2024-11-11
  • Windows下使用Graalvm将Springboot应用编译成exe大大提高启动和运行效率(推荐)

    Windows下使用Graalvm将Springboot应用编译成exe大大提高启动和运行效率(推荐)

    这篇文章主要介绍了Windows下使用Graalvm将Springboot应用编译成exe大大提高启动和运行效率,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-02-02
  • 修改idea的这些启动参数,令你的idea健步如飞

    修改idea的这些启动参数,令你的idea健步如飞

    这篇文章主要介绍了修改idea的这些启动参数,令你的idea健步如飞~具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-01-01
  • SpringBoot实现JWT token自动续期的示例代码

    SpringBoot实现JWT token自动续期的示例代码

    本文主要介绍了SpringBoot实现JWT token自动续期的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-01-01
  • IDEA运行spring项目时,控制台未出现的解决方案

    IDEA运行spring项目时,控制台未出现的解决方案

    文章总结了在使用IDEA运行代码时,控制台未出现的问题和解决方案,问题可能是由于点击图标或重启IDEA后控制台仍未显示,解决方案提供了解决方法,包括通过右三角Run运行(快捷键:Alt+42)或以Debug运行(快捷键:Alt+5)来解决
    2025-01-01

最新评论