Spring @Transactional 与 @DS 注解使用详解笔记
Spring @Transactional 与 @DS 注解使用详解笔记
目录
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 会:
- 从默认数据源(通常是 master)获取一个数据库连接
- 将这个连接绑定到当前线程的 ThreadLocal 中
- 整个事务期间的所有数据库操作都复用这个连接
而 @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 最佳实践建议
-
多数据源事务处理
- 尽量将读操作和写操作分离
- 写操作使用 master 库事务,读操作使用 slave 库(不加事务或使用
NOT_SUPPORTED) - 必须同时操作多个数据源时,考虑使用
@DSTransactional
-
避免同类调用
- 不要在一个 Service 内部直接调用自己的
@Transactional方法 - 使用
@Autowired private XxxService self注入自身代理 - 或者将有事务的方法拆分到独立的 Service 类中
- 不要在一个 Service 内部直接调用自己的
-
事务传播行为选择
传播行为 适用场景 REQUIRED(默认)大多数业务场景 REQUIRES_NEW需要独立事务(如日志记录、数据源切换) NOT_SUPPORTED纯查询、不需要事务的场景 SUPPORTS有事务则加入,无事务则以非事务方式执行 -
注解使用规范
@DS和@Transactional同时使用时,注意事务传播行为- 建议在类级别标注
@DS,方法级别标注@Transactional - 使用
@DSTransactional可以更好地解决多数据源事务问题
5.3 快速排查清单
遇到事务或数据源切换失效时,按以下顺序排查:
- 是否在同一个类中直接调用(
this.method())? - 外层是否有
@Transactional导致数据源被锁定? - 内层方法是否使用了合适的传播行为(
REQUIRES_NEW)? - 是否开启了 AOP 代理(
@EnableAspectJAutoProxy)? - 是否正确注入了代理对象而非原始对象?
- 是否开启了动态数据源的 debug 日志查看切换情况?
更多推荐


所有评论(0)