准备2台mysql8数据库
192.168.18.111 mysql8
192.168.18.253 mysql8
用druid配置连接池,AOP实现负载均衡(轮询,用redis存放数据库集群数量下标)mysql数据库集群

springboot druid 负载均衡 mysql集群

pom.xml

        <!-- jdbc -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.4</version>
        </dependency>
        <!-- aop -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!--集成redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--        druid连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.9</version>
        </dependency>

application.yml

spring:
  redis:
    timeout: 15000
    #单机
    host: localhost
    port: 6379
    password:
  #数据库
  datasource:
    username: x
    password: x
    driver-class-name: com.mysql.cj.jdbc.Driver
    #druid连接池
    type: com.alibaba.druid.pool.DruidDataSource
    #设置成双机热备数据库最好
    mysql1:
      url: jdbc:mysql://192.168.18.111:3306/demo?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai
      # 初始连接数
      initial-size: 10
      # 最小连接池数量
      min-idle: 10
      # 最大连接池数量
      max-active: 100
      # 配置获取连接等待超时的时间
      max-wait: 6000
      # 打开PSCache,并且指定每个连接上PSCache的大小
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 20
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      minEvictableIdleTimeMillis: 30000
      # 配置检测连接是否有效
      validationQuery: SELECT 1
      #申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      keepAlive: true

    mysql2:
      url: jdbc:mysql://192.168.18.253:3306/demo?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai
      # 初始连接数
      initial-size: 10
      # 最小连接池数量
      min-idle: 10
      # 最大连接池数量
      max-active: 100
      # 配置获取连接等待超时的时间
      max-wait: 6000
      # 打开PSCache,并且指定每个连接上PSCache的大小
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 20
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      minEvictableIdleTimeMillis: 30000
      # 配置检测连接是否有效
      validationQuery: SELECT 1
      #申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      keepAlive: true
    #监控信息配置
    monitor:
      username: root
      password: root

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.fu.demo.entity

DemoApplication.java启动类

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

@MapperScan("com.fu.demo.mapper")
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})//取消自动配置数据库
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

DynamicDataSourceContextHolder.java

public class DynamicDataSourceContextHolder {
    /**
     * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
     *  所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
     */
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    /**
     * 设置数据源的变量
     */
    public static void setDateSourceType(String dsType) {
        CONTEXT_HOLDER.set(dsType);
    }

    /**
     * 获得数据源的变量
     */
    public static String getDateSourceType() {
        return CONTEXT_HOLDER.get();
    }

    /**
     * 清空数据源变量
     */
    public static void clearDateSourceType() {
        CONTEXT_HOLDER.remove();
    }
}

DynamicDataSource.java

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.Map;

public class DynamicDataSource extends AbstractRoutingDataSource {

    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        this.setDefaultTargetDataSource(defaultTargetDataSource);
        this.setTargetDataSources(targetDataSources);
        this.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDateSourceType();
    }
}

DruidConfig.java

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;


@Configuration
public class DruidConfig {
    /**
     * 配置别名
     */
    @Value("${mybatis.type-aliases-package}")
    private String typeAliasesPackage;

    /**
     * 配置mapper的扫描,找到所有的mapper.xml映射文件
     */
    @Value("${mybatis.mapper-locations}")
    private String mapperLocations;

    /**
     * 获取druid监控信息
     * */
    @Value("${spring.datasource.monitor.username}")
    private String username;

    @Value("${spring.datasource.monitor.password}")
    private String password;

    @Bean
    @ConfigurationProperties("spring.datasource.mysql1")
    public DataSource mysql1DataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.mysql2")
    public DataSource mysql2DataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "dynamicDataSource")
    @Primary
    public DynamicDataSource dataSource() {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("mysql1", mysql1DataSource());
        targetDataSources.put("mysql2", mysql2DataSource());
        return new DynamicDataSource(mysql1DataSource(), targetDataSources);
    }

    /**
     * SqlSessionFactory 配置并放入容器中
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sqlSessionFactoryBean.setTypeAliasesPackage(typeAliasesPackage);
        return sqlSessionFactoryBean.getObject();
    }

    /**
     * 事务
     */
    @Bean
    public PlatformTransactionManager transactionManager(@Qualifier("dynamicDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }


    /**
     * 连接池监控
     * */
    @Bean
    public ServletRegistrationBean druidServlet() {
        ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
        servletRegistrationBean.addInitParameter("loginUsername", username);
        servletRegistrationBean.addInitParameter("loginPassword", password);
        servletRegistrationBean.addInitParameter("resetEnable", "false");
        return servletRegistrationBean;
    }

    @Bean
    public FilterRegistrationBean druidFilter() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter());
        filterRegistrationBean.addUrlPatterns("/*");
        filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*");
        return filterRegistrationBean;
    }

}

DataSourceAspect.java

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

@Aspect
@Order(1)
@Component
public class DataSourceAspect {
    @Resource
    private RedisTemplate<String,Integer> redisTemplate;

    @Around("execution(public * com.fu.demo.mapper.*.*(..))")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        //轮询数据库
        String INDEX = "index";
        if (!redisTemplate.hasKey(INDEX)) redisTemplate.opsForValue().set(INDEX, 0);//如果key不存在就创建key,并设置下标初始值为0
        int index = redisTemplate.opsForValue().get(INDEX);//有下标则直接获取
        List<String> list = new ArrayList<>();
        list.add("mysql1");
        list.add("mysql2");
        if (index >= list.size()) index = 0;//超过list集合的值就重新赋值(轮询)
        DynamicDataSourceContextHolder.setDateSourceType(list.get(index));
        redisTemplate.opsForValue().set(INDEX, ++index);//利用redis单线程的特性存放全局index下标
        try {
            return point.proceed();
        } finally {
            // 销毁数据源 在执行方法之后
            DynamicDataSourceContextHolder.clearDateSourceType();
        }
    }
}

演示
192.168.18.111(本机)
在这里插入图片描述
192.168.18.253(主要修改了名称
在这里插入图片描述
启动springboot项目访问测试接口,如:localhost:81/user/select?id=1
第一次
在这里插入图片描述
第二次
在这里插入图片描述
第三次
在这里插入图片描述
到此springboot负载均衡mysql数据库已经实现

题外

2台数据库没有双机热备,因此数据不一致。要解决这个问题只要把2台服务器的mysql做成双机热备集群即可。
《mysql双机热备、互为主从集群》
因为有负载均衡了,因此可以不用nginx实现高可用了。
当redis单机宕机时会影响mysql负载均衡,因此redis建议也搭成集群。
《redis 集群搭建 分布式锁》
至此mysql负载均衡、高可用均已实现,但当项目宕机时,用户仍然无法使用。因此要保证springboot项目高可用、负载均衡…
至此。。。你会发现你的项目要改成springcloud微服务了。
如果mysql数据库压力不大,可以只做高可用。
在这里插入图片描述
如果数据库压力大可以弄双击热备mysql集群,负载均衡。
在这里插入图片描述
压力巨大,建议分库分表、读写分离了。
没图了,我摊牌了,我不会了!

优化:实现高可用、负载均衡

application.yml
把druid的max-wait改成1毫秒,这样宕机一台,马上可以换另外一台

      # 配置获取连接等待超时的时间
      max-wait: 1

DataSourceAspect.java
把轮询放到外面去,这样就不用写2次。catch MyBatisSystemException捕获到连接异常。我只是简单的捕获,实际开发当中应该捕获具体的异常。

	@Around("execution(public * com.fu.demo.mapper.*.*(..))")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        try {
            robin();//轮询数据库
            return point.proceed();
        } catch (MyBatisSystemException e) {
            robin();//连接不上就再轮询一次,获取另外一个mysql数据库连接
            return point.proceed();
        } finally {
            // 销毁数据源 在执行方法之后
            DynamicDataSourceContextHolder.clearDateSourceType();
        }
    }

    /**
     * 轮询mysql数据库
     */
    public void robin(){
        String INDEX = "index";
        if (!redisTemplate.hasKey(INDEX)) redisTemplate.opsForValue().set(INDEX, 0);//如果key不存在就创建key,并设置下标初始值为0
        int index = redisTemplate.opsForValue().get(INDEX);//有下标则直接获取
        List<String> list = new ArrayList<>();
        list.add("mysql1");
        list.add("mysql2");
        if (index >= list.size()) index = 0;//超过list集合的值就重新赋值(轮询)
        DynamicDataSourceContextHolder.setDateSourceType(list.get(index));
        redisTemplate.opsForValue().set(INDEX, ++index);//利用redis单线程的特性存放全局index下标
    }

这样,就算关掉其中一台数据库,也是能立马响应的,只是控制台会不断抛出异常。这个留给你们自己优化了。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐