前面配置了mysql数据库的主从复制模式,在数据库上实现了master-slave配置,通过这种方式可以实现一主一从,或者一主多从,从而提升系统的高可用。 这是数据库层面的实现。在数据库实现了主从模式之后,我们需要考率的问题就是,在我们的应用代码中,如何将不同的数据库操作按需要分配到不同的数据库去执行。
1.需要的依赖
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'mysql:mysql-connector-java:8.0.25'
implementation 'com.zaxxer:HikariCP:4.0.3'
2.yml配置
在application.yml文件中,数据源相关配置如下:
# 自定义的动态数据源配置
custom:
datasource:
- key: master
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.161.114:3306/gts?useSSL=false&autoReconnect=true&characterEncoding=UTF-8&serverTimezone=UTC
username: gts
password: mysql
default: true
- key: slave1
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.161.115:3306/gts?useSSL=false&autoReconnect=true&characterEncoding=UTF-8&serverTimezone=UTC
username: gts
password: mysql
- key: slave2
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.161.114:3306/gts?useSSL=false&autoReconnect=true&characterEncoding=UTF-8&serverTimezone=UTC
username: gts
password: mysql
在custom.datasource,定义了一组数据源配置。master配置到主库,slave1喝slave2分贝表示多组从库。本文用主库来表示从库。
3.动态数据源配置
数据源定义:
package com.dhb.gts.javacourse.week7.dynamic;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceType();
}
}
动态数据源切面配置:
package com.dhb.gts.javacourse.week7.dynamic;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
@Order(-1)// 保证该AOP在@Transactional之前执行
@Component
@Slf4j
public class DynamicDataSourceAspect {
@Before("@annotation(ds)")
public void changeDataSource(JoinPoint point, TargetDataSource ds) throws Throwable {
String dsId = ds.name();
if (!DynamicDataSourceContextHolder.containsDataSource(dsId)) {
log.error("数据源[{}]不存在,使用默认数据源 > {}", ds.name(), point.getSignature());
}else {
log.info("Use DataSource : {} > {}", dsId, point.getSignature());
DynamicDataSourceContextHolder.setDataSourceType(dsId);
}
}
@After("@annotation(ds)")
public void restoreDataSource(JoinPoint point, TargetDataSource ds) {
log.debug("Revert DataSource : {} > {}", ds.name(), point.getSignature());
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
数据源切换处理
package com.dhb.gts.javacourse.week7.dynamic;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class DynamicDataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static List<String> dataSourceIds = new ArrayList<>();
public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
log.info("dataSourceType set is {}",dataSourceType);
}
public static String getDataSourceType() {
return contextHolder.get();
}
public static void clearDataSourceType() {
contextHolder.remove();
}
/**
* 判断指定DataSrouce当前是否存在
*/
public static boolean containsDataSource(String dataSourceId){
return dataSourceIds.contains(dataSourceId);
}
}
数据源注册类
package com.dhb.gts.javacourse.week7.dynamic;
import com.google.common.base.Strings;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationPropertyNameAliases;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {
// 如配置文件中未指定数据源类型,使用该默认值
private static final Object DATASOURCE_TYPE_DEFAULT = "com.zaxxer.hikari.HikariDataSource";
/**
* 数据源参数配置别名
*/
private final static ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases();
static {
//由于部分数据源配置不同,所以在此处添加别名,避免切换数据源出现某些参数无法注入的情况
aliases.addAliases("url", "jdbc-url");
aliases.addAliases("username", "user");
}
/**
* 参数绑定工具
*/
private Binder binder;
/**
* 配置上下文(也可以理解为配置文件的获取工具)
*/
private Environment env;
// 默认数据源
private DataSource defaultDataSource;
/**
* 自定义数据源
*/
private Map<String, DataSource> customDataSources = new HashMap<>();
public DataSource buildDataSource(Map<String, Object> dsMap) {
try {
Object type = dsMap.get("type");
if (type == null) {
// 默认DataSource
type = DATASOURCE_TYPE_DEFAULT;
}
Class<? extends DataSource> dataSourceType;
dataSourceType = (Class<? extends DataSource>) Class.forName((String) type);
String driverClassName = dsMap.get("driver-class-name").toString();
String url = dsMap.get("url").toString();
String username = dsMap.get("username").toString();
String password = dsMap.get("password").toString();
DataSourceBuilder factory = DataSourceBuilder.create().driverClassName(driverClassName).url(url)
.username(username).password(password).type(dataSourceType);
return factory.build();
} catch (Throwable e) {
log.error("buildDataSource failed!", e);
}
return null;
}
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
// 将主数据源添加到更多数据源中
targetDataSources.put("dataSource", defaultDataSource);
DynamicDataSourceContextHolder.dataSourceIds.add("dataSource");
// 添加更多数据源
targetDataSources.putAll(customDataSources);
// 创建DynamicDataSource
DynamicDataSourceContextHolder.dataSourceIds.addAll(customDataSources.keySet());
//bean定义类
GenericBeanDefinition define = new GenericBeanDefinition();
//设置bean的类型,此处DynamicDataSource是继承AbstractRoutingDataSource的实现类
define.setBeanClass(DynamicDataSource.class);
//需要注入的参数,类似spring配置文件中的<property/>
MutablePropertyValues mpv = define.getPropertyValues();
//添加默认数据源,避免key不存在的情况没有数据源可用
mpv.add("defaultTargetDataSource", defaultDataSource);
//添加其他数据源
mpv.add("targetDataSources", targetDataSources);
//将该bean注册为datasource,不使用spring-boot自动生成的datasource
registry.registerBeanDefinition("datasource", define);
log.info("Dynamic DataSource Registry success !");
}
@Override
public void setEnvironment(Environment environment) {
this.env = environment;
//绑定配置器
binder = Binder.get(env);
initCustomDataSources();
}
private void initCustomDataSources() {
// 读取配置文件获取更多数据源,也可以通过defaultDataSource读取数据库获取更多数据源
List<Map> configs = binder.bind("custom.datasource", Bindable.listOf(Map.class)).get();
String dsPrefix;
DataSource custom;
for (Map config : configs) {
String key = config.get("key").toString();
if (!Strings.isNullOrEmpty(key)) {
dsPrefix = key;
} else {
dsPrefix = "default";
}
custom = buildDataSource(config);
customDataSources.put(dsPrefix, custom);
dataBinder(custom, config);
//如果 default标识为true,则将其设置为defaultDataSource
if (null != config.get("default") && "true".equals(config.get("default").toString())) {
defaultDataSource = custom;
}
}
//如果default数据源没有,将master设置为default的数据源。
if (null == defaultDataSource) {
defaultDataSource = customDataSources.get("master");
}
}
private void dataBinder(DataSource dataSource, Map properties) {
ConfigurationPropertySource source = new MapConfigurationPropertySource(properties);
Binder binderEx = new Binder(source.withAliases(aliases));
//将参数绑定到对象
binderEx.bind(ConfigurationPropertyName.EMPTY, Bindable.ofInstance(dataSource));
}
}
定义一个注解,在使用数据源的时候通过注解进行配置:
package com.dhb.gts.javacourse.week7.dynamic;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 自定义一个注解,在方法上使用,用于指定使用哪个数据源
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
String name();
}
4.使用说明
4.1 启动类配置
需要import定义的DynamicDataSourceRegister。 @Import({DynamicDataSourceRegister.class}) 另外需要开启切面。 @EnableAspectJAutoProxy(proxyTargetClass=true)
package com.dhb.gts.javacourse.week7;
import com.dhb.gts.javacourse.week7.dynamic.DynamicDataSourceRegister;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Import;
@SpringBootApplication(scanBasePackages = {"com.dhb.gts.javacourse.fluent.dao","com.dhb.gts.javacourse.week7"} )
@MapperScan(basePackages = {"com.dhb.gts.javacourse.fluent.mapper"})
@Import({DynamicDataSourceRegister.class})
@EnableAspectJAutoProxy(proxyTargetClass=true)
public class Starter {
public static void main(String[] args) {
SpringApplication.run(Starter.class, args);
}
}
4.2 Service层注解的使用
现在将自定义的注解,配置到Service层即可使用:
@Async
@TargetDataSource(name = "master")
public ListenableFuture<OrderSummaryEntity> asyncQueryOrderById(int order_id){
OrderSummaryEntity entity = orderSummaryDao.selectById(order_id);
return new AsyncResult<>(entity);
}
@TargetDataSource(name = "slave1")
public OrderSummaryEntity queryOrderById(int order_id){
return orderSummaryDao.selectById(order_id);
}
通过Controller进行调用:
@RequestMapping("/asyncQueryByKey")
public String asyncQueryByKey(String key) {
Stopwatch stopwatch = Stopwatch.createStarted();
Integer orde_id = Integer.parseInt(key);
OrderSummaryEntity entity = null;
try {
entity = orderService.asyncQueryOrderById(orde_id).get();
}catch (Exception e) {
e.printStackTrace();
}
stopwatch.stop();
log.info("通过key查询,走索引耗时:" + stopwatch);
return JSON.toJSONString(entity);
}
@RequestMapping("/queryByKey")
public String queryByKey(String key) {
Stopwatch stopwatch = Stopwatch.createStarted();
Integer orde_id = Integer.parseInt(key);
OrderSummaryEntity entity = orderService.queryOrderById(orde_id);
stopwatch.stop();
log.info("通过key查询,走索引耗时:" + stopwatch);
return JSON.toJSONString(entity);
}
查询日志:
2021-09-18 19:13:03.406 INFO 18368 --- [async-service-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2021-09-18 19:13:03.504 INFO 18368 --- [async-service-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2021-09-18 19:13:03.560 INFO 18368 --- [async-service-1] c.d.g.j.week7.service.OrderService : 通过线程池插入完成,共耗时:196.5 ms
2021-09-18 19:13:39.022 INFO 18368 --- [async-service-2] c.d.g.j.w.d.DynamicDataSourceAspect : Use DataSource : master > ListenableFuture com.dhb.gts.javacourse.week7.service.OrderService.asyncQueryOrderById(int)
2021-09-18 19:13:39.022 INFO 18368 --- [async-service-2] d.g.j.w.d.DynamicDataSourceContextHolder : dataSourceType set is master
2021-09-18 19:13:39.031 INFO 18368 --- [nio-8084-exec-4] c.d.g.j.w.controller.OrderController : 通过key查询,走索引耗时:14.56 ms
2021-09-18 19:15:17.534 INFO 18368 --- [nio-8084-exec-5] c.d.g.j.w.d.DynamicDataSourceAspect : Use DataSource : slave1 > OrderSummaryEntity com.dhb.gts.javacourse.week7.service.OrderService.queryOrderById(int)
2021-09-18 19:15:17.535 INFO 18368 --- [nio-8084-exec-5] d.g.j.w.d.DynamicDataSourceContextHolder : dataSourceType set is slave1
2021-09-18 19:15:17.535 INFO 18368 --- [nio-8084-exec-5] com.zaxxer.hikari.HikariDataSource : HikariPool-2 - Starting...
2021-09-18 19:15:17.553 INFO 18368 --- [nio-8084-exec-5] com.zaxxer.hikari.HikariDataSource : HikariPool-2 - Start completed.
2021-09-18 19:15:17.556 INFO 18368 --- [nio-8084-exec-5] c.d.g.j.w.controller.OrderController : 通过key查询,走索引耗时:21.28 ms
|