Spring @Transactional 与 @DS 注解使用详解笔记

目录

  1. 问题概述
  2. 问题一:@Transactional 导致 @DS(“slave”) 被忽略
  3. 问题二:同类方法调用导致 @Transactional 失效
  4. 综合代码示例
  5. 总结与最佳实践

1. 问题概述

在使用 Spring 框架进行开发时,经常会同时使用 @Transactional(事务注解)和 @DS(动态数据源注解,如 dynamic-datasource 框架)。这两个注解在特定场景下会产生冲突或失效的问题:

  • 问题一:外层方法使用 @Transactional 开启事务后,内层方法上的 @DS("slave") 会被忽略,无法切换到指定的数据源。
  • 问题二:同一个类中的 A 方法调用 B 方法,B 方法上的 @Transactional 注解会失效。

这两个问题的根本原因都源于 Spring AOP 代理机制。


2. 问题一:@Transactional 导致 @DS(“slave”) 被忽略

2.1 根本原因

@Transactional 的原理是 Spring 通过 AOP 代理在方法执行前后开启和关闭事务。当事务开始时,Spring 会:

  1. 从默认数据源(通常是 master)获取一个数据库连接
  2. 将这个连接绑定到当前线程的 ThreadLocal 中
  3. 整个事务期间的所有数据库操作都复用这个连接

而 @DS 注解的原理也是通过 AOP 代理,在执行数据库操作前动态切换数据源。但当线程已经绑定了一个事务连接时,为了保持事务的一致性,动态数据源框架无法再切换数据源。

核心结论:事务 > 数据源切换。一旦事务开启,数据源就被"锁定"了。

2.2 错误示例代码

@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Autowired
    private UserSlaveService userSlaveService;
    
    /**
     * ❌ 错误示例:外层有事务,内层 @DS("slave") 会失效
     */
    @Transactional  // 使用 master 数据源开启事务,绑定连接
    public void saveAndQuery(Long userId) {
        // 写操作:使用 master 库(正确)
        User user = new User();
        user.setName("张三");
        userMapper.insert(user);
        
        // 读操作:想要切换到 slave 库(实际会失效!)
        // 仍然使用 master 库,因为外层事务已经绑定了 master 连接
        User queriedUser = userSlaveService.queryFromSlave(userId);
        
        System.out.println("查询结果:" + queriedUser);
    }
}

@Service
public class UserSlaveService {
    
    @Autowired
    private UserMapper userMapper;
    
    // 期望切换到 slave 库,但因为上层有事务,这个注解会被忽略
    @DS("slave")
    public User queryFromSlave(Long id) {
        // 实际执行的数据库还是 master
        return userMapper.selectById(id);
    }
}

2.3 解决方案

方案一:使用 REQUIRES_NEW 传播行为(推荐)

通过让内层方法开启一个新事务,挂起外层事务,从而获得独立的数据库连接,实现数据源切换。

@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Autowired
    private UserSlaveService userSlaveService;
    
    @Transactional
    public void saveAndQuery(Long userId) {
        // 写 master
        User user = new User();
        user.setName("张三");
        userMapper.insert(user);
        
        // ✅ 正确:内层方法开启新事务,会挂起外层事务,从而可以切换到 slave
        User queriedUser = userSlaveService.queryFromSlaveWithNewTx(userId);
        
        System.out.println("查询结果:" + queriedUser);
    }
}

@Service
public class UserSlaveService {
    
    @Autowired
    private UserMapper userMapper;
    
    /**
     * ✅ 解决方案:使用 REQUIRES_NEW 传播行为
     * 该方法会开启一个新事务,挂起外层事务,从而可以自由切换数据源
     */
    @DS("slave")
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public User queryFromSlaveWithNewTx(Long id) {
        // 此时处于独立的新事务中,@DS("slave") 可以正常生效
        return userMapper.selectById(id);
    }
}
方案二:使用 NOT_SUPPORTED 传播行为(纯查询场景)

如果内层方法只需要查询,不需要事务,可以将其放在一个非事务环境中执行。

@Service
public class UserSlaveService {
    
    @Autowired
    private UserMapper userMapper;
    
    /**
     * ✅ 纯查询场景:不使用事务,挂起外层事务
     * 适用于只读操作,不涉及数据修改
     */
    @DS("slave")
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public User queryFromSlaveNoTx(Long id) {
        // 没有事务,数据源可以正常切换
        return userMapper.selectById(id);
    }
}
方案三:使用 @DSTransactional(框架支持)

如果项目使用的是 dynamic-datasource 框架,可以使用其提供的专门解决此问题的注解。

@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Autowired
    private UserSlaveService userSlaveService;
    
    /**
     * ✅ 使用动态数据源框架提供的 @DSTransactional
     * 这个注解天然支持多数据源下的分布式事务
     * 注意:这是 dynamic-datasource 提供的注解,不是 Spring 的 @Transactional
     */
    @DSTransactional
    public void saveAndQueryWithDsTx(Long userId) {
        // 写 master
        User user = new User();
        user.setName("张三");
        userMapper.insert(user);
        
        // 读 slave - 可以正常工作
        User queriedUser = userSlaveService.queryFromSlave(userId);
        
        System.out.println("查询结果:" + queriedUser);
    }
}

3. 问题二:同类方法调用导致 @Transactional 失效

3.1 根本原因

Spring AOP 代理机制的局限性。

当类中的方法 A 调用方法 B 时,使用的是 this.methodB(),这里的 this 指向的是原始对象,而不是 Spring 生成的代理对象。因此,方法 B 上的 @Transactional 注解不会被解析和处理,事务不会生效。

调用链路对比:

  • 外部调用(生效):代理对象 → 开启事务 → 调用真实对象的方法
  • 内部调用(失效):真实对象 → 直接调用另一个方法 → 没有代理 → 注解被忽略

3.2 错误示例代码

@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    /**
     * 调用方:没有事务
     */
    public void methodA() {
        // ❌ 问题:直接使用 this 调用,绕过了代理
        // methodB 上的 @Transactional 不会生效!
        this.methodB();
        
        System.out.println("methodA 执行完成");
    }
    
    /**
     * 被调用方:期望有事务
     */
    @Transactional
    public void methodB() {
        // 期望:这个方法应该在事务中执行
        // 实际:由于是通过 this 直接调用,事务不会开启
        userMapper.insert(new User(null, "李四"));
        
        // 如果这里抛出异常,因为没事务,数据不会被回滚
        if (true) {
            throw new RuntimeException("出错了,事务不会回滚");
        }
    }
}

3.3 解决方案

方案一:注入自身代理对象(最常用)

通过 @Autowired 注入自身的代理对象,然后通过代理对象调用方法。

@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    /**
     * ✅ 关键:注入自身的代理对象
     * Spring 会注入一个代理,而不是当前实例
     */
    @Autowired
    private UserService self;  // 注入自身代理
    
    public void methodA() {
        // ✅ 通过代理对象调用,事务会生效
        self.methodB();
        
        System.out.println("methodA 执行完成");
    }
    
    @Transactional
    public void methodB() {
        userMapper.insert(new User(null, "李四"));
        
        // 抛出异常,数据会被回滚(因为事务生效了)
        if (true) {
            throw new RuntimeException("出错了,事务会回滚");
        }
    }
}
方案二:使用 AopContext 获取代理

需要先开启 AspectJ 的代理暴露功能。

// 1. 在配置类或启动类上添加 @EnableAspectJAutoProxy(exposeProxy = true)
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)  // 开启代理暴露
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

// 2. 在 Service 中使用 AopContext 获取代理
@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    public void methodA() {
        // ✅ 通过 AopContext 获取当前类的代理对象
        UserService proxy = (UserService) AopContext.currentProxy();
        proxy.methodB();
        
        System.out.println("methodA 执行完成");
    }
    
    @Transactional
    public void methodB() {
        userMapper.insert(new User(null, "李四"));
        
        if (true) {
            throw new RuntimeException("出错了,事务会回滚");
        }
    }
}
方案三:拆分到不同的 Service 类(结构最清晰)

将需要事务处理的方法拆分到一个独立的 Service 类中。

// 原来的 UserService 只负责调用
@Service
public class UserService {
    
    @Autowired
    private UserTransactionalService txService;  // 注入独立的 Service
    
    public void methodA() {
        // ✅ 调用另一个 Service 的方法,必然经过代理,事务生效
        txService.methodB();
        
        System.out.println("methodA 执行完成");
    }
}

// 新创建的 Service,专门处理事务
@Service
public class UserTransactionalService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Transactional
    public void methodB() {
        userMapper.insert(new User(null, "李四"));
        
        if (true) {
            throw new RuntimeException("出错了,事务会回滚");
        }
    }
}

4. 综合代码示例

以下是一个完整的示例,包含实体类、Mapper 和完整的 Service 层实现:

4.1 实体类和 Mapper

// 实体类
@Data
public class User {
    private Long id;
    private String name;
}

// Mapper 接口
@Mapper
public interface UserMapper {
    
    @Select("SELECT * FROM user WHERE id = #{id}")
    User selectById(Long id);
    
    @Insert("INSERT INTO user(name) VALUES (#{name})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    void insert(User user);
    
    @Delete("DELETE FROM user WHERE id = #{id}")
    void deleteById(Long id);
}

4.2 Service 实现

@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Autowired
    private UserService self;  // 注入自身代理(解决同类调用问题)
    
    @Autowired
    private UserSlaveService userSlaveService;
    
    /**
     * 场景一:写 master,读 slave(需要事务隔离)
     * 解决方案:内层方法使用 REQUIRES_NEW
     */
    @Transactional
    public void saveAndQueryWithTx(Long userId) {
        // 1. 写 master
        User newUser = new User();
        newUser.setName("张三");
        userMapper.insert(newUser);
        System.out.println("插入用户成功,ID: " + newUser.getId());
        
        // 2. 读 slave(通过 REQUIRES_NEW 开启新事务)
        User queriedUser = userSlaveService.queryFromSlaveWithNewTx(userId);
        System.out.println("从 slave 查询结果: " + queriedUser);
    }
    
    /**
     * 场景二:同类方法调用事务方法
     * 解决方案:通过自身代理对象调用
     */
    public void callTransactionalMethod() {
        // 通过代理调用,事务生效
        self.doUpdate();
    }
    
    @Transactional
    public void doUpdate() {
        User user = new User();
        user.setName("李四");
        userMapper.insert(user);
        System.out.println("事务方法执行成功");
    }
}

@Service
public class UserSlaveService {
    
    @Autowired
    private UserMapper userMapper;
    
    /**
     * 从 slave 库查询数据(开启新事务,避免被外层事务锁定)
     */
    @DS("slave")
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public User queryFromSlaveWithNewTx(Long id) {
        return userMapper.selectById(id);
    }
    
    /**
     * 从 slave 库查询数据(不使用事务,适用于只读场景)
     */
    @DS("slave")
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public User queryFromSlaveNoTx(Long id) {
        return userMapper.selectById(id);
    }
}

4.3 测试代码

@SpringBootTest
public class TransactionAndDSTest {
    
    @Autowired
    private UserService userService;
    
    /**
     * 测试事务锁库问题
     */
    @Test
    public void testTransactionLockDataSource() {
        // 执行前确认数据
        System.out.println("=== 测试开始 ===");
        
        // 调用带事务的方法
        userService.saveAndQueryWithTx(1L);
        
        System.out.println("=== 测试结束 ===");
        // 观察日志,确认是否成功切换到 slave 库
    }
    
    /**
     * 测试同类调用事务失效问题
     */
    @Test
    public void testSameClassCall() {
        System.out.println("=== 测试开始 ===");
        
        try {
            // 通过 methodA 间接调用 doUpdate
            userService.callTransactionalMethod();
        } catch (Exception e) {
            System.out.println("捕获异常: " + e.getMessage());
        }
        
        // 检查数据库:
        // - 如果事务生效:数据不会插入
        // - 如果事务失效:数据会被插入
        System.out.println("=== 测试结束,请检查数据库 ===");
    }
}

4.4 配置文件示例

# application.yml
spring:
  datasource:
    dynamic:
      primary: master  # 默认数据源
      datasource:
        master:
          url: jdbc:mysql://localhost:3306/master_db
          username: root
          password: 123456
        slave:
          url: jdbc:mysql://localhost:3306/slave_db
          username: root
          password: 123456

# 开启动态数据源日志(用于调试)
logging:
  level:
    com.baomidou.dynamic.datasource: debug

5. 总结与最佳实践

5.1 问题总结

问题场景根本原因推荐解决方案
@Transactional 导致 @DS 失效事务连接被绑定到线程,无法切换数据源内层方法使用 Propagation.REQUIRES_NEW
同类调用导致 @Transactional 失效直接调用原始对象,绕过了 AOP 代理注入自身代理对象或拆分 Service

5.2 最佳实践建议

  1. 多数据源事务处理

    • 尽量将读操作和写操作分离
    • 写操作使用 master 库事务,读操作使用 slave 库(不加事务或使用 NOT_SUPPORTED)
    • 必须同时操作多个数据源时,考虑使用 @DSTransactional
  2. 避免同类调用

    • 不要在一个 Service 内部直接调用自己的 @Transactional 方法
    • 使用 @Autowired private XxxService self 注入自身代理
    • 或者将有事务的方法拆分到独立的 Service 类中
  3. 事务传播行为选择

    传播行为适用场景
    REQUIRED(默认)大多数业务场景
    REQUIRES_NEW需要独立事务(如日志记录、数据源切换)
    NOT_SUPPORTED纯查询、不需要事务的场景
    SUPPORTS有事务则加入,无事务则以非事务方式执行
  4. 注解使用规范

    • @DS 和 @Transactional 同时使用时,注意事务传播行为
    • 建议在类级别标注 @DS,方法级别标注 @Transactional
    • 使用 @DSTransactional 可以更好地解决多数据源事务问题

5.3 快速排查清单

遇到事务或数据源切换失效时,按以下顺序排查:

  • 是否在同一个类中直接调用(this.method())?
  • 外层是否有 @Transactional 导致数据源被锁定?
  • 内层方法是否使用了合适的传播行为(REQUIRES_NEW)?
  • 是否开启了 AOP 代理(@EnableAspectJAutoProxy)?
  • 是否正确注入了代理对象而非原始对象?
  • 是否开启了动态数据源的 debug 日志查看切换情况?
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐