• 使用了@Transactional注解为何还是会出现事务失效的问题?
  • 发布于 12小时前
  • 38 热度
    0 评论
在SpringBoot项目开发中,@Transactional注解是保障数据一致性的核心手段,但同类中非事务方法调用事务方法时,常出现事务失效问题。这会导致数据错乱、业务逻辑异常等严重后果,直接影响系统稳定性。本文将从失效现象出发,拆解底层原理,提供4种可落地的解决方案,并深入解析核心属性,帮助我们彻底规避事务风险。

一. @Transactional同类调用失效现象复现
在UserService中,无事务的selectUser()方法调用有事务的updateUser()方法。updateUser()内部包含两次用户信息更新操作,中间刻意抛出除零异常。测试后发现,即使抛出异常,第一次更新操作仍生效,数据未回滚,证明事务失效。
@Service
public class  UserServiceImpl implements UserService {
    @Autowired
    private  UserMapper userMapper;

    // 无事务的方法,调用有事务的updateUser()
    @Override
    public  void  selectUser(Long userId1, Long userId2) {
        // 调用同类的事务方法
        updateUser(userId1, userId2);
    }

    // 加了@Transactional的事务方法
    @Override
    @Transactional(rollbackFor = Exception.class )
    public  void  updateUser(Long userId1, Long userId2) {
        // 第一次更新用户信息(假设更新年龄为16)
        User user1 = new   User();
        user1.setId(userId1);
        user1.setAge(16);
        userMapper.updateById(user1);

        // 刻意抛出除零异常,模拟业务异常
        int  i = 1 / 0;
        // 堆代码 duidaima.com
        // 第二次更新用户信息(假设更新年龄为20)
        User user2 = new   User();
        user2.setId(userId2);
        user2.setAge(20);
        userMapper.updateById(user2);
    }
}
代码解析
selectUser()方法未加@Transactional,内部通过this.updateUser()调用事务方法;updateUser()加了@Transactional(rollbackFor = Exception.class),定义了异常回滚规则。测试前数据库中两个用户年龄均为18,调用selectUser()后,第一个用户年龄变为16,第二个仍为18,且异常抛出。这说明updateUser()的事务未生效,因为this是原始对象,而非Spring代理对象,无法触发事务增强逻辑。

适当扩展
这种失效场景在实际业务中极为常见,例如订单处理模块中,无事务的createOrder()调用有事务的updateStock()。若事务失效,会出现订单创建成功但库存未扣减的情况,导致超卖问题。因此,必须在项目初期就重视同类方法调用的事务处理,避免线上数据不一致。

二. 解决方案一:为调用方方法添加@Transactional
给原本无事务的selectUser()方法添加@Transactional注解,让外层方法开启事务。此时updateUser()的异常会向上抛,触发selectUser()的事务回滚,最终保证数据一致性。
@Service
public class  UserServiceImpl implements UserService {
    @Autowired
    private  UserMapper userMapper;

    // 为调用方添加@Transactional,开启外层事务
    @Override
    @Transactional(rollbackFor = Exception.class )
    public  void  selectUser(Long userId1, Long userId2) {
        // 仍为this调用,但外层已有事务
        updateUser(userId1, userId2);
    }

    // 原有事务方法
    @Override
    @Transactional(rollbackFor = Exception.class )
    public  void  updateUser(Long userId1, Long userId2) {
        // 第一次更新用户信息
        User user1 = new   User();
        user1.setId(userId1);
        user1.setAge(16);
        userMapper.updateById(user1);

        // 抛出除零异常
        int  i = 1 / 0;

        // 第二次更新用户信息
        User user2 = new   User();
        user2.setId(userId2);
        user2.setAge(20);
        userMapper.updateById(user2);
    }
}
代码解析
selectUser()添加@Transactional后,调用selectUser()时会先开启事务;内部this.updateUser()虽未触发代理,但updateUser()的异常会传递到selectUser()。事务管理器检测到异常,会回滚selectUser()事务范围内的所有操作,包括第一次更新。测试后两个用户年龄均保持18,证明事务生效。

适当扩展
该方案的优势是实现简单,无需修改调用方式,仅需添加注解。但需注意,外层事务范围会覆盖调用方所有操作,若selectUser()中包含查询、远程调用等耗时操作,会增加事务持有时间,可能导致数据库锁竞争加剧。因此,该方案更适合调用方本身需要事务控制的场景。

三. 解决方案二:通过ApplicationContext获取代理对象
核心思路是从Spring容器(ApplicationContext)中获取UserService的代理对象,而非使用this(原始对象)。通过代理对象调用updateUser()方法,触发事务增强逻辑,解决事务失效问题。
@Service
public class  UserServiceImpl implements UserService {
    @Autowired
    private  UserMapper userMapper;
    // 注入Spring容器ApplicationContext
    @Autowired
    private  ApplicationContext applicationContext;

    @Override
    public  void  selectUser(Long userId1, Long userId2) {
        // 从容器中获取UserService代理对象
        UserService userServiceProxy = applicationContext.getBean(UserService.class );
        // 通过代理对象调用事务方法
        userServiceProxy.updateUser(userId1, userId2);
    }

    @Override
    @Transactional(rollbackFor = Exception.class )
    public  void  updateUser(Long userId1, Long userId2) {
        // 第一次更新用户信息
        User user1 = new   User();
        user1.setId(userId1);
        user1.setAge(16);
        userMapper.updateById(user1);

        // 抛出除零异常
        int  i = 1 / 0;

        // 第二次更新用户信息
        User user2 = new   User();
        user2.setId(userId2);
        user2.setAge(20);
        userMapper.updateById(user2);
    }
}
代码解析
ApplicationContext是Spring的核心容器,存储了所有已初始化的Bean,其中Service Bean默认是代理对象。通过applicationContext.getBean(UserService.class)获取的是代理对象,调用updateUser()时,代理会先执行事务开启逻辑,再调用原始对象的方法。若出现异常,代理会触发事务回滚,测试后数据保持初始状态,事务生效。

适当扩展
该方案需依赖Spring容器,若项目未注入ApplicationContext,需在配置类中添加@Bean注解注册ApplicationContext。但直接依赖容器会增加代码与Spring框架的耦合度,若后续容器配置变更,可能需要同步修改该部分代码。此外,多线程环境下需确保ApplicationContext的线程安全性,避免因容器未初始化完成导致的Bean获取失败。

四. 解决方案三:注入自身Service获取代理对象
利用Spring默认支持循环依赖的特性,在UserServiceImpl中注入自身(UserService)。注入的对象是Spring生成的代理对象,通过该代理调用updateUser(),即可触发事务。
@Service
public class  UserServiceImpl implements UserService {
    @Autowired
    private  UserMapper userMapper;
    // 注入自身Service,Spring会注入代理对象
    @Autowired
    private  UserService userService;

    @Override
    public  void  selectUser(Long userId1, Long userId2) {
        // 通过注入的代理对象调用事务方法
        userService.updateUser(userId1, userId2);
    }

    @Override
    @Transactional(rollbackFor = Exception.class )
    public  void  updateUser(Long userId1, Long userId2) {
        // 第一次更新用户信息
        User user1 = new   User();
        user1.setId(userId1);
        user1.setAge(16);
        userMapper.updateById(user1);

        // 抛出除零异常
        int  i = 1 / 0;

        // 第二次更新用户信息
        User user2 = new   User();
        user2.setId(userId2);
        user2.setAge(20);
        userMapper.updateById(user2);
    }
}
代码解析
Spring通过三级缓存机制解决循环依赖,当UserServiceImpl注入自身时,容器会先创建原始对象,生成代理对象后,再将代理对象注入到自身属性中。因此,userService属性是代理对象,调用updateUser()时会触发事务拦截器,执行开启事务、异常回滚等逻辑。测试后数据未被修改,事务正常生效。

适当扩展
该方案代码简洁,无需依赖容器或额外配置,是项目中常用的解决方案。但需注意,若项目通过spring.main.allow-circular-references=false禁用了循环依赖,该方案会失效,此时需改用其他方案。此外,注入自身时需避免在构造方法中使用该属性,防止构造方法执行时属性未完成注入,导致空指针异常。

五. 解决方案四:通过AopContext获取代理对象
借助Spring AOP提供的AopContext类,从当前线程中获取代理对象。该方案需引入AOP依赖,并配置暴露代理对象,确保AopContext能获取到代理。
<!-- 引入Spring AOP依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
// 在启动类或配置类添加注解,暴露代理对象到线程
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class  SpringBootTransactionApplication {
    public  static  void  main(String[] args) {
        SpringApplication.run(SpringBootTransactionApplication.class , args);
    }
}

@Service
public class  UserServiceImpl implements UserService {
    @Autowired
    private  UserMapper userMapper;

    @Override
    public  void  selectUser(Long userId1, Long userId2) {
        // 从AopContext获取当前线程的代理对象
        UserService userServiceProxy = (UserService) AopContext.currentProxy();
        // 通过代理对象调用事务方法
        userServiceProxy.updateUser(userId1, userId2);
    }

    @Override
    @Transactional(rollbackFor = Exception.class )
    public  void  updateUser(Long userId1, Long userId2) {
        // 第一次更新用户信息
        User user1 = new   User();
        user1.setId(userId1);
        user1.setAge(16);
        userMapper.updateById(user1);

        // 抛出除零异常
        int  i = 1 / 0;

        // 第二次更新用户信息
        User user2 = new   User();
        user2.setId(userId2);
        user2.setAge(20);
        userMapper.updateById(user2);
    }
}
代码解析
@EnableAspectJAutoProxy(exposeProxy = true)表示开启AspectJ自动代理,并将代理对象暴露到当前线程中。AopContext通过ThreadLocal存储代理对象,确保线程隔离,调用AopContext.currentProxy()可获取当前线程的代理对象。通过代理对象调用updateUser(),会触发事务增强逻辑,异常时回滚操作,测试后数据保持初始状态。

适当扩展
该方案依赖Spring AOP模块,若项目已使用AOP(如日志切面、权限切面),则无需额外引入依赖,集成成本低。但在多线程场景下,需注意ThreadLocal的特性:子线程无法继承父线程的ThreadLocal值,若selectUser()中开启子线程调用AopContext.currentProxy(),会获取到null。此时需改用其他方案,或通过线程池传递代理对象。

六. 事务失效核心原因:代理对象与this的区别
Spring事务的实现基于动态代理,@Autowired注入的Service对象是代理对象,而方法内部的this是原始目标对象。代理对象会在调用方法前后执行事务增强逻辑,原始对象调用则无此过程,这是同类调用事务失效的核心原因。
// 简化的动态代理调用流程(JDK动态代理示例)
public class  TransactionProxy implements InvocationHandler {
    // 原始目标对象(UserServiceImpl实例)
    private  Object target;

    public  TransactionProxy(Object target) {
        this.target = target;
    }

    @Override
    public  Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = null;
        try {
            // 事务增强逻辑:开启事务
            beginTransaction();
            // 调用原始对象的方法(this指向target)
            result = method.invoke(target, args);
            // 事务增强逻辑:提交事务
            commitTransaction();
        } catch (Exception e) {
            // 事务增强逻辑:回滚事务
            rollbackTransaction();
            throw  e;
        }
        return  result;
    }

    private  void  beginTransaction() { /* 开启事务逻辑 */ }
    private  void  commitTransaction() { /* 提交事务逻辑 */ }
    private  void  rollbackTransaction() { /* 回滚事务逻辑 */ }
}
代码解析
Spring创建Service Bean时,会生成代理对象(如TransactionProxy),代理对象持有原始目标对象(target)。Controller中@Autowired的UserService是代理对象,调用selectUser()时,会进入代理的invoke()方法,先执行开启事务逻辑(若有@Transactional),再调用target.selectUser()。在target.selectUser()内部,this指向target(原始对象),调用updateUser()时直接执行原始方法,无代理的事务增强逻辑,导致事务失效。

适当扩展
不仅@Transactional,Spring中基于AOP的注解(如@Async、@Cacheable)均存在同类调用失效问题,原理完全一致。理解代理机制后,可举一反三:只要是通过this调用的AOP增强方法,均无法触发增强逻辑。因此,在使用这类注解时,需确保通过代理对象调用方法,避免直接使用this。

七. 重新理解@Transactional核心属性
7.1 rollbackFor:指定触发回滚的异常类型
@Transactional默认仅对Error和RuntimeException及其子类触发回滚,自定义业务异常(若为受检异常)不会触发回滚。因此,项目中需显式指定rollbackFor,确保所有异常都能触发事务回滚。
@Service
public class  UserServiceImpl implements UserService {
    @Autowired
    private  UserMapper userMapper;

    // 自定义受检异常
    public static class  BusinessException extends  Exception {
        public  BusinessException(String message) {
            super(message);
        }
    }

    @Override
    @Transactional(rollbackFor = {Exception.class , BusinessException.class })
    public  void  updateUser(Long userId) throws BusinessException {
        User user = new   User();
        user.setId(userId);
        user.setAge(16);
        userMapper.updateById(user);

        // 抛出自定义受检异常
        throw new   BusinessException("业务异常");
    }
}
代码解析
rollbackFor属性接收异常类型数组,指定后,事务管理器会捕获该类型及其子类的异常,触发回滚。若不指定rollbackFor,抛出BusinessException(受检异常)时,事务不会回滚,更新操作会生效;指定后,异常触发回滚,数据保持初始状态。项目中建议直接指定rollbackFor = Exception.class,覆盖所有异常场景,避免因异常类型遗漏导致事务不回滚。

适当扩展
需注意,rollbackFor指定的异常需在方法上声明抛出(受检异常)或无需声明(运行时异常)。若方法内部捕获异常且未重新抛出,即使指定了rollbackFor,事务也不会回滚,因为事务管理器无法检测到异常。因此,Service层应避免直接捕获异常,需将异常向上抛,由全局异常处理器(@RestControllerAdvice)统一处理。

7.2 propagation:定义事务传播行为
事务传播行为描述了被调用方法与调用方事务的关系,是给调用方“制定”的规则,决定被调用方法是否加入调用方事务,或创建新事务。最常用的传播行为是REQUIRED(默认)和REQUIRES_NEW。
@Service
public class  OrderServiceImpl implements OrderService {
    @Autowired
    private  OrderMapper orderMapper;
    @Autowired
    private  LogService logService;

    // 传播行为:REQUIRED(默认)
    @Override
    @Transactional(rollbackFor = Exception.class , propagation = Propagation.REQUIRED)
    public  void  createOrder(Order order) {
        // 1. 创建订单(加入当前事务)
        orderMapper.insert(order);
        try {
            // 2. 记录日志(传播行为:REQUIRES_NEW,新建独立事务)
            logService.recordLog("创建订单:" + order.getId());
        } catch (Exception e) {
            // 日志记录失败不影响订单创建
            e.printStackTrace();
        }
        // 3. 抛出异常,回滚订单创建
        int  i = 1 / 0;
    }
}

@Service
public class  LogServiceImpl implements LogService {
    @Autowired
    private  LogMapper logMapper;

    // 传播行为:REQUIRES_NEW
    @Override
    @Transactional(rollbackFor = Exception.class , propagation = Propagation.REQUIRES_NEW)
    public  void  recordLog(String content) {
        Log log = new   Log();
        log.setContent(content);
        logMapper.insert(log);
    }
}
代码解析
createOrder()的传播行为是REQUIRED,调用时会开启事务;内部调用recordLog(),其传播行为是REQUIRES_NEW,会新建独立事务,与外层事务隔离。createOrder()抛出异常后,外层事务回滚(订单创建操作取消),但recordLog()的独立事务已提交(日志记录成功)。这体现了REQUIRES_NEW的特性:被调用方法创建新事务,与外层事务互不影响,适合日志记录、消息发送等无需与主业务事务绑定的场景。

适当扩展
项目中99%的场景使用默认的REQUIRES_NEW即可满足需求,仅在以下场景使用其他传播行为:
REQUIRES_NEW:主业务事务回滚不影响被调用方法(如日志、消息)。
SUPPORTS:被调用方法是否有事务取决于调用方(如查询操作,可支持事务也可无事务)。
MANDATORY:要求调用方必须有事务(如核心业务操作,确保事务一致性)。
避免滥用传播行为,否则会增加事务管理复杂度,导致难以排查的事务问题。

八. 实际项目开发中的@Transactional运用规范
在实际项目中,正确使用@Transactional需遵循一定规范,避免因使用不当导致事务失效或性能问题。以下是结合项目经验总结的核心规范,附带代码示例。
@Service
public class  OrderServiceImpl implements OrderService {
    @Autowired
    private  OrderMapper orderMapper;
    @Autowired
    private  StockMapper stockMapper;
    // 注入自身,避免同类调用事务失效
    @Autowired
    private  OrderService orderService;
    // 堆代码 duidaima.com
    // 1. 明确事务边界:仅包含核心写操作,排除查询、远程调用
    @Override
    @Transactional(rollbackFor = Exception.class )
    public  void  createOrder(OrderDTO orderDTO) {
        // 2. 核心写操作1:创建订单
        Order order = convertToOrder(orderDTO);
        orderMapper.insert(order);

        // 3. 核心写操作2:扣减库存(通过自身代理调用,确保事务生效)
        orderService.deductStock(orderDTO.getProductId(), orderDTO.getQuantity());

        // 4. 避免在事务中做耗时操作(如文件上传、第三方接口调用)
        // 若需调用第三方,可在事务提交后异步执行
    }

    // 事务方法:扣减库存
    @Override
    @Transactional(rollbackFor = Exception.class )
    public  void  deductStock(Long productId, Integer quantity) {
        Stock stock = stockMapper.selectByProductId(productId);
        if (stock.getQuantity() < quantity) {
            throw new   RuntimeException("库存不足");
        }
        stock.setQuantity(stock.getQuantity() - quantity);
        stockMapper.updateById(stock);
    }

    // 非事务方法:DTO转换(查询、转换等无写操作的逻辑)
    private  Order convertToOrder(OrderDTO orderDTO) {
        Order order = new   Order();
        order.setId(orderDTO.getOrderId());
        order.setProductId(orderDTO.getProductId());
        order.setQuantity(orderDTO.getQuantity());
        return  order;
    }
}
代码解析
该示例遵循了项目中@Transactional的核心运用规范:
事务边界明确:createOrder()仅包含订单创建和库存扣减的核心写操作,排除DTO转换(非事务)、第三方调用(耗时操作)。
避免同类调用失效:deductStock()是事务方法,createOrder()通过注入的orderService(代理对象)调用,确保事务生效。
事务方法仅含写操作:查询操作(如convertToOrder()中的数据转换)在外层非事务方法中执行,减少事务持有时间。
排除耗时操作:若需调用第三方接口(如支付接口),可通过@Async在事务提交后异步执行,避免事务超时。

适当扩展
除代码规范外,项目中还需配合监控和日志:
日志记录:在事务方法前后打印日志,记录事务开启、提交/回滚状态,便于排查事务问题。
监控告警:通过Spring Boot Actuator监控事务数量、事务耗时,设置告警阈值(如事务耗时超过5秒告警),及时发现慢事务。
数据库优化:事务操作涉及的表需建立合适索引,避免全表扫描导致事务耗时过长;同时设置合理的事务隔离级别(如MySQL默认的REPEATABLE READ),平衡一致性和性能。

结尾
@Transactional看似简单,实则是Spring事务管理机制与动态代理的结合体。同类调用失效问题的本质,是对代理对象与原始对象的混淆;而核心属性的正确配置,是保障事务生效的基础。在项目开发中,我们需牢记“代理调用”这一关键前提,遵循运用规范,结合实际场景选择合适的解决方案,才能让事务真正成为数据一致性的“守护者”,避免因事务问题引发线上故障。
用户评论