Spring Boot 动态切换数据源二——负载均衡

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_35981283/article/details/82117779

这块内容前前后后总共写了三篇
1. Spring Boot HikariCP 一 ——集成多数据源
2. Spring Boot 动态切换数据源二——负载均衡
3. Spring Boot 动态切换数据源三——动态获取配置文件中的配置信息

如果仅仅是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<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master", properties.getMaster());
        targetDataSources.put("slave1", properties.getSlave1());
        targetDataSources.put("slave2", properties.getSlave2());
        //采用是想AbstractRoutingDataSource的对象包装多数据源
        DynamicDataSource dataSource = new DynamicDataSource();
        dataSource.setTargetDataSources(targetDataSources);
        //设置默认的数据源,当拿不到数据源时,使用此配置
        dataSource.setDefaultTargetDataSource(properties.getMaster());
        return dataSource;
    }

    @Bean
    public PlatformTransactionManager txManager() {
        return new DataSourceTransactionManager(dataSource());
    }
}

注: 这里设置所有配置的数据库

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<String> 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 动态切换数据源三——动态获取配置文件中的配置信息

猜你喜欢

转载自blog.csdn.net/qq_35981283/article/details/82117779
今日推荐