前言:关于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<? extends DataSource> 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<String> local = new ThreadLocal<String>(); public static ThreadLocal<String> 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<Object, Object> targetDataSources = new HashMap<Object, Object>(); 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<Employee> pageByName(String userName, Pageable page) { Page<Employee> 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 ----主要参考这篇文章!!
datajpa:
LocalContainerEntityManagerFactoryBean创建的方法: