面试题:说一下Spring的事务传播特性

一、核心概念

事务传播行为,指的是当一个事务方法被另一个事务方法调用时,这个方法应该如何参与事务的规则 。Spring 提供了 7 种传播行为,通过 @Transactional(propagation = Propagation.XXX) 来指定。


二、7 种传播行为详解(附代码示例)

1. REQUIRED(默认传播行为)
  • 规则:如果当前存在事务,就加入该事务;如果当前没有事务,就创建一个新事务。

  • 适用场景:绝大多数业务场景,保证方法在事务中执行。

  • 代码示例

    @Service
    public class OrderService {
    @Autowired
    private OrderDao orderDao;
    @Autowired
    private ProductService productService;

    复制代码
      // 外层方法:无事务时创建新事务
      @Transactional(propagation = Propagation.REQUIRED)
      public void createOrder(Order order) {
          orderDao.save(order);
          // 调用内层方法:加入当前事务
          productService.reduceStock(order.getProductId(), order.getCount());
      }

    }

    @Service
    public class ProductService {
    @Autowired
    private ProductDao productDao;

    复制代码
      // 内层方法:默认REQUIRED,会加入外层事务
      @Transactional(propagation = Propagation.REQUIRED)
      public void reduceStock(Long productId, Integer count) {
          productDao.updateStock(productId, count);
          // 若此处抛异常,外层事务也会回滚
          if (count < 0) {
              throw new RuntimeException("库存不足");
          }
      }

    }

  • 说明 :如果 createOrder 先执行(无事务),会创建新事务;reduceStock 调用时会加入这个事务。若 reduceStock 抛异常,整个事务回滚,订单和库存操作都会撤销。


2. REQUIRES_NEW
  • 规则:无论当前是否存在事务,都创建一个新事务;如果当前存在事务,将当前事务挂起。

  • 适用场景:需要独立事务的场景(如日志记录、第三方通知,即使主事务回滚,日志也要保留)。

  • 代码示例

    @Service
    public class OrderService {
    @Autowired
    private OrderDao orderDao;
    @Autowired
    private LogService logService;

    复制代码
      @Transactional(propagation = Propagation.REQUIRED)
      public void createOrder(Order order) {
          orderDao.save(order);
          // 调用日志方法:创建新事务
          logService.recordLog("创建订单:" + order.getId());
          // 模拟异常,主事务回滚
          throw new RuntimeException("创建订单失败");
      }

    }

    @Service
    public class LogService {
    @Autowired
    private LogDao logDao;

    复制代码
      @Transactional(propagation = Propagation.REQUIRES_NEW)
      public void recordLog(String content) {
          Log log = new Log();
          log.setContent(content);
          logDao.save(log);
      }

    }

  • 说明createOrder 抛异常回滚,但 recordLog 是独立新事务,会正常提交,日志会被保存,不受主事务影响。


3. SUPPORTS
  • 规则:如果当前存在事务,就加入该事务;如果当前没有事务,就以非事务方式执行。

  • 适用场景:不需要强制事务的辅助方法(如查询类方法,有事务时共享事务,无事务时正常执行)。

  • 代码示例

    @Service
    public class OrderService {
    @Autowired
    private OrderDao orderDao;
    @Autowired
    private QueryService queryService;

    复制代码
      @Transactional(propagation = Propagation.REQUIRED)
      public void updateOrder(Order order) {
          orderDao.update(order);
          // 调用查询方法:加入当前事务
          queryService.getOrderDetail(order.getId());
      }

    }

    @Service
    public class QueryService {
    @Autowired
    private OrderDao orderDao;

    复制代码
      @Transactional(propagation = Propagation.SUPPORTS)
      public Order getOrderDetail(Long orderId) {
          // 有事务时,查询在事务中;无事务时,直接查询
          return orderDao.selectById(orderId);
      }

    }

  • 说明 :如果 updateOrder 有事务,getOrderDetail 会加入事务;如果直接调用 getOrderDetail(无事务),则非事务执行。


4. NOT_SUPPORTED
  • 规则:以非事务方式执行;如果当前存在事务,将当前事务挂起。

  • 适用场景:不需要事务支持的操作(如纯查询、缓存更新,避免事务锁影响性能)。

  • 代码示例

    @Service
    public class CacheService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    复制代码
      @Transactional(propagation = Propagation.NOT_SUPPORTED)
      public void updateCache(String key, Object value) {
          // 非事务执行,即使外层有事务也会被挂起
          redisTemplate.opsForValue().set(key, value);
      }

    }

  • 说明 :如果外层方法有事务,调用 updateCache 时会挂起外层事务,执行完后再恢复外层事务。


5. MANDATORY
  • 规则:如果当前存在事务,就加入该事务;如果当前没有事务,就抛出异常。

  • 适用场景:必须在事务中执行的核心业务方法(确保方法不会在无事务环境下执行)。

  • 代码示例

    @Service
    public class PaymentService {
    @Autowired
    private PaymentDao paymentDao;

    复制代码
      @Transactional(propagation = Propagation.MANDATORY)
      public void pay(Payment payment) {
          paymentDao.save(payment);
      }

    }

    // 测试:无事务调用 MANDATORY 方法
    @Service
    public class TestService {
    @Autowired
    private PaymentService paymentService;

    复制代码
      // 无事务注解
      public void testMandatory() {
          // 调用 pay 时,因为无事务,会抛出 IllegalTransactionStateException
          paymentService.pay(new Payment());
      }

    }

  • 说明testMandatory 无事务,调用 pay(MANDATORY)会直接抛异常,确保 pay 必须在事务中执行。


6. NESTED
  • 规则:如果当前存在事务,就在嵌套事务中执行;如果当前没有事务,就创建一个新事务。嵌套事务是外层事务的子事务,可独立提交 / 回滚,但外层回滚时子事务也会回滚。

  • 适用场景:需要部分回滚的场景(如批量操作,某条失败回滚该条,不影响其他)。

  • 代码示例

    @Service
    public class BatchService {
    @Autowired
    private ItemDao itemDao;

    复制代码
      @Transactional(propagation = Propagation.REQUIRED)
      public void batchInsert(List<Item> items) {
          for (Item item : items) {
              try {
                  insertItem(item);
              } catch (Exception e) {
                  // 子事务回滚,不影响外层事务
                  System.out.println("插入失败:" + item.getName() + ",仅回滚该条");
              }
          }
      }
    
      @Transactional(propagation = Propagation.NESTED)
      public void insertItem(Item item) {
          itemDao.save(item);
          if (item.getPrice() < 0) {
              throw new RuntimeException("价格非法");
          }
      }

    }

  • 说明batchInsert 是外层事务,insertItem 是嵌套事务。若某条 item 价格非法,insertItem 回滚,但外层事务继续执行其他插入,最终外层提交时所有成功的插入都会生效。


7. NEVER
  • 规则:以非事务方式执行;如果当前存在事务,就抛出异常。

  • 适用场景:绝对不允许在事务中执行的方法(如某些性能敏感的纯查询,避免事务锁)。

  • 代码示例

    @Service
    public class FastQueryService {
    @Autowired
    private DataDao dataDao;

    复制代码
      @Transactional(propagation = Propagation.NEVER)
      public List<Data> fastQuery() {
          // 非事务执行,若外层有事务则抛异常
          return dataDao.selectAll();
      }

    }

    // 测试:有事务调用 NEVER 方法
    @Service
    public class TestService {
    @Autowired
    private FastQueryService fastQueryService;

    复制代码
      @Transactional
      public void testNever() {
          // 调用 fastQuery 时,因为外层有事务,会抛出 IllegalTransactionStateException
          fastQueryService.fastQuery();
      }

    }

  • 说明testNever 有事务,调用 fastQuery(NEVER)会直接抛异常,确保方法不在事务中执行。


三、关键补充

  • 传播行为的生效依赖 Spring AOP 代理,因此同类方法内部调用不会触发传播行为(需通过代理对象调用)。
  • 嵌套事务(NESTED)依赖数据库的保存点(Savepoint)功能,并非所有数据库都支持。

保存点(Savepoint)是什么?

保存点是数据库事务中的一个标记点 ,作用是让你在事务执行过程中,只回滚到这个标记点,而不是回滚整个事务

举个例子:假设你在一个事务里执行了三步操作:

  1. 插入订单记录
  2. 扣减商品库存
  3. 生成支付流水

你在第 2 步执行完后设置了一个保存点 sp1。如果第 3 步失败了,你可以选择只回滚到 sp1,这样 "插入订单" 和 "扣减库存" 的操作会保留,只有 "生成支付流水" 被撤销,然后你可以修正第 3 步的逻辑继续执行,最后整个事务提交。

如果没有保存点,一旦第 3 步失败,整个事务(1+2+3)都会被回滚,之前的操作全部作废。


为什么 Spring 的 NESTED 嵌套事务依赖保存点?

Spring 的 NESTED 传播行为,本质是外层事务的子事务,它的核心特性是:

  • 内层方法可以独立提交 / 回滚,但外层事务回滚时,内层也会跟着回滚
  • 内层回滚时,不会影响外层事务的其他部分

这个特性完全依赖数据库的 Savepoint 实现:

  1. 当进入内层 NESTED 方法时,Spring 会通过 JDBC 向数据库发送 SAVEPOINT sp_name 命令,设置一个保存点。
  2. 如果内层方法正常执行,Spring 会释放这个保存点。
  3. 如果内层方法抛出异常,Spring 会执行 ROLLBACK TO SAVEPOINT sp_name,只回滚到保存点的位置,而不是回滚整个外层事务。
  4. 外层事务可以继续执行其他逻辑,最终一起提交。

如果数据库不支持 Savepoint,Spring 就无法创建这个标记点,NESTED 的 "独立回滚" 特性会失效 ------ 内层异常会直接导致整个外层事务回滚,达不到嵌套事务的预期效果。


哪些数据库不支持 Savepoint?

主流的生产级数据库大多支持 Savepoint,但一些轻量级、嵌入式或较老的数据库可能不支持或支持有限:

数据库类型 Savepoint 支持情况
MySQL(InnoDB) ✅ 支持
PostgreSQL ✅ 支持
Oracle ✅ 支持
SQL Server ✅ 支持
SQLite ⚠️ 3.6.8 之后支持,但部分场景有局限
HSQLDB(早期版本) ❌ 不支持
Derby(某些模式) ⚠️ 支持但有性能限制
Access / FoxPro ❌ 不支持
一些小众嵌入式数据库 ❌ 不支持

开发中的注意事项

  1. 如果用了不支持 Savepoint 的数据库

    • 不要使用 NESTED 传播行为,否则会导致事务行为异常。
    • 可以改用 REQUIRES_NEW(完全独立的新事务)来实现类似 "部分回滚" 的效果,但注意 REQUIRES_NEW 会创建独立事务,外层回滚不会影响内层提交。
  2. 即使数据库支持 Savepoint

    • 要确保 JDBC 驱动版本支持 Savepoint API(JDBC 3.0 及以上才支持)。
    • 有些数据库对嵌套 Savepoint(一个事务里设置多个保存点)的支持有限,需要测试验证。
相关推荐
TDengine (老段)2 小时前
TDengine ODBC 连接器进阶指南
大数据·数据库·物联网·时序数据库·tdengine·涛思数据
Cliven_2 小时前
Github自动打包推送Maven中央仓库
java·github·maven
indexsunny2 小时前
互联网大厂Java求职面试实战:Spring Boot微服务与Kafka消息队列解析
java·spring boot·微服务·面试·kafka·jpa
invicinble2 小时前
关于spring的全量认识
java·spring
齐 飞2 小时前
JDK8中stream中常用方法
java
菩提小狗2 小时前
Sqli-Labs Less4:双引号字符型 SQL 注入详解|靶场|网络安全
数据库·sql·web安全
小旭95272 小时前
【Java 基础】泛型<T>
java·开发语言·intellij-idea
她说..2 小时前
FIND_IN_SET()方法
xml·java·spring boot
花间相见2 小时前
【JAVA开发】—— Maven核心用法与实战指南
java·python·maven