在上一篇,介绍了docker搭建mysql主从复制集群 docker搭建mysql主从复制

读写分离的方案也可通过中间件代理,如mysql-proxy,mycat。
通过中间件代理,可以很好的做到负载均衡,以及自动故障切换,高可用性
这里用另一种方式,springboot通过aop和druid来实现mybatis的多数据源设置,从而实现读写分离

druid

Druid是阿里巴巴开源的一个数据源,主要用于java数据库连接池,相比spring推荐的DBCP和hibernate推荐的C3P0、Proxool数据库连接池,Druid在市场上占有绝对的优势;

为什么选择Druid作为数据库连接池?

文章从市场占有率、性能上比较C3P0、DBCP、HikariCP和Druid,说明了Druid数据源由于有强大的监控特性、可拓展性等特点值得作者推荐。虽说 HikariCP 的性能比 Druid 高,但是因为 Druid 包括很多维度的统计和分析功能,所以大家都选择使用Druid 的更多;

下面直接贴代码

springboot项目结构

durid 配置postgresql数据源_spring boot

pom.xml

需要加入的依赖,直接贴代码

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!--devtools热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
            <scope>true</scope>
        </dependency>

        <!--=========mybatis plus======================-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!-- 引入 Druid 数据源依赖:https://mvnrepository.com/artifact/com.alibaba/druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.9</version>
        </dependency>

        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--StringUtils工具包-->
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>

springboot启动类

package com.example.ms;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class MsApplication {

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

启动类的注解上加入:(exclude = DataSourceAutoConfiguration.class)
该注解的作用是,排除自动注入数据源的配置(取消数据库配置),一般使用在客户端(消费者)服务中,这样就去掉了spring boot 默认的mybatis 自动配置

至于为什么要做这个设置呢,下面详细做出解释:

exclude,排除此类的AutoConfig,即禁止 SpringBoot 自动注入数据源配置,怎么讲?
DataSourceAutoConfiguration.class 会自动查找 application.yml 或者 properties 文件里的
spring.datasource.* 相关属性并自动配置单数据源「注意这里提到的单数据源」。
那么问题来了,排除了自动配置,Spring还怎么识别到数据库配置呢?
答:显然接下来就需要手动配置,what?那我为什么要排除?然后手动指定数据源?
如果你发现项目中存在这个排除的骚操作,可以在项目中搜一下Java关键字@ConfigurationProperties("spring.datasource ,你可能会发现手动配置数据源的类。
再来回答为何要手动配置数据源,因为要配置多数据源,上边有提到DataSourceAutoConfiguration.class默认会帮我们自动配置单数据源,所以,如果想在项目中使用多数据源就需要排除它,手动指定多数据源

MyDatabase数据源实体类

package com.example.ms.pojo;
import lombok.Data;

@Data
public class MyDatabase {
        //数据库连接url
        private  String url;
        //数据库连接用户名
        private  String username;
        //数据库连接密码
        private  String password;
        //数据库是主or从
        private  String type;
}

application.yml

spring:
  devtools:
    restart:
      enabled: true  #设置开启热部署
      additional-paths: src/main/java #重启目录
      exclude: WEB-INF/**   #排除项,无须自动重启,但是会重新加载。
  freemarker:
    cache: false    #页面不加载缓存,修改即时生效
  thymeleaf:
    cache: true   #即页面修改后会立即生效  true


mybatis-plus:
  # 如果是放在src/main/java目录下 classpath:/com/yourpackage/*/mapper/*Mapper.xml
  mapper-locations: classpath:mybatis/*Mapper.xml
  typeAliasesPackage: com.example.ms.pojo
  global-config:
    db-config:
      #id-type: uuid
      #字段策略 0:"忽略判断",1:"非 NULL 判断"),2:"非空判断"
      field-strategy: 1
      #驼峰下划线转换
      #db-column-underline: true
      #刷新mapper 调试神器
      #refresh-mapper: true
      #数据库大写下划线转换
      #capital-mode: true
      # Sequence序列接口实现类配置
      #key-generator: com.baomidou.mybatisplus.incrementer.OracleKeyGenerator
      #逻辑删除配置(下面3个配置)
      #logic-delete-value: 1
      #logic-not-delete-value: 0
      #sql-injector: com.baomidou.mybatisplus.mapper.LogicSqlInjector
      #自定义填充策略接口实现
      #meta-object-handler: com.baomidou.springboot.MyMetaObjectHandler
  configuration:
    #配置返回数据库(column下划线命名&&返回java实体是驼峰命名),自动匹配无需as(没开启这个,SQL需要写as: select user_id as userId)
    map-underscore-to-camel-case: false
    cache-enabled: false
    #配置JdbcTypeForNull, oracle数据库必须配置
    jdbc-type-for-null: 'null'
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    database-id: mysql

注意,这里就不需要配置datasource数据源了

multidatabase.properties 数据源配置文件

#下面的main  和 read 数据将会以轮询的方式 被 访问。
#mian 循环main
#read 循环read  具体代码查看DataSourceAOP

#自定义多数据源配置
my.datasource.driver=com.mysql.jdbc.Driver

#  main 代表主服务器 可读写   read  = 只读
my.datasource[0].type=main
my.datasource[0].url=jdbc:mysql://192.168.0.201:3306/test
my.datasource[0].username=root
my.datasource[0].password=zaqxsw

my.datasource[1].type=read
my.datasource[1].url=jdbc:mysql://192.168.0.202:3306/test
my.datasource[1].username=root
my.datasource[1].password=zaqxsw

my.datasource[2].type=read
my.datasource[2].url=jdbc:mysql://192.168.0.203:3306/test
my.datasource[2].username=root
my.datasource[2].password=zaqxsw

这里配置了3个数据源type=main是主节点,type=read是从节点

DatabaseContextHolder

内部使用 ThreadLocal 类,通过ThreadLocal 可以给每个线程设置和获取数据,起作用是在 AOP拦截到对应的 方法时,实现读写分离。

package com.example.ms.config.mybatisConfig;

public class DatabaseContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal();
    public static void setDataSourceType(String dataSourceType) {
        contextHolder.set(dataSourceType);
    }
    public static String getDataSourceType() {
        return contextHolder.get();
    }
}

DynamicDataSource

spring boot jdbc 提供了一个 AbstractRoutingDataSource,通过实现,我们可以在操作数据库之前,动态的设置 数据源

package com.example.ms.config.mybatisConfig;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        String dataSourceType = DatabaseContextHolder.getDataSourceType();
        System.out.println("动态获取到的 数据源key == "+dataSourceType);
        return dataSourceType;
    }
}

MultDataSource 读取配置文件

package com.example.ms.config.mybatisConfig;

import com.alibaba.druid.pool.DruidDataSource;
import com.example.ms.pojo.MyDatabase;
import lombok.Data;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Component
@Configuration
@MapperScan(basePackages = "com.example.ms.dao")
@PropertySource(value = "classpath:multidatabase.properties", encoding = "utf-8")
@ConfigurationProperties("my")
@Data
public class MultDataSource {
    public static final String MAIN = "main";
    public static final String READ = "read";

    public List<String> mainKeys = new ArrayList<>();
    public List<String> readKeys = new ArrayList<>();

    @Value("${my.datasource.driver}")
    private String driver;

    /**
     *  读取配置文件获取。
     */
    private List<MyDatabase> datasource;

    public DruidDataSource getDataSource(MyDatabase database) {
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setUrl(database.getUrl());
        druidDataSource.setUsername(database.getUsername());
        druidDataSource.setDriverClassName(driver);
        druidDataSource.setPassword(database.getPassword());
        druidDataSource.setInitialSize(1);
        druidDataSource.setMaxWait(6000);
        druidDataSource.setMinIdle(8);
        return druidDataSource;
    }

    @Bean
    @Primary
    public DynamicDataSource dataSource() {
        Map<Object, Object> targetDataSources = new HashMap<>();
        for (int i = 0; i < datasource.size(); i++) {
            String type = datasource.get(i).getType();
            DruidDataSource dataSource = getDataSource(datasource.get(i));
            if (MAIN.equals(type)) {
                mainKeys.add(MAIN+i);
                targetDataSources.put(MAIN+i,dataSource);
            } else {
                readKeys.add(READ+i);
                targetDataSources.put(READ+i,dataSource);
            }
        }
        DynamicDataSource dataSource = new DynamicDataSource();
        // 该方法是AbstractRoutingDataSource的方法
        dataSource.setTargetDataSources(targetDataSources);
        // 默认的datasource设置为myTestDbDataSource
        dataSource.setDefaultTargetDataSource(targetDataSources.get(mainKeys.get(0)));
        return dataSource;
    }
}

读取配置文件,根据配置文件的type是mian还是read,来判断走哪个数据库。

  • @MapperScan(basePackages = “com.example.ms.dao”),给这个路径下的class类都统一加入@mapper注解。
  • @PropertySource 加载指定配置文件
  • @ConfigurationProperties 可以和@PropertySource配合使用,代表将本类的全局变量和配置文件指定的属性相互绑定,注解后面的(“my”)对应multidatabase.properties里的my,本类中的“ private List datasource;”全局变量,就和配置文件My绑定。
  • @Data lombok注解,自动生成类属性的get,set方法
  • getDataSource(MyDatabase database),获取数据源实体属性,放入DruidDataSource,再根据DruidDataSource动态切换数据源
  • dataSource()。遍历配置文件里的数据源,根据数据源的读写分类,动态切换数据源
  • @Primary:自动装配时当出现多个Bean候选者时,被注解为@Primary的Bean将作为首选者,否则将抛出异常

aopAspectConfig

aop配置,拦截mybatis-plus以及dao层的方法,通过方法名,来判断走主库还是从库。
这里就要注意,因为是用aop来判断方法名字的办法来拦截的,数据交互层的方法名就要统一规范,否则拦截不到。Mybatis-plus内部自己封装的方法也是可以拦截到的,比如insert,selectById等等,切入点设置同样也是设置到dao层

package com.example.ms.aop;


import com.example.ms.config.mybatisConfig.DatabaseContextHolder;
import com.example.ms.config.mybatisConfig.MultDataSource;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.List;


@Component
@Aspect
public class aopAspectConfig {

    private Logger logger = LoggerFactory.getLogger(aopAspectConfig.class);

    @Autowired
    MultDataSource multDataSource;
    //切入点当前有效

    @Before("execution(* com.example.ms.dao..*.insert*(..)) ||" +
            "execution(* com.example.ms.dao..*.update*(..)) ||" +
            "execution(* com.example.ms.dao..*.delete*(..)) "
    )
    public  void setReadDataSource(){
        DatabaseContextHolder.setDataSourceType(getMainKey());
        System.out.println("主库的写操作");
    }

    @Before("execution(* com.example.ms.dao..*.select*(..))")
    public  void setWriteDataSource(){
        DatabaseContextHolder.setDataSourceType(getReadKey());
        System.out.println("从库的读操作");
    }

    /**
     * 轮询方式
     */
    int m = 0;
    public String getMainKey(){
        List<String> readKeys = multDataSource.getMainKeys();
        m ++;
        m = m%readKeys.size();
        return readKeys.get( m );
    }

    int i = 0;
    public String getReadKey(){
        List<String> readKeys = multDataSource.getReadKeys();
        i ++;
        i = i%readKeys.size();
        return readKeys.get( i );
    }
}

测试

到此为止,spring通过设置mybatis多数据源的方式实现读写分离,就配置好了,下面我们写一个测试案例

上一篇介绍了搭建mysql主从集群,这里在主库里添加一个表,加入2行数据。同样从库
也会出现相同的数据。
Mysql为1主2从的结构,我们把2个从的节点数据从数据库里改一下。

主节点

durid 配置postgresql数据源_mybatis_02


从节点1

durid 配置postgresql数据源_spring boot_03

从节点2

durid 配置postgresql数据源_java_04


然后配置好了实体类以及dao层以后,写个接口测试

@RestController
@RequestMapping("test")
public class testController {

            @Autowired
            public WagesInfoMapper wagesInfoMapper;

            @RequestMapping(value="insert",method = RequestMethod.POST)
            public String insert(){
                WagesInfo w = new WagesInfo();
                w.setName("xxx");
                w.setWages(3000);
                wagesInfoMapper.insert(w);
                return"ddd";
            }
            @RequestMapping(value="getOne",method = RequestMethod.POST)
            public WagesInfo getOne(){
                WagesInfo w =  wagesInfoMapper.selectById(1);
                return w;
            }
}

因为在aop配置里,加了输出语句,执行insert的时候,控制台会打印 “主库的写操作”.执行正确

执行getOne接口的时候,控制台会打印"从库的读操作",并且每一次请求,都会轮询请求从节点数据库,刚才数据库里的值修改了,通过Postman来测试,就可以看出来是每一次是请求不同的从节点数据库