02-下篇-SpringBoot下MySQL的读写分离

前言:关于MySQL读写主从实现,分两步:

第一步,需要现有主从的环境,利用docker快速实现; -----上篇

第二步,利用已有的环境进行JavaEE的Web项目配置。 -----下篇,基于SpringBoot的SpringDataJpa的实现!即本文

 

环境:

SpringBoot:2.0.3 DB:MySQL5.7.20 主从模式

持久化框架:SpringDataJpa

 

1.多数据源的配置application.yml:

#####MySQL数据库的主从配置开始#####
mysql:
    datasource:
        readSize: 1  #读库个数,可以有多个
        type: com.alibaba.druid.pool.DruidDataSource
        write:
           url: jdbc:mysql://192.168.1.121:3306/db_frms?useUnicode=true&characterEncoding=utf-8
           username: root
           password: 123456
           driver-class-name: com.mysql.jdbc.Driver
           minIdle: 5
           maxActive: 100
           initialSize: 10
           maxWait: 60000
           timeBetweenEvictionRunsMillis: 60000
           minEvictableIdleTimeMillis: 300000
           validationQuery: select 'x'
           testWhileIdle: true
           testOnBorrow: false
           testOnReturn: false
           poolPreparedStatements: true
           maxPoolPreparedStatementPerConnectionSize: 50
           removeAbandoned: true
           filters: stat
        read01:
           url: jdbc:mysql://192.168.1.121:3307/db_frms?useUnicode=true&characterEncoding=utf-8
           username: root
           password: 123456
           driver-class-name: com.mysql.jdbc.Driver
           minIdle: 5
           maxActive: 100
           initialSize: 10
           maxWait: 60000
           timeBetweenEvictionRunsMillis: 60000
           minEvictableIdleTimeMillis: 300000
           validationQuery: select 'x'
           testWhileIdle: true
           testOnBorrow: false
           testOnReturn: false
           poolPreparedStatements: true
           maxPoolPreparedStatementPerConnectionSize: 50
           removeAbandoned: true
           filters: stat
#        read02:					#因为我只用docker配置了一个slave,所以没有第二个slave,故这段配置注释掉!
#           url: jdbc:mysql://192.168.1.121:3308/test_02?useUnicode=true&characterEncoding=utf-8
#           username: root
#           password: root
#           driver-class-name: com.mysql.jdbc.Driver
#           minIdle: 5
#           maxActive: 100
#           initialSize: 10
#           maxWait: 60000
#           timeBetweenEvictionRunsMillis: 60000
#           minEvictableIdleTimeMillis: 300000
#           validationQuery: select 'x'
#           testWhileIdle: true
#           testOnBorrow: false
#           testOnReturn: false
#           poolPreparedStatements: true
#           maxPoolPreparedStatementPerConnectionSize: 50
#           removeAbandoned: true
#           filters: stat
#####MySQL数据库的主从配置结束#####

2.定义数据库的类型枚举类DataSourceType

package com.ddbin.frms.config.datasource;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * Description:数据源类型的枚举类
 *
 * @param
 * @author dbdu
 * @date 18-7-14 上午8:05
 */
@Getter
@AllArgsConstructor
public enum DataSourceType {

    read("read", "从库"), write("write", "主库");

    /**
     * Description:类型,是读还是写
     *
     * @author dbdu
     * @date 18-7-14 上午8:14
     */
    private String type;

    /**
     * Description:数据源的名称
     *
     * @author dbdu
     * @date 18-7-14 上午8:15
     */
    private String name;

}

要注意的是:枚举实例小写,大写会报错!!

read("read", "从库"), write("write", "主库");

3.多个数据源的实例化配置类DataSourceConfiguration

有多少个数据源,就配置多少个对应的Bean。

package com.ddbin.frms.config.datasource;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;

/**
 * Description:MySQL读写主从数据库源配置
 *
 * @param
 * @author dbdu
 * @date 18-7-14 上午7:51
 * @return
 */
@Configuration
@Slf4j
public class DataSourceConfiguration {

    @Value("${mysql.datasource.type}")
    private Class dataSourceType;

    /**
     * 写库 数据源配置
     *
     * @return
     */
    @Bean(name = "writeDataSource")
    @Primary
    @ConfigurationProperties(prefix = "mysql.datasource.write")
    public DataSource writeDataSource() {
        log.info("writeDataSource init ...");
        return DataSourceBuilder.create().type(dataSourceType).build();
    }

    /**
     * 有多少个从库就要配置多少个
     *
     * @return
     */
    @Bean(name = "readDataSource01")
    @ConfigurationProperties(prefix = "mysql.datasource.read01")
    public DataSource readDataSourceOne() {
        log.info("read01 DataSourceOne init ...");
        return DataSourceBuilder.create().type(dataSourceType).build();
    }

//    @Bean(name = "readDataSource02")
//    @ConfigurationProperties(prefix = "mysql.datasource.read02")
//    public DataSource readDataSourceTwo() {
//        log.info("read02 DataSourceTwo init ...");
//        return DataSourceBuilder.create().type(dataSourceType).build();
//    }


}

 

4.配置数据源的切换类DataSourceContextHolder

设置这个类的对应的read和write,就被内部用来读取不同的数据源

package com.ddbin.frms.config.datasource;

import lombok.extern.slf4j.Slf4j;

/**
 * Description:本地线程,数据源上下文切换
 *
 * @author dbdu
 * @date 18-7-14 上午8:17
 */
@Slf4j
public class DataSourceContextHolder {
    //线程本地环境
    private static final ThreadLocal local = new ThreadLocal();

    public static ThreadLocal getLocal() {
        return local;
    }

    /**
     * 读库
     */
    public static void setRead() {
        local.set(DataSourceType.read.getType());
        //log.info("数据库切换到READ库...");
    }

    /**
     * 写库
     */
    public static void setWrite() {
        local.set(DataSourceType.write.getType());
        // log.info("数据库切换到WRITE库...");
    }

    public static String getReadOrWrite() {
        return local.get();
    }

    public static void clear() {
        local.remove();
    }
}

5.数据源的代理路由配置DatasourceAgentConfig---请读者特别注意这个类,网上很多说的都是mybatis框架的,这里是SpringDataJpa框架对应的关联代理数据源路由的配置,此处配置出错就会失败!

package com.ddbin.frms.config.datasource;

import com.ddbin.frms.FrmsApplication;
import com.ddbin.frms.util.SpringContextsUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;

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

@Configuration
@AutoConfigureAfter(DataSourceConfiguration.class)
@EnableTransactionManagement(order = 10)
@Slf4j
public class DatasourceAgentConfig {

    @Value("${mysql.datasource.readSize}")
    private String readDataSourceSize;

    private AbstractRoutingDataSource proxy;


    @Autowired
    @Qualifier("writeDataSource")
    private DataSource writeDataSource;
    @Autowired
    @Qualifier("readDataSource01")
    private DataSource readDataSource01;
//    @Autowired
//    @Qualifier("readDataSource02")
//    private DataSource readDataSource02;


    /**
     * 把所有数据库都放在路由中
     * 重点是roundRobinDataSouceProxy()方法,它把所有的数据库源交给AbstractRoutingDataSource类,
     * 并由它的determineCurrentLookupKey()进行决定数据源的选择,其中读库进行了简单的负载均衡(轮询)。
     *
     * @return
     */
    @Bean(name = "roundRobinDataSouceProxy")
    public AbstractRoutingDataSource roundRobinDataSouceProxy() {

        /**
         * Description:把所有数据库都放在targetDataSources中,注意key值要和determineCurrentLookupKey()中代码写的一至,
         *     否则切换数据源时找不到正确的数据源
         */
        Map targetDataSources = new HashMap();

        targetDataSources.put(DataSourceType.write.getType(), writeDataSource);
        targetDataSources.put(DataSourceType.read.getType() + "1", readDataSource01);
        //targetDataSources.put(DataSourceType.read.getType() + "2", readDataSource02);

        //路由类,寻找对应的数据源
        final int readSize = Integer.parseInt(readDataSourceSize);
        MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource(readSize);

        proxy.setTargetDataSources(targetDataSources);
        //默认库
        proxy.setDefaultTargetDataSource(writeDataSource);
        this.proxy = proxy;
        return proxy;
    }

    /**
     * Description:要特别注意,这个Bean是配置读写分离成败的关键,
     *
     * @param []
     * @return org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean
     * @author dbdu
     * @date 18-7-15 下午5:08
     */
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        vendorAdapter.setDatabase(Database.MYSQL);
        //是否生成表
             vendorAdapter.setGenerateDdl(true);
        //是否显示sql语句
            vendorAdapter.setShowSql(true);

        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(vendorAdapter);
        //配置扫描的位置
        factory.setPackagesToScan(FrmsApplication.class.getPackage().getName());
        // 这个数据源设置为代理的数据源,----这是关键性配置!!!
        factory.setDataSource(proxy);
        return factory;
    }


    @Bean(name = "transactionManager")
    public MyJpaTransactionManager transactionManager() {

        MyJpaTransactionManager transactionManager = new MyJpaTransactionManager();
        transactionManager.setDataSource(proxy);
        transactionManager.setEntityManagerFactory((EntityManagerFactory) SpringContextsUtil.getBean("entityManagerFactory"));
        return transactionManager;
    }
}

说明:entityManagerFactory是关键配置,网上很多说的都是mybatis的方式sqlSessionFactory的Bean会关联代理数据源,

SpringDataJpa的方式使用entityManagerFactory来关联代理数据源,否则读写分离是假的,这个可以通过主从库数据不同查询可以知道!

/**

* Description:要特别注意,这个Bean是配置读写分离成败的关键,

*

* @param []

* @return org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean

* @author dbdu

* @date 18-7-15 下午5:08

*/

@Bean

public LocalContainerEntityManagerFactoryBean entityManagerFactory() {

HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();

vendorAdapter.setDatabase(Database.MYSQL);

//是否生成表

vendorAdapter.setGenerateDdl(true);

//是否显示sql语句

vendorAdapter.setShowSql(true);

 

LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();

factory.setJpaVendorAdapter(vendorAdapter);

//配置扫描的位置

factory.setPackagesToScan(FrmsApplication.class.getPackage().getName());

// 这个数据源设置为代理的数据源,----这是关键性配置!!!

factory.setDataSource(proxy);

return factory;

}

6.自定义的路由数据源及事务管理器的子类:

package com.ddbin.frms.config.datasource;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * Description: 抽象数据源的路由的子类
 * Created at:2018-07-15 13:37,
 * by dbdu
 */
@Getter
@Setter
@AllArgsConstructor
public class MyAbstractRoutingDataSource extends AbstractRoutingDataSource {

    /**
     * Description:读库的数量,可以用来实现负载均衡
     */
    private int readSize;

    //private AtomicLong count = new AtomicLong(0);

    /**
     * 这是AbstractRoutingDataSource类中的一个抽象方法,
     * 而它的返回值是你所要用的数据源dataSource的key值,有了这个key值,
     * targetDataSources就从中取出对应的DataSource,如果找不到,就用配置默认的数据源。
     */
    @Override
    protected Object determineCurrentLookupKey() {
        String typeKey = DataSourceContextHolder.getReadOrWrite();

        if (typeKey == null || typeKey.equals(DataSourceType.write.getType())) {
            System.err.println("使用数据库write.............");
            return DataSourceType.write.getType();
        } else {
            //读库, 简单负载均衡
//                    int number = count.getAndAdd(1);
//                    int lookupKey = number % readSize;
//                    System.err.println("使用数据库read-" + (lookupKey + 1));
//                    return DataSourceType.read.getType() + (lookupKey + 1);

            return DataSourceType.read.getType() + "1";

        }
    }
}
package com.ddbin.frms.config.datasource;

import lombok.extern.slf4j.Slf4j;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.DefaultTransactionStatus;

@SuppressWarnings("serial")
@Slf4j
public class MyJpaTransactionManager extends JpaTransactionManager {

    @Override
    protected void doBegin(Object transaction, TransactionDefinition definition) {
        if (definition.isReadOnly()) {
            DataSourceContextHolder.setRead();
        } else {
            DataSourceContextHolder.setWrite();
        }
        log.info("jpa-transaction:begin-----now dataSource is [" + DataSourceContextHolder.getReadOrWrite() + "]");
        super.doBegin(transaction, definition);
    }

    @Override
    protected void doCommit(DefaultTransactionStatus status) {
        log.info("jpa-transaction:commit-----now dataSource is [" + DataSourceContextHolder.getReadOrWrite() + "]");
        super.doCommit(status);
    }
} 

说明:如果方法命名不符合规则,也没有加注解,则typeKey会有可能为null,下面的逻辑是

typeKey为空使用写库!---也就是主库。

/**

* 这是AbstractRoutingDataSource类中的一个抽象方法,

* 而它的返回值是你所要用的数据源dataSource的key值,有了这个key值,

* targetDataSources就从中取出对应的DataSource,如果找不到,就用配置默认的数据源。

*/

@Override

protected Object determineCurrentLookupKey() {

String typeKey = DataSourceContextHolder.getReadOrWrite();

 

if (typeKey == null || typeKey.equals(DataSourceType.write.getType())) {

System.err.println("使用数据库write.............");

return DataSourceType.write.getType();

} else {

//读库, 简单负载均衡

// int number = count.getAndAdd(1);

// int lookupKey = number % readSize;

// System.err.println("使用数据库read-" + (lookupKey + 1));

// return DataSourceType.read.getType() + (lookupKey + 1);

 

return DataSourceType.read.getType() + "1";

 

}

}

 

7.读写数据源的注解,非必需,有则可以更加灵活:

package com.ddbin.frms.config.datasource;

import java.lang.annotation.*;

/**
 * Description:读数据源的注解
 *
 * @author dbdu
 * @date 18-7-14 上午8:21
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface ReadDataSource {

}
package com.ddbin.frms.config.datasource;

import java.lang.annotation.*;

/**
 * Description:写数据源的注解
 *
 * @author dbdu
 * @date 18-7-14 上午8:21
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface WriteDataSource {

}

8.配置service切面,来切换不同的数据源:

package com.ddbin.frms.config.datasource;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.PriorityOrdered;
import org.springframework.stereotype.Component;

/**
 * Description:在service层决定数据源
 * 必须在事务AOP之前执行,所以实现Ordered,order的值越小,越先执行
 * 如果一旦开始切换到写库,则之后的读都会走写库;
 * 方法名符合切入点规则或加上读写注解都可以使用对应的数据库!!
 *
 * @author dbdu
 * @date 18-7-14 上午8:32
 */
@Aspect
@EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true)
@Component
public class DataSourceAopInService implements PriorityOrdered {

    @Before("execution(* com.ddbin.frms.service..*.find*(..)) "
            + " or execution(* com.ddbin.frms.service..*.get*(..)) "
            + " or execution(* com.ddbin.frms.service..*.query*(..))"
            + " or execution(* com.ddbin.frms.service..*.list*(..))"
            + " or @annotation(com.ddbin.frms.config.datasource.ReadDataSource) "
    )
    public void setReadDataSourceType() {
        //如果已经开启写事务了,那之后的所有读都从写库读
        if (!DataSourceType.write.getType().equals(DataSourceContextHolder.getReadOrWrite())) {
            DataSourceContextHolder.setRead();
        }

    }

    @Before("execution(* com.ddbin.frms.service..*.insert*(..)) "
            + " or execution(* com.ddbin.frms.service..*.add*(..))"
            + " or execution(* com.ddbin.frms.service..*.save*(..))"
            + " or execution(* com.ddbin.frms.service..*.create*(..))"
            + " or execution(* com.ddbin.frms.service..*.update*(..))"
            + " or execution(* com.ddbin.frms.service..*.mod*(..))"
            + " or execution(* com.ddbin.frms.service..*.delete*(..))"
            + " or execution(* com.ddbin.frms.service..*.del*(..))"
            + " or execution(* com.ddbin.frms.service..*.truncate*(..))"
            + " or @annotation(com.ddbin.frms.config.datasource.WriteDataSource) "
    )
    public void setWriteDataSourceType() {
        DataSourceContextHolder.setWrite();
    }

    @Override
    public int getOrder() {
        /**
         * 值越小,越优先执行
         * 要优于事务的执行
         * 在启动类中加上了@EnableTransactionManagement(order = 10)
         */
        return 1;
    }

}

说明:

A.如果方法的命名符合切入点的规则,则自动设定使用需要的数据源;

B.如果不符合A 的方法命名规则,使用注解也一样。

C.如果方法命名不符合A 的规则也没有对应的注解 ,则默认使用主库!

特别注意:

一定不要对从库或说read库进行写操作,这样做的后果是,轻者导致数据不一致(主库到从库的单向同步),重者导致从库同步失败,主库的更改不会同步到从库!

因此,写库注解可以加到service的任意方法上,因为是操作主库,但是读库注解不能加到写的方法上!

9.测试用类:读者自己去写就好了。

/**
 * Description:这个方法用来测试 方法名不符合规范及注解不同是什么效果!!
 * 方法名不符合规则也没有注解,默认走主库!
 *
 * @author dbdu
 * @date 18-7-15 下午5:45
 */
@ReadDataSource
@Override
public Page pageByName(String userName, Pageable page) {
    Page page1 = employeeRepository.findByName(userName, page);
    return page1;
}

 

 

 

 

 

 

参考地址:

A.https://blog.csdn.net/dream_broken/article/details/72851329 https://github.com/269941633/spring-boot-mybatis-mysql-write-read        ----主要参考这篇文章!!

B.https://blog.csdn.net/u011493599/article/details/53930394

C.https://www.cnblogs.com/pier2/p/spring-boot-read-write-split.html

D.https://www.jianshu.com/p/8813ec02926a

E.https://my.oschina.net/wxdl/blog/1628007

F.http://blog.51cto.com/13645072/2090313

datajpa:

https://blog.csdn.net/hangge111/article/details/51605558

https://blog.csdn.net/ba5189tsl/article/details/47341425

LocalContainerEntityManagerFactoryBean创建的方法:

http://www.javased.com/index.php?api=org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean

 

你可能感兴趣的:(应用配置)