数据库事务的坑:@Transactional注解的隐藏陷阱

0 阅读4分钟

一、问题现场还原

那是一个月黑风高的夜晚,小王正准备下班,突然运营群里炸了:

【运营】重大bug!用户下单成功了,但没扣库存!
【运营】已有多名用户反馈...
【运维】涉及金额已达¥12,580...

小王赶紧打开代码:

@Service
public class OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private InventoryService inventoryService;
    
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(OrderDTO order) {
        // 1. 创建订单
        Order orderEntity = new Order();
        orderEntity.setUserId(order.getUserId());
        orderEntity.setAmount(order.getAmount());
        orderMapper.insert(orderEntity);  // 插入成功
        
        // 2. 扣减库存
        inventoryService.decreaseStock(order.getSkuId(), order.getQuantity()); 
        // ❌ 如果这里抛异常,订单已经插入了,但库存没扣!
        
        // 3. 发送消息
        messageService.sendOrderCreatedMessage(orderEntity.getId());
    }
}

@Service
public class InventoryService {
    
    public void decreaseStock(String skuId, Integer quantity) {
        // 扣减库存逻辑
        // ...
        if (库存不足) {
            throw new RuntimeException("库存不足");
        }
    }
}

问题分析:虽然createOrder加了@Transactional,但inventoryService.decreaseStock()是this调用(自调用),不走Spring的代理,所以事务根本没有生效!

二、原因剖析:Spring事务的代理机制

2.1 Spring事务基于AOP代理

┌─────────────────────────────────────────────────────────┐
│                     Spring IOC容器                      │
├─────────────────────────────────────────────────────────┤
│                                                          │
│   ┌──────────────────┐      ┌──────────────────┐        │
│   │   OrderService    │      │   InventoryService│        │
│   │  ┌────────────┐  │      │                  │        │
│   │  │ createOrder │  │      │                  │        │
│   │  │  @Transactional│      │                  │        │
│   │  └─────┬──────┘  │      │                  │        │
│   │        │          │      │                  │        │
│   │        ▼          │      │                  │        │
│   │  ┌────────────┐  │      │                  │        │
│   │  │  事务代理   │  │      │                  │        │
│   │  │  (AOP)     │  │      │                  │        │
│   │  └────────────┘  │      │                  │        │
│   └────────┬─────────┘      └──────────────────┘        │
│            │                                                  │
│            ▼                                                  │
│   ┌──────────────────┐                                      │
│   │   decreaseStock() │  ← this调用,不经过代理!           │
│   └──────────────────┘                                      │
│                                                          │
└─────────────────────────────────────────────────────────┘

2.2 自调用失效的原因

当我们在同一个类中调用另一个方法时:

public void methodA() {
    this.methodB();  // this.methodB() 不会走代理!
}

Spring的事务是通过AOP代理实现的,只有外部调用才会经过代理,内部调用(自调用) 会直接跳过代理。

2.3 @Transactional失效的场景汇总

场景示例是否生效
自调用this.method()❌ 不生效
private方法@Transactional private method()❌ 不生效
异常被catchtry { } catch { }❌ 不生效
非RuntimeExceptionthrow new Exception()❌ 不生效(默认只回滚RuntimeException)
多数据源未指定两个DataSource⚠️ 需要指定transactionManager

三、解决方案:让事务”生效”

方案一:注入自身(推荐)

@Service
public class OrderService {
    
    @Autowired
    private OrderService self;  // 注入自身
    
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(OrderDTO order) {
        // 1. 创建订单
        orderMapper.insert(orderEntity);
        
        // 2. 扣减库存 - 通过代理调用
        self.decreaseStockInTransaction(order.getSkuId(), order.getQuantity());
        
        // 3. 发送消息
        messageService.sendOrderCreatedMessage(orderEntity.getId());
    }
    
    @Transactional(rollbackFor = Exception.class)
    public void decreaseStockInTransaction(String skuId, Integer quantity) {
        // 扣减库存逻辑
        inventoryMapper.decreaseStock(skuId, quantity);
    }
}

方案二:使用TransactionTemplate

@Service
public class OrderService {
    
    @Autowired
    private TransactionTemplate transactionTemplate;
    
    public void createOrder(OrderDTO order) {
        transactionTemplate.executeWithoutResult(status -> {
            // 1. 创建订单
            orderMapper.insert(orderEntity);
            
            // 2. 扣减库存
            inventoryService.decreaseStock(skuId, quantity);
            
            // 3. 发送消息
            messageService.sendOrderCreatedMessage(orderEntity.getId());
        });
    }
}

方案三:使用AopContext.currentProxy()

@SpringBootApplication(exposeProxy = true)  // 需要开启暴露代理
@EnableAspectJAutoProxy(exposeProxy = true)
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

@Service
public class OrderService {
    
    public void createOrder(OrderDTO order) {
        ((OrderService) AopContext.currentProxy())
            .decreaseStockInTransaction(skuId, quantity);
    }
    
    @Transactional(rollbackFor = Exception.class)
    public void decreaseStockInTransaction(String skuId, Integer quantity) {
        // ...
    }
}

方案四:确保异常能被正确感知

@Transactional(rollbackFor = Exception.class)  // 明确指定回滚条件
public void createOrder(OrderDTO order) {
    try {
        inventoryService.decreaseStock(skuId, quantity);
    } catch (Exception e) {
        log.error("扣减库存失败", e);
        // 不要吞掉异常,否则事务不会回滚
        throw e;  // 重新抛出,或者不catch
    }
}

四、事务传播行为详解

4.1 七种传播行为

public enum Propagation {
    REQUIRED,      // 如果当前有事务,加入该事务(默认)
    REQUIRES_NEW,  // 开启新事务,挂起当前事务
    SUPPORTS,      // 如果有事务,加入事务;没有则以非事务执行
    NOT_SUPPORTED, // 以非事务执行,挂起当前事务
    MANDATORY,     // 必须在事务中执行,否则抛异常
    NEVER,         // 必须在非事务中执行,否则抛异常
    NESTED         // 嵌套事务( Savepoint)
}

4.2 常见场景选择

@Service
public class UserService {
    
    @Autowired
    private AccountService accountService;
    
    @Transactional
    public void registerUser(User user) {
        // 1. 创建用户 - 使用当前事务
        userMapper.insert(user);
        
        // 2. 初始化账户 - 单独事务,失败不影响用户创建
        accountService.initAccountWithNewTransaction(user.getId());
        
        // 3. 发送欢迎邮件 - 非事务,失败不影响主流程
        emailService.sendWelcomeEmail(user.getEmail());  // NOT_SUPPORTED
    }
}

@Service
public class AccountService {
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void initAccountWithNewTransaction(Long userId) {
        // 这个方法会开启新事务
        // 即使这里失败,UserService的事务也不会回滚
        accountMapper.initAccount(userId);
    }
}

@Service
public class EmailService {
    
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void sendWelcomeEmail(String email) {
        // 以非事务执行,失败不影响主流程
    }
}

五、排查工具:事务Debug

5.1 开启事务日志

# application.yml
logging:
  level:
    org.springframework.orm.jpa: DEBUG
    org.springframework.transaction: DEBUG

5.2 使用@Transactional注解剖析

@Configuration
public class TransactionAspectConfig {
    
    @Bean
    public BeanFactoryTransactionAnnotationParser transactionAnnotationParser() {
        return new BeanFactoryTransactionAnnotationParser();
    }
    
    public boolean isTransactional(Method method) {
        Transactional tx = method.getAnnotation(Transactional.class);
        return tx != null;
    }
}

5.3 事务超时配置

@Transactional(timeout = 30)  // 30秒超时
public void createOrder(OrderDTO order) {
    // 如果超过30秒,自动回滚
}

六、预防措施:最佳实践清单

6.1 事务使用检查表

@Transactional使用检查
├── 1. 是否在public方法上?(private方法不生效)
├── 2. 是否是外部调用?(自调用需要通过代理)
├── 3. 异常是否被catch吞掉?
├── 4. 是否指定了rollbackFor?(默认只回滚RuntimeException)
├── 5. 是否有多个数据源?是否指定了transactionManager?
├── 6. 是否需要配置事务超时?
└── 7. 传播行为是否正确?

6.2 编码规范建议

// 建议1:不要在事务方法中进行远程调用
@Transactional
public void createOrder(OrderDTO order) {
    // ❌ 不好:远程调用在事务中,事务时间过长
    remoteService.call();
    
    // ✅ 好:先完成本地事务,再异步调用远程
}

// 建议2:大事务拆分
@Transactional
public void createOrder(OrderDTO order) {
    // 保持事务简短
    orderMapper.insert(order);
}

// 异步执行其他操作
@Async
public void afterOrderCreated(OrderDTO order) {
    messageService.sendMessage(order);
    inventoryService.decreaseStock(order.getSkuId(), order.getQuantity());
}

七、总结

今天我们学到了:

要点说明
问题本质Spring事务基于代理,自调用不走代理
失效场景private方法、自调用、异常被catch、非RuntimeException
解决方案注入自身、TransactionTemplate、AopContext.currentProxy()
传播行为REQUIRED(默认)、REQUIRES_NEW(开启新事务)
最佳实践保持事务简短、避免远程调用在事务中

彩蛋:小王最后用了”注入自身”的方案修复了bug。他在周会上分享经验时说道: “Spring的事务就像高考监考——只有从外部(监考老师)看过去才是有效的。你自己看着自己考试,那不就作弊了吗?”