这块内容前前后后总共写了三篇
1. Spring Boot HikariCP 一 ——集成多数据源
2. Spring Boot 动态切换数据源二——负载均衡
3. Spring Boot 动态切换数据源三——动态获取配置文件中的配置信息
4. 插件GitHubrhettpang/dynamic-datasource
如果仅仅是master-slave模式可以参考我前边的文章Spring Boot HikariCP集成多数据源。
这篇文章也是在那个基础上修改的,上篇文章中的多数据源是有限制的,哪条sql使用哪个数据库必须在代码中写死。现在针对这点做优化,真正的集成多个数据源,且实现简单的负载均衡。
slave:
hosts: slave1,slave2
hikari:
master:
jdbc-url: jdbc:mysql://master_host:3306/mydb?useUnicode=true&characterEncoding=utf8&useSSL=true&allowMultiQueries=true&verifyServerCertificate=false
username: root
password: root
maximum-pool-size: 10
pool-name: master(localhost)
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1765000
data-source-properties:
cachePrepStmts: true
prepStmtCacheSize: 250
prepStmtCacheSqlLimit: 2048
useServerPrepStmts: true
useLocalSessionState: true
useLocalTransactionState: true
rewriteBatchedStatements: true
cacheResultSetMetadata: true
cacheServerConfiguration: true
elideSetAutoCommits: true
maintainTimeStats: false
slave1:
jdbc-url: jdbc:mysql://slave1_host:3306/mydb?useUnicode=true&characterEncoding=utf8&useSSL=true&allowMultiQueries=true&verifyServerCertificate=false
username: root
password: root
maximum-pool-size: 10
pool-name: slave1(localhost)
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1765000
read-only: true
slave2:
jdbc-url: jdbc:mysql://slave2_host:3306/mydb?useUnicode=true&characterEncoding=utf8&useSSL=true&allowMultiQueries=true&verifyServerCertificate=false
username: root
password: root
maximum-pool-size: 10
pool-name: slave2(localhost)
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1765000
read-only: true
注:
1、slave下的data-source-properties:相关配置同master,这里节省篇幅就省了,我这里是公司的项目,没有专门写demo,所以代码不方便上传git,只能贴出来了。
2、slave.hosts这里配置是为了代码中简单的负载均衡用的。
启动类
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@Configuration
@MapperScan(basePackages="com.test.mapper")
public class AuthcenterApplication {
public static void main(String[] args) {
SpringApplication.run(AuthcenterApplication.class, args);
}
}
注:exclude = DataSourceAutoConfiguration.class这里是不让启动加载数据源的,要不然启动会报错。
TargetDataSource
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Created by pangkunkun on 2017/12/18.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TargetDataSource {
//此处接收的是数据源的名称
String value();
}
DBProperties
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @author Created by pangkunkun on 2017/12/18.
*/
@Component
@ConfigurationProperties(prefix = "hikari")
public class DBProperties {
private HikariDataSource master;
private HikariDataSource slave1;
private HikariDataSource slave2;
//省略getter和setter
}
DataSourceConfig
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @author Created by pangkunkun on 2017/12/18.
*/
@Configuration
public class DataSourceConfig {
@Autowired
private DBProperties properties;
@Bean(name = "dataSource")
public DataSource dataSource() {
//按照目标数据源名称和目标数据源对象的映射存放在Map中
Map
注: 这里设置所有配置的数据库
DataSourceAspect
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.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Random;
/**
* @author Created by pangkunkun on 2017/12/18.
*/
@Component
@Aspect
public class DataSourceAspect {
private final static Logger log= LoggerFactory.getLogger(DataSourceAspect.class);
@Value("${slave.hosts}")
private String slaveHosts;
//切换放在mapper接口的方法上,所以这里要配置AOP切面的切入点
@Pointcut("execution( * com.test.mapper.*.*(..))")
public void dataSourcePointCut() {
}
@Before("dataSourcePointCut()")
public void before(JoinPoint joinPoint) {
Object target = joinPoint.getTarget();
String method = joinPoint.getSignature().getName();
Class>[] clazz = target.getClass().getInterfaces();
Class>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameterTypes();
try {
Method m = clazz[0].getMethod(method, parameterTypes);
//如果方法上存在切换数据源的注解,则根据注解内容进行数据源切换
if (m != null && m.isAnnotationPresent(TargetDataSource.class)) {
TargetDataSource data = m.getAnnotation(TargetDataSource.class);
String dataSourceName = data.value();
//判断指定的数据源类型,如果是slave,则调用LB方法,随机分配slave数据库
if (dataSourceName.equals("slave")){
dataSourceName = slaveLoadBalance();
}
DynamicDataSourceHolder.putDataSource(dataSourceName);
log.debug("current thread " + Thread.currentThread().getName() + " add " + dataSourceName + " to ThreadLocal");
} else {
log.debug("switch datasource fail,use default");
}
} catch (Exception e) {
log.error("current thread " + Thread.currentThread().getName() + " add data to ThreadLocal error", e);
}
}
//执行完切面后,将线程共享中的数据源名称清空
@After("dataSourcePointCut()")
public void after(JoinPoint joinPoint){
DynamicDataSourceHolder.removeDataSource();
}
//自己实现的随机指定slave数据源的LB
private String slaveLoadBalance() {
String[] slaves = slaveHosts.split(",");
//通过随机获取数组中数据库的名称来随机分配要使用的数据库
int num = new Random().nextInt(slaves.length);
return slaves[num];
}
}
dataSourcePointCut这里指定切面的生效范围,这里定义的是自己的mapper,我是在mybatis被调用的接口处指定数据源的。
DynamicDataSourceHolder
/**
* @author Created by pangkunkun on 2017/12/18.
*/
public class DynamicDataSourceHolder {
/**
* 本地线程共享对象
*/
private static final ThreadLocal THREAD_LOCAL = new ThreadLocal<>();
public static void putDataSource(String name) {
THREAD_LOCAL.set(name);
}
public static String getDataSource() {
return THREAD_LOCAL.get();
}
public static void removeDataSource() {
THREAD_LOCAL.remove();
}
}
DynamicDataSource
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* @author Created by pangkunkun on 2017/12/18.
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 数据源路由,此方用于产生要选取的数据源逻辑名称
*/
@Override
protected Object determineCurrentLookupKey() {
//从共享线程中获取数据源名称
return DynamicDataSourceHolder.getDataSource();
}
}
还有最后一部分在mapper接口中通过AOP来指定要使用的数据源
import java.util.List;
@Mapper
public interface AuthMapper {
public int save(Auth auth);
@TargetDataSource("slave")
public Auth getById(String Id);
}
Auth 是我自己的实体类。
@TargetDataSource(“slave”)这里指定slave说明是走slave数据库,将会走上边配置的数据源切换。没有这个注解的都是走master数据库。
其实DBProperties和DataSourceConfig这两个类中的代码还可以继续优化,这里写死了数据源的个数,不利于扩展。应该动态加载才对,这个请参考我另外一篇文章Spring Boot 动态切换数据源三——动态获取配置文件中的配置信息。