Spring 动态数据源-主从数据库
Spring内置了一个AbstractRoutingDataSource,它可以把多个数据源配置成一个Map,根据不同的key返回不同的数据源。因为AbstractRoutingDataSource也是一个DataSource接口,因此,应用程序可以先设置好key, 访问数据库的代码就可以从AbstractRoutingDataSource拿到对应的一个真实的数据源,从而访问指定的数据库。
配置数据源
spring:
master-datasource:
username: root
password: 123456;a
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost/test_master
hikari:
pool-name: HikariCP
auto-commit: false
slave-datasource:
username: root
password: 123456;a
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost/test_slave
hikari:
pool-name: HikariCP
auto-commit: false
mybatis:
mapper-locations: classpath:mapper/*.xml
数据源Config
@Slf4j
@Configuration
public class DataSourceConfig {
/**
* Master data source.
*/
@Bean("masterDataSource")
@ConfigurationProperties(prefix = "spring.master-datasource")
public DataSource masterDataSource() {
log.info("create master datasource...");
return DataSourceBuilder.create().build();
}
/**
* Slave (read only) data source.
*/
@Bean("slaveDataSource")
@ConfigurationProperties(prefix = "spring.slave-datasource")
public DataSource slaveDataSource() {
log.info("create slave datasource...");
return DataSourceBuilder.create().build();
}
}
继承AbstractRoutingDataSource 重写determineCurrentLookupKey
把两个真实的数据源代理为一个动态数据源:
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return "masterDataSource";
}
}
配置主数据源
@Bean
@Primary
DataSource primaryDataSource(
@Autowired @Qualifier("masterDataSource") DataSource masterDataSource,
@Autowired @Qualifier("slaveDataSource") DataSource slaveDataSource
) {
log.info("create routing datasource...");
Map<Object, Object> map = new HashMap<>();
map.put("masterDataSource", masterDataSource);
map.put("slaveDataSource", slaveDataSource);
RoutingDataSource routing = new RoutingDataSource();
routing.setTargetDataSources(map);
routing.setDefaultTargetDataSource(masterDataSource);
return routing;
}
RoutingDataSource配置好了,但是,路由的选择是写死的,即永远返回"masterDataSource"
存储动态选择的key
编写一个RoutingDataSourceContext,来设置并动态存储key
public class RoutingDataSourceContext implements AutoCloseable {
// holds data source key in thread local:
static final ThreadLocal<String> threadLocalDataSourceKey = new ThreadLocal<>();
public static String getDataSourceRoutingKey() {
String key = threadLocalDataSourceKey.get();
return !StringUtils.hasText(key) ? "masterDataSource" : key;
}
public RoutingDataSourceContext(String key) {
threadLocalDataSourceKey.set(key);
}
public void close() {
threadLocalDataSourceKey.remove();
}
}
修改RoutingDataSource,获取key的代码如下
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return RoutingDataSourceContext.getDataSourceRoutingKey();
}
}
在某个地方,例如一个Controller的方法内部,就可以动态设置DataSource的Key
@Controller
public class MyController {
@Get("/")
public String index() {
String key = "slaveDataSource";
try (RoutingDataSourceContext ctx = new RoutingDataSourceContext(key)) {
// TODO:
}
}
}
注解aop动态选择数据源
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
切面代码
@Aspect
@Component
public class DataSourceAop {
@Around("@annotation(sourceWith)")
public Object routingWithDataSource(ProceedingJoinPoint joinPoint, sourceWith sourceWith) throws Throwable {
String key = sourceWith.value();
if (StringUtils.hasText(key)) {
key += "DataSource";
}
try (RoutingDataSourceContext ctx = new RoutingDataSourceContext(key)) {
return joinPoint.proceed();
}
}
}
对应的注解代码
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface sourceWith {
String value() default "master";
}
使用
在对应的controller或者dao,service上添加注解
示例:
@Mapper
public interface UserDao {
@sourceWith(value = "slave")
User getById(Integer id);
@sourceWith(value = "master")
Integer insert(User user);
}
注意:受Servlet线程模型的局限,动态数据源不能在一个请求内设定后再修改,也就是@sourceWith 不能嵌套。此外,@sourceWith@Transactional混用时,要设定AOP的优先级