您现在的位置是:首页 >技术杂谈 >@DS+@Transactional注解切换数据源失效解决方案网站首页技术杂谈
@DS+@Transactional注解切换数据源失效解决方案
背景
项目中使用了MySQL数据库,并按照功能模块采用了分库的策略。因此,一个业务逻辑类中可能涉及多个MySQL数据库的操作。
我们项目中是采用@DS(“xxx”)来实现数据源切换。
当注解添加到类上,意味着此类里的方法都使用此数据源;
当注解添加到方法上时,意味着此方法上使用的数据源优先级高于其他一切配置;
问题分析
代码
- 依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
- yml配置
spring:
datasource:
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
datasource:
master:
url: jdbc:mysql://localhost:3306/demo_01?useSSL=false&autoReconnect=true&characterEncoding=utf8
username: root
password: xxx
driver-class-name: com.mysql.cj.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
slave:
url: jdbc:mysql://172.23.168.70:3306/dynamic?useSSL=false&autoReconnect=true&characterEncoding=utf8
username: root
password: xxx
driver-class-name: com.mysql.cj.jdbc.Driver
- 对象实体
/**
* @author itender
* @date 2023/4/28 11:01
* @desc
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("t_dynamic_template")
public class DynamicTemplateEntity {
@TableId(type = IdType.AUTO)
private Integer id;
/**
* 语言
*/
private String language;
/**
* 语言编码
*/
@TableField("language_code")
private String languageCode;
/**
* 创建时间
*/
@TableField("created_time")
private Date createdTime;
/**
* 创建人
*/
@TableField("created_by")
private Integer createdBy;
/**
* 创建人名称
*/
@TableField("created_by_name")
private String createdByName;
}
/**
* @author itender
* @date 2023/4/28 10:57
* @desc
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@TableName("t_user")
public class UserEntity {
/**
* 主键id
*/
@TableId(type = IdType.AUTO)
private Integer id;
/**
* 用户名称
*/
private String username;
}
- controller代码
/**
* @author itender
* @date 2023/4/28 10:34
* @desc
*/
@RestController
@RequestMapping("template")
public class DynamicTemplateController {
private final DynamicTemplateService dynamicTemplateService;
@Autowired
public DynamicTemplateController(DynamicTemplateService dynamicTemplateService) {
this.dynamicTemplateService = dynamicTemplateService;
}
@GetMapping
public List<DynamicTemplateEntity> list() {
return dynamicTemplateService.list();
}
@PostMapping
public Integer add(@RequestBody DynamicTemplateEntity template) {
return dynamicTemplateService.add(template);
}
}
- service
/**
* @author itender
* @date 2023/4/28 10:36
* @desc
*/
public interface DynamicTemplateService {
/**
* 查询模板集合
*
* @return
*/
List<DynamicTemplateEntity> list();
/**
* 添加模板
*
* @param template
* @return
*/
Integer add(DynamicTemplateEntity template);
}
- mapper
/**
* @author itender
* @date 2023/4/28 11:09
* @desc
*/
@DS("slave")
@Mapper
@Repository
public interface DynamicTemplateMapper extends BaseMapper<DynamicTemplateEntity> {
}
/**
* @author itender
* @date 2023/4/28 11:08
* @desc
*/
@Mapper
@Repository
@DS("master")
public interface UserMapper extends BaseMapper<UserEntity> {
}
- 业务代码
/**
* @author itender
* @date 2023/4/28 11:15
* @desc
*/
@Service
public class DynamicTemplateServiceImpl implements DynamicTemplateService {
private final DynamicTemplateMapper dynamicTemplateMapper;
private final UserMapper userMapper;
private final UserService userService;
@Autowired
public DynamicTemplateServiceImpl(DynamicTemplateMapper dynamicTemplateMapper, UserMapper userMapper, UserService userService) {
this.dynamicTemplateMapper = dynamicTemplateMapper;
this.userMapper = userMapper;
this.userService = userService;
}
@Override
public List<DynamicTemplateEntity> list() {
List<DynamicTemplateEntity> templateList = dynamicTemplateMapper.selectList(new QueryWrapper<>());
if (CollectionUtils.isEmpty(templateList)) {
return Lists.newArrayList();
}
List<UserEntity> userList = userMapper.selectList(new QueryWrapper<>());
if (CollectionUtils.isEmpty(userList)) {
return templateList;
}
Map<Integer, String> userMap = userList.stream().collect(Collectors.toMap(UserEntity::getId, UserEntity::getUsername, (key1, key2) -> key1));
templateList.forEach(template -> template.setCreatedByName(userMap.get(template.getCreatedBy())));
return templateList;
}
@Transactional(rollbackFor = Exception.class)
@Override
public Integer add(DynamicTemplateEntity template) {
List<UserEntity> userList = userMapper.selectList(new QueryWrapper<>());
if (CollectionUtils.isEmpty(userList)) {
template.setCreatedByName("");
}
Map<Integer, String> userMap = userList.stream().collect(Collectors.toMap(UserEntity::getId, UserEntity::getUsername, (key1, key2) -> key1));
template.setCreatedByName(userMap.get(template.getCreatedBy()));
template.setCreatedTime(new Date());
dynamicTemplateMapper.insert(template);
return template.getId();
}
}
测试
- 当方法没有@Transactional注解时,可以正常切换数据源
[
{
"id": 1,
"language": "中文",
"languageCode": "chinese",
"createdTime": "2023-04-27T18:56:25.000+00:00",
"createdBy": 1,
"createdByName": "itender"
}
]
可以正常切换数据源。
- 当方法有@Transactional注解时,切换数据源失败
### Error updating database. Cause: java.sql.SQLSyntaxErrorException: Table 'demo_01.t_dynamic_template' doesn't exist
### The error may exist in com/itender/threadpool/mapper/DynamicTemplateMapper.java (best guess)
### The error may involve com.itender.threadpool.mapper.DynamicTemplateMapper.insert-Inline
### The error occurred while setting parameters
### SQL: INSERT INTO t_dynamic_template ( language, language_code, created_time, created_by, created_by_name ) VALUES ( ?, ?, ?, ?, ? )
### Cause: java.sql.SQLSyntaxErrorException: Table 'demo_01.t_dynamic_template' doesn't exist
; bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException: Table 'demo_01.t_dynamic_template' doesn't exist] with root cause
java.sql.SQLSyntaxErrorException: Table 'demo_01.t_dynamic_template' doesn't exist
分析
-
spring 的@Transactional声明式事务管理时通过动态代理实现的。
-
@DS注解加到mapper接口、service接口、service方法里都不生效,获取的还是默认的主数据源。猜测是由于spring的aop切面机制导致拦截不到@DS注解,进而不能切换数据源,正确的做法是添加到service实现类或者实现类里具体的方法上。
-
在事务方法内调用@DS注解的方法,@DS注解同样不生效,原因是spring只能拦截到最外层方法的@Transactional注解,此时加载该事务的数据源,在事务方法内即使调用了@DS注解的方法,获取的是外层事务的数据源,导致@DS失效。
-
在同一个实现类中,一个非DS注解的常规方法里调用@DS注解的方法,同样存在@DS失效的情况,原因同2,是由spring的aop机制导致的,如果确有这种业务需要,可以将该DS注解方法定义在不同的类中,通过bean注入的方式调用,就不会出现这个问题。
解决方案
- 把查询user的逻辑放到另外一个单独的业务逻辑类里面
/**
* @author itender
* @date 2023/4/28 14:25
* @desc
*/
public interface UserService {
/**
* 查询用户集合
*
* @return
*/
List<UserEntity> list();
}
/**
* @author itender
* @date 2023/4/28 14:27
* @desc
*/
@Service
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
@Autowired
public UserServiceImpl(UserMapper userMapper) {
this.userMapper = userMapper;
}
@DS("master")
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
@Override
public List<UserEntity> list() {
return userMapper.selectList(new QueryWrapper<>());
}
}
- 修改template业务类
@DS("slave")
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
@Override
public Integer add(DynamicTemplateEntity template) {
// List<UserEntity> userList = userMapper.selectList(new QueryWrapper<>());
List<UserEntity> userList = userService.list();
if (CollectionUtils.isEmpty(userList)) {
template.setCreatedByName("");
}
Map<Integer, String> userMap = userList.stream().collect(Collectors.toMap(UserEntity::getId, UserEntity::getUsername, (key1, key2) -> key1));
template.setCreatedByName(userMap.get(template.getCreatedBy()));
template.setCreatedTime(new Date());
dynamicTemplateMapper.insert(template);
return template.getId();
}
测试成功插入一条数据。
总结
-
spring 的@Transactional声明式事务管理时通过动态代理实现的。
-
@DS注解加到mapper接口、service接口、service方法里都不生效,获取的还是默认的主数据源。猜测是由于spring的aop切面机制导致拦截不到@DS注解,进而不能切换数据源,正确的做法是添加到service实现类或者实现类里具体的方法上。
-
在事务方法内调用@DS注解的方法,@DS注解同样不生效,原因是spring只能拦截到最外层方法的@Transactional注解,此时加载该事务的数据源,在事务方法内即使调用了@DS注解的方法,获取的是外层事务的数据源,导致@DS失效。
-
在同一个实现类中,一个非DS注解的常规方法里调用@DS注解的方法,同样存在@DS失效的情况,原因同2,是由spring的aop机制导致的,如果确有这种业务需要,可以将该DS注解方法定义在不同的类中,通过bean注入的方式调用,就不会出现这个问题。