声明式事务与编程式事务

候选人小蒋在面试蚂蚁 P7 时,面试官问道:

"你用过编程式事务吗?和声明式事务有什么区别?"

小蒋说:"用过 TransactionTemplate..."

面试官追问:"什么场景下你选择编程式事务而不是声明式?"

小蒋说:"呃...好像大部分时候都是声明式..."

面试官继续追问:"那你为什么要用编程式?"

小蒋答不上来。

【面试官心理】 这道题我用来测试候选人对事务的全面理解。只用声明式事务的人很多,但能说出编程式事务价值和适用场景的人很少。能说出"需要细粒度控制"、"需要在事务外执行"、"需要复用事务逻辑"等场景的,基本都有复杂业务经验。


一、核心问题 🔴

1.1 问题拆解

第一层:概念

  • "什么是声明式事务?什么是编程式事务?"
  • "两者在代码写法上有什么区别?"

第二层:对比

  • "声明式事务和编程式事务各有什么优缺点?"
  • "Spring 默认用的是哪种?"

第三层:场景选择

  • "什么场景下应该用编程式事务?"
  • "什么场景下用声明式事务就够了?"

第四层:实现细节

  • "TransactionTemplate 是怎么工作的?"
  • "PlatformTransactionManager 是什么?"

1.2 ❌ 错误示范

候选人原话 A:"声明式事务就是用 @Transactional,编程式事务就是用 TransactionTemplate。"

问题诊断

  • 知道表面区别,但不理解本质
  • 说不清各自的适用场景

候选人原话 B:"编程式事务性能更好,因为不需要 AOP 代理。"

问题诊断

  • 错误理解,编程式事务也有开销
  • 两种方式的性能差异不是主要区别

候选人原话 C:"声明式事务更好,所以永远不用编程式。"

问题诊断

  • 不知道编程式事务的价值
  • 不理解复杂业务场景的需求

1.3 标准回答

P5 回答:两种实现方式

声明式事务:通过 @Transactional 注解声明,由 Spring AOP 代理在方法前后自动管理事务。

编程式事务:通过 TransactionTemplatePlatformTransactionManager 手动控制事务边界。

// 声明式事务(@Transactional)
@Service
public class OrderService {
    @Transactional
    public void createOrder(Order order) {
        orderRepository.save(order);
        paymentService.process(order.getPayment());
        inventoryService.deduct(order.getItems());
    }
}

// 编程式事务(TransactionTemplate)
@Service
public class OrderService {
    @Autowired
    private TransactionTemplate transactionTemplate;

    public void createOrder(Order order) {
        transactionTemplate.executeWithoutResult(status -> {
            orderRepository.save(order);
            paymentService.process(order.getPayment());
            inventoryService.deduct(order.getItems());
        });
    }
}

// 编程式事务(PlatformTransactionManager)
@Service
public class OrderService {
    @Autowired
    private PlatformTransactionManager transactionManager;

    public void createOrder(Order order) {
        TransactionDefinition def = new DefaultTransactionDefinition();
        TransactionStatus status = transactionManager.getTransaction(def);

        try {
            orderRepository.save(order);
            paymentService.process(order.getPayment());
            inventoryService.deduct(order.getItems());
            transactionManager.commit(status);
        } catch (Exception e) {
            transactionManager.rollback(status);
            throw e;
        }
    }
}

1.4 追问升级

追问 1:优缺点对比

对比维度声明式事务编程式事务
代码侵入低,注解方式高,需要显式调用
灵活性一般,受限于注解高,完全控制
复用性低,每个方法都要标注高,可封装逻辑
异常处理自动(受限于 rollbackFor)手动,完全控制
适用场景简单 CRUD 方法复杂事务逻辑
调试难度较难(涉及代理)较易(代码直观)
Spring 推荐✅ 是仅复杂场景使用

追问 2:编程式事务的典型场景

场景一:需要在事务外执行某些操作

@Service
public class UserService {
    @Autowired
    private TransactionTemplate transactionTemplate;

    public void registerUser(User user) {
        // 1. 保存用户(需要事务)
        User saved = transactionTemplate.execute(status -> {
            return userRepository.save(user);
        });

        // 2. 发送欢迎邮件(不需要事务,且不应该在事务中)
        // 因为如果事务回滚,邮件已经发出去了
        emailService.sendWelcomeEmail(saved.getEmail());
    }
}
⚠️

这里有个陷阱:如果 emailService.sendWelcomeEmail() 在事务外执行,而事务在后续逻辑中回滚了,用户没创建成功但邮件已经发出去了。正确做法是使用 TransactionSynchronization.afterCommit()

public void registerUser(User user) {
    User saved = userRepository.save(user); // 保存用户

    // 注册事务提交后的回调
    TransactionSynchronizationManager.registerSynchronization(
        new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                emailService.sendWelcomeEmail(saved.getEmail());
            }
        }
    );
}

场景二:需要细粒度控制事务

@Service
public class DataImportService {
    @Autowired
    private TransactionTemplate transactionTemplate;

    public void importBatch(List<Data> dataList) {
        for (Data data : dataList) {
            try {
                transactionTemplate.executeWithoutResult(status -> {
                    importData(data);
                });
            } catch (Exception e) {
                log.error("导入失败: " + data.getId(), e);
                // 单条失败不影响其他条
            }
        }
    }
}

场景三:需要复用事务逻辑

@Service
public class TransactionHelper {
    @Autowired
    private TransactionTemplate transactionTemplate;

    // 封装事务逻辑
    public <T> T executeInTransaction(Supplier<T> action) {
        return transactionTemplate.execute(status -> {
            return action.get();
        });
    }

    public void executeInTransaction(Runnable action) {
        transactionTemplate.executeWithoutResult(status -> action.run());
    }
}

@Service
public class OrderService {
    @Autowired
    private TransactionHelper helper;

    public void createOrder() {
        helper.executeInTransaction(() -> {
            orderRepository.save(order);
            inventoryService.deduct();
        });
    }
}

追问 3:TransactionTemplate 的实现原理

// TransactionTemplate 继承自 DefaultTransactionDefinition
public class TransactionTemplate extends DefaultTransactionDefinition {
    private final PlatformTransactionManager transactionManager;

    @Override
    public <T> T execute(TransactionCallback<T> action) {
        // 1. 获取事务
        TransactionStatus status = transactionManager.getTransaction(this);

        try {
            // 2. 执行业务逻辑
            T result = action.doInTransaction(status);
            // 3. 正常返回,提交事务
            if (!status.isRollbackOnly()) {
                transactionManager.commit(status);
            }
            return result;
        } catch (Throwable ex) {
            // 4. 异常,判断是否回滚
            if (rollbackWhen(ex)) {
                transactionManager.rollback(status);
            } else {
                transactionManager.commit(status);
            }
            throw ex;
        }
    }

    private boolean rollbackWhen(Throwable ex) {
        // 根据 rollbackOn 判断
        return (ex instanceof RuntimeException || ex instanceof Error);
    }
}

PlatformTransactionManager 接口:

public interface PlatformTransactionManager {
    // 获取事务
    TransactionStatus getTransaction(TransactionDefinition definition);

    // 提交事务
    void commit(TransactionStatus status);

    // 回滚事务
    void rollback(TransactionStatus status);
}

常见的实现类:

  • DataSourceTransactionManager:JDBC/MyBatis
  • JpaTransactionManager:JPA/Hibernate
  • HibernateTransactionManager:Hibernate
  • JtaTransactionManager:分布式事务(JTA)

二、延伸问题 🟡

2.1 Spring Boot 中的声明式事务

Spring Boot 自动配置了事务管理器,不需要手动配置:

// Spring Boot 自动配置
// DataSourceTransactionManagerAutoConfiguration
@Bean
@ConditionalOnMissingBean(PlatformTransactionManager.class)
public DataSourceTransactionManager transactionManager(DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}

// JpaBaseConfiguration
@Bean
@ConditionalOnMissingBean(PlatformTransactionManager.class)
public JpaTransactionManager transactionManager() {
    return new JpaTransactionManager();
}

多数据源时需要手动配置:

@Configuration
public class TransactionManagerConfig {
    @Primary
    @Bean
    public DataSourceTransactionManager transactionManager1(DataSource dataSource1) {
        return new DataSourceTransactionManager(dataSource1);
    }

    @Bean
    public DataSourceTransactionManager transactionManager2(DataSource dataSource2) {
        return new DataSourceTransactionManager(dataSource2);
    }
}

// 使用时指定
@Service
public class MyService {
    @Transactional("transactionManager2")
    public void methodWithDs2() {
        // 使用 dataSource2
    }
}

2.2 声明式事务的局限

// 局限1:无法获取事务状态
@Service
public class OrderService {
    @Transactional
    public void createOrder() {
        // ❌ 无法知道当前是否在事务中
        // 无法动态决定是否开启新事务
    }
}

// 编程式可以
public void createOrder() {
    if (TransactionSynchronizationManager.isActualTransactionActive()) {
        // 已经有事务
    }
    transactionTemplate.execute(status -> {
        // 可以动态标记回滚
        status.setRollbackOnly();
    });
}

// 局限2:无法在事务外执行
@Service
public class OrderService {
    @Transactional
    public void createOrder() {
        // ❌ 所有代码都在事务中
        // 如果远程调用失败,整个事务都要回滚
        paymentService.remoteCall(); // 耗时操作,不应该在事务中
    }
}

// 编程式可以
public void createOrder() {
    // 事务外执行远程调用
    String result = paymentService.remoteCall(); // 不占事务

    transactionTemplate.executeWithoutResult(status -> {
        // 事务内执行本地操作
        orderRepository.save(order);
    });
}

三、生产避坑

3.1 混用两种事务的问题

@Service
public class BadService {
    @Autowired
    private TransactionTemplate transactionTemplate;

    @Transactional  // 声明式事务
    public void methodA() {
        // 这里是事务1
        transactionTemplate.executeWithoutResult(status -> {
            // ❌ 在声明式事务中又开启了一个编程式事务
            // 编程式事务可能是新的事务(REQUIRES_NEW)
            // 可能导致不一致
        });
    }
}

3.2 编程式事务的资源泄漏

@Service
public class BadService {
    @Autowired
    private PlatformTransactionManager transactionManager;

    public void badMethod() {
        TransactionStatus status = transactionManager.getTransaction(null);
        try {
            // 业务逻辑
        } finally {
            // ❌ 如果忘记调用 commit 或 rollback
            // 会导致连接泄漏
            // 或者导致事务超时
        }
    }
}

// ✅ 正确做法:使用 TransactionTemplate
@Service
public class GoodService {
    @Autowired
    private TransactionTemplate transactionTemplate;

    public void goodMethod() {
        transactionTemplate.executeWithoutResult(status -> {
            // 业务逻辑
            // 即使抛出异常,TransactionTemplate 也会处理
        });
    }
}

四、工程选型

4.1 选择指南

场景推荐方式原因
普通 CRUD 方法声明式简单,一目了然
需要在事务外执行编程式可以精确控制边界
复杂异常处理编程式可以精细控制回滚条件
需要复用事务逻辑编程式(封装后)抽取公共逻辑
需要获取事务状态编程式可以检查 isActualTransactionActive
大数据量批量处理编程式可以控制每批的大小

4.2 最佳实践

// 推荐:封装编程式事务为工具类
@Component
public class TransactionExecutor {
    @Autowired
    private TransactionTemplate transactionTemplate;

    public <T> T execute(Supplier<T> action) {
        return transactionTemplate.execute(status -> action.get());
    }

    public void executeVoid(Runnable action) {
        transactionTemplate.executeWithoutResult(status -> action.run());
    }

    // 带超时控制
    public <T> T execute(Supplier<T> action, int timeoutSeconds) {
        transactionTemplate.setTimeout(timeoutSeconds);
        return execute(action);
    }
}

// 使用
@Service
public class OrderService {
    @Autowired
    private TransactionExecutor executor;

    public void createOrder() {
        executor.executeVoid(() -> {
            orderRepository.save(order);
        });
    }
}

五、面试总结

声明式 vs 编程式事务,不是"谁更好"的问题,而是"谁更适合"的问题。

P5 候选人能说出两种写法的区别。 P6 候选人能说出各自的优缺点,能说出简单的场景选择。 P7 候选人能说出编程式事务的典型场景(事务外执行、细粒度控制),能说出混用的问题和最佳实践。

记住,大多数场景声明式事务就够了,但遇到复杂业务时,编程式事务是你的好帮手。