前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Springboot环境中多个DataSource基于自定义注解进行切换使用过程

Springboot环境中多个DataSource基于自定义注解进行切换使用过程

作者头像
冬天里的懒猫
发布2021-09-26 16:23:35
1.6K0
发布2021-09-26 16:23:35
举报

前面配置了mysql数据库的主从复制模式,在数据库上实现了master-slave配置,通过这种方式可以实现一主一从,或者一主多从,从而提升系统的高可用。 这是数据库层面的实现。在数据库实现了主从模式之后,我们需要考率的问题就是,在我们的应用代码中,如何将不同的数据库操作按需要分配到不同的数据库去执行。

1.需要的依赖

代码语言:javascript
复制
    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文件中,数据源相关配置如下:

代码语言:javascript
复制
# 自定义的动态数据源配置
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.动态数据源配置

数据源定义:

代码语言:javascript
复制
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();
	}
}

动态数据源切面配置:

代码语言:javascript
复制
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();
	}
}

数据源切换处理

代码语言:javascript
复制
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);
	}
}

数据源注册类

代码语言:javascript
复制
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));
	}

}

定义一个注解,在使用数据源的时候通过注解进行配置:

代码语言:javascript
复制
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)

代码语言:javascript
复制
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层即可使用:

代码语言:javascript
复制
	@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进行调用:

代码语言:javascript
复制
	@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);
	}
asyncQueryByKey使用master请求
asyncQueryByKey使用master请求
queryByKey 通过slave数据源查询
queryByKey 通过slave数据源查询

查询日志:

代码语言:javascript
复制
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
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-09-18 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.需要的依赖
  • 2.yml配置
  • 3.动态数据源配置
  • 4.使用说明
    • 4.1 启动类配置
      • 4.2 Service层注解的使用
      相关产品与服务
      数据库
      云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档