从数据库读取数据源信息,然后实现动态切换数据源
实现的步骤
1 创建数据源实体类
2 获取数据源缓存。读取数据库,获取数据源信息,生成druidDataSource实体,放入Map中 key是数据源名称,value是对应数据源druidDataSource实体
3 明确本线程的数据源 创建线程本地变量ThreadLocal 存放线程的key
4 创建db注解 放在需要的方法上 例如 @Db(“master”)
5 切库 使用Aop在方法执行前通过ThreaLocal的key,替换注解的value,实现切库。
1 创建数据源实体类
@ApiModelProperty("注册在spring中bean名字")
@TableId("dbname")
private String dbname;
@ApiModelProperty("数据库IP")
@TableField("databaseip")
private String databaseip;
@ApiModelProperty("数据库端口")
@TableField("databaseport")
private String databaseport;
@ApiModelProperty("数据库名称")
@TableField("databasename")
private String databasename;
@ApiModelProperty("数据库用户名")
@TableField("username")
private String username;
@ApiModelProperty("数据库密码")
@TableField("password")
private String password;
2 获取数据源缓存。读取数据库,获取数据源信息,生成如druidDataSource的实体,放入Map中 key是数据源名称,value是对应数据源druidDataSource实体
@Component
@Slf4j
public class DataSourceCache implements InitializingBean {
public static final Map<String, DataSource> DATA_SOURCE_MAP = new HashMap<>();
@Autowired
GlobalDatasourceEntityMapper globalDatasourceEntityMapper;
@Autowired
DynamicDataSource dynamicDataSource;
@Override
public void afterPropertiesSet() throws Exception {
initDataSourceCache();
}
// 初始化数据源
public void initDataSourceCache() {
try {
LambdaQueryWrapper<GlobalDatasourceEntity> queryWrapper = new LambdaQueryWrapper<GlobalDatasourceEntity>()
.isNotNull(GlobalDatasourceEntity::getDbname)
.isNotNull(GlobalDatasourceEntity::getDatabaseip)
.isNotNull(GlobalDatasourceEntity::getDatabasename)
.isNotNull(GlobalDatasourceEntity::getDatabaseport)
.isNotNull(GlobalDatasourceEntity::getUsername)
.isNotNull(GlobalDatasourceEntity::getPassword);
List<GlobalDatasourceEntity> globalDatasourceEntities = globalDatasourceEntityMapper.selectList(queryWrapper);
globalDatasourceEntities.forEach(globalDatasourceEntity -> {
String url = "jdbc:mysql://"+globalDatasourceEntity.getDatabaseip()+":"+globalDatasourceEntity.getDatabaseport()+"/"+globalDatasourceEntity.getDatabasename()+"?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true";
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
druidDataSource.setUrl(url);
druidDataSource.setUsername(globalDatasourceEntity.getUsername());
druidDataSource.setPassword(globalDatasourceEntity.getPassword());
DATA_SOURCE_MAP.put(globalDatasourceEntity.getDbname(),druidDataSource);
});
setTargetDataSources(DATA_SOURCE_MAP);
log.info("dataSource.size = "+DATA_SOURCE_MAP.entrySet().size());
}catch (Exception e) {
log.error("init dataSource error: {}",e.getMessage());
}
}
private void setTargetDataSources(Map<String, DataSource> dataSourceMap) {
try {
//将当前的数据源与数据源名称绑定
HashMap<Object, Object> targetDataSources = new HashMap<>();
for (Map.Entry<String, DataSource> entry : DATA_SOURCE_MAP.entrySet()) {
targetDataSources.put(entry.getKey(), entry.getValue());
}
dynamicDataSource.setTargetDataSources(targetDataSources);
}catch (Exception e) {
e.printStackTrace();
log.error("绑定数据源出错 " + e.getMessage());
}
}
}
实现抽象路由数据源 重写方法setTargetDataSources方法
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.get();//实现切换
}
// 数据源放入
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
super.setTargetDataSources(targetDataSources);
// 同步到resolvedDataSources中
super.afterPropertiesSet();
}
}
将DynamicDataSource 存入spring中
@Configuration
public class DataSourceConfig {
@Bean(name = "masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource")//读取配置文件中的数据源信息当作默认数据源
public DataSource masterDataSource() {
return DataSourceBuilder.create()
.type(DruidDataSource.class)
.build();
}
/**
* 动态数据源
*/
@Bean
@Primary
public DynamicDataSource dynamicDataSource(Environment env) {
DynamicDataSource dynamicRoutingDataSource = new DynamicDataSource();
dynamicRoutingDataSource.setDefaultTargetDataSource(masterDataSource());
dynamicRoutingDataSource.setTargetDataSources(new HashMap<>());
return dynamicRoutingDataSource;
}
}
3 明确本线程的数据源 创建线程本地变量ThreadLocal 存放线程的key
public class DataSourceContextHolder {
// 存放数据源的key
private static ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void set(String dbType) {
contextHolder.set(dbType);
}
public static String get() {
return contextHolder.get();
}
public static void clear() {
contextHolder.remove();
}
}
4 创建注解 放在需要的方法上 例如 @Db(“master”)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Db {
String value() default "masterDataSource";
}
@Db("shuiyi")
public Result getDataAllForTime(String dbname,Integer num) {
....
}
5 切库 使用Aop在方法执行前通过ThreaLocal的key,替换注解的value,实现切库。
@Component
@Aspect
public class DbAop {
@Around("@annotation(db)")
public Object around(ProceedingJoinPoint point, Db db) throws Throwable {
try {
Object[] args = point.getArgs();
if(args.length > 0) {
String dbname = (String)args[0];
DataSourceContextHolder.set(dbname);
}
return point.proceed();
} finally {
DataSourceContextHolder.clear();
}
}
}
ok
原理
分两步:装载数据源,切换数据源
1 设置多数据源:将数据源信息存入数据库,实现InitializingBean,重写afterPropertiesSet方法,在方法中调用AbstractRoutingDataSource类(2继承了这个类)中setTargetDataSource方法设置多数据源。
2 继承AbstractRoutingDataSource类,然后重写determineCurrentLookupKey(确定当前关键字)方法。返回对应数据库设置的key,这个key放在TreadLocal中,每一个线程都可以使用不同的数据源。