springboot+springAOP實現數據庫讀寫分離及數據庫同步(MySQL)----最新可用2019-2-14

原文:https://blog.csdn.net/wsbgmofo/article/details/79260896 

1,數據源配置文件,以下

datasource.readSize=1
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

# 主數據源,默認的
spring.master.driver-class-name=com.mysql.jdbc.Driver
spring.master.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
spring.master.username=root
spring.master.password=root
spring.master.initialSize=5
spring.master.minIdle=5
spring.master.maxActive=50
spring.master.maxWait=60000
spring.master.timeBetweenEvictionRunsMillis=60000
spring.master.minEvictableIdleTimeMillis=300000
spring.master.poolPreparedStatements=true
spring.master.maxPoolPreparedStatementPerConnectionSize=20

# 從數據源
spring.slave.driver-class-name=com.mysql.jdbc.Driver
spring.slave.url=jdbc:mysql://localhost:3306/data_source_02?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
spring.slave.username=root
spring.slave.password=root
spring.slave.initialSize=5
spring.slave.minIdle=5
spring.slave.maxActive=50
spring.slave.maxWait=60000
spring.slave.timeBetweenEvictionRunsMillis=60000
spring.slave.minEvictableIdleTimeMillis=300000
spring.slave.poolPreparedStatements=true
spring.slave.maxPoolPreparedStatementPerConnectionSize=20java


2,新建數據庫配置類DataSourceConfiguration,以下

package com.aop.writeAndRead.config;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
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;
@Configuration
public class DataSourceConfiguration {
private static Logger log = LoggerFactory.getLogger(DataSourceConfiguration.class);
@Value("${spring.datasource.type}")
private Class<? extends DataSource> dataSourceType;
@Bean(name="writeDataSource", destroyMethod = "close", initMethod="init")
@Primary
@ConfigurationProperties(prefix = "spring.master")
public DataSource writeDataSource() {
log.info("-------------------- writeDataSource init ---------------------");
return DataSourceBuilder.create().type(dataSourceType).build();
}
/**
* 有多少個從庫就要配置多少個
* @return
*/
@Bean(name = "readDataSource", destroyMethod = "close", initMethod="init")
@ConfigurationProperties(prefix = "spring.slave")
public DataSource readDataSourceOne(){
log.info("-------------------- readDataSourceOne init ---------------------");
return DataSourceBuilder.create().type(dataSourceType).build();
}
/**
* 這裏的list是多個從庫的狀況下爲了實現簡單負載均衡
* @return
* @throws SQLException
*/
@Bean("readDataSources")
public List<DataSource> readDataSources() throws SQLException{
List<DataSource> dataSources=new ArrayList<>();
dataSources.add(readDataSourceOne());
return dataSources;
}
}mysql

3,新建DataSourceContextHolder類,根據ThreadLocal來實現數據源的動態改變,以下

package com.aop.writeAndRead.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DataSourceContextHolder {
private static Logger log = LoggerFactory.getLogger(DataSourceContextHolder.class);
private static final ThreadLocal<String> local = new ThreadLocal<String>();
public static ThreadLocal<String> getLocal() {
return local;
}
/**
* 讀多是多個庫
*/
public static void read() {
local.set(DataSourceType.read.getType());
System.out.println("==:" + DataSourceType.read.getType());
log.info("數據庫切換到讀庫...");
}
/**
* 寫只有一個庫
*/
public static void write() {
local.set(DataSourceType.write.getType());
log.info("數據庫切換到寫庫...");
}
public static String getJdbcType() {
return local.get();
}
}spring

 

4,新建一個枚舉類DataSourceType,以下


package com.aop.writeAndRead.config;
public enum DataSourceType {
read("read", "從庫"),
write("write", "主庫");
private String type;
private String name;
DataSourceType(String type, String name) {
this.type = type;
this.name = name;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}sql

 


5,新建MybatisConfiguration類,以下

package com.aop.writeAndRead.config;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Resource;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
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.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@ConditionalOnClass({EnableTransactionManagement.class})
@Import({ DataSourceConfiguration.class})
@MapperScan(basePackages={"com.aop.writeAndRead.mapper"})
public class MybatisConfiguration {
@Value("${spring.datasource.type}")
private Class<? extends DataSource> dataSourceType;
@Value("${datasource.readSize}")
private String dataSourceSize;
// @Resource(name = "writeDataSource")
// private DataSource writeDataSource;
// @Qualifier("readDataSource")
// private DataSource readDataSource;
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(ApplicationContext ac) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(roundRobinDataSouceProxy(ac));
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:mappings/**/*.xml"));
sqlSessionFactoryBean.setTypeAliasesPackage("com.aop.writeAndRead.entity");
sqlSessionFactoryBean.getObject().getConfiguration().setMapUnderscoreToCamelCase(true);
return sqlSessionFactoryBean.getObject();
}
/**
* 有多少個數據源就要配置多少個bean
* @return
*/
@Bean
public AbstractRoutingDataSource roundRobinDataSouceProxy(ApplicationContext ac) {
int size = Integer.parseInt(dataSourceSize);
System.out.println("size:" + size);
MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource(size);
Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
//多個讀數據庫時
DataSource writeDataSource = (DataSource)ac.getBean("writeDataSource");
List<DataSource> readDataSources = (List<DataSource>)ac.getBean("readDataSources");
for (int i = 0; i < size; i++) {
targetDataSources.put(i, readDataSources.get(i));
}
proxy.setDefaultTargetDataSource(writeDataSource);
proxy.setTargetDataSources(targetDataSources);
return proxy;
}
}
把第2步註冊的bean放入一個map裏面,後面就能夠動態從這個map裏面獲取對應的數據源數據庫

注意:以前報錯的地方就是在這裏,用@Resource和@Qualifier這兩種方式都沒法獲取到第2步註冊的bean,只能是經過applicationContext上下文獲取,應該是跟註解的優先級有關,Resource和Qualifier先執行,這個時候第2步的bean還未註冊,因此娶不到,若是有知道更詳細緣由的朋友,請留言告知apache

 

6,,新建MyAbstractRoutingDataSource,以下

 

package com.aop.writeAndRead.config;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class MyAbstractRoutingDataSource extends AbstractRoutingDataSource{
private final int dataSourceNumber;
private AtomicInteger count = new AtomicInteger(0);
public MyAbstractRoutingDataSource(int dataSourceNumber) {
this.dataSourceNumber = dataSourceNumber;
}
@Override
protected Object determineCurrentLookupKey() {
String typeKey = DataSourceContextHolder.getJdbcType();
if (typeKey.equals(DataSourceType.write.getType())) {
return DataSourceType.write.getType();
}
// 讀 簡單負載均衡
int number = count.getAndAdd(1);
int lookupKey = number % dataSourceNumber;
return new Integer(lookupKey);
}
}
這裏的determineCurrentLookupKey方法是根據DataSourceContextHolder這個類所改變的數據源而返回對應的bean的key,
這裏的key要跟第5步放入map裏面的key對應上服務器

7,新建springAOP類,以下

package com.aop.writeAndRead.config;
import java.lang.reflect.Method;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class DataSourceAop {
private static Logger log = LoggerFactory.getLogger(DataSourceAop.class);
@Pointcut("@annotation(com.aop.writeAndRead.config.WriteDataSource)")
public void writeMethod(){}
@Pointcut("@annotation(com.aop.writeAndRead.config.ReadDataSource)")
public void readMethod(){}
@Before("writeMethod()")
public void beforeWrite(JoinPoint point) {
DataSourceContextHolder.write();
String className = point.getTarget().getClass().getName();
String methodName = point.getSignature().getName();
System.out.println("開始執行:"+className+"."+methodName+"()方法...");
log.info("dataSource切換到:write");
}
@Before("readMethod()")
public void beforeRead(JoinPoint point) throws ClassNotFoundException {
//設置數據庫爲讀數據
DataSourceContextHolder.read();

/*spring AOP測試代碼*/
String currentClassName = point.getTarget().getClass().getName();//根據切點獲取當前調用的類名
String methodName = point.getSignature().getName();//根據切點獲取當前調用的類方法
Object[] args = point.getArgs();//根據切點獲取當前類方法的參數
System.out.println("開始執行:"+currentClassName+"."+methodName+"()方法...");
Class reflexClassName = Class.forName(currentClassName);//根據反射獲取當前調用類的實例
Method[] methods = reflexClassName.getMethods();//獲取該實例的全部方法
for(Method method : methods){
if(method.getName().equals(methodName)){
String desrciption = method.getAnnotation(ReadDataSource.class).description();//獲取該實例方法上註解裏面的描述信息
System.out.println("desrciption:" + desrciption);
}
}

log.info("dataSource切換到:Read");
}
}
利用springAOP對方法的切入,在方法執行前判斷使用哪一個數據源session

@Pointcut("@annotation(com.aop.writeAndRead.config.WriteDataSource)")
這裏是對自定義註解做切點,雙引號裏面也能夠換成對方法,可是我的以爲若是對方法做切點的話,若是方法多了這裏寫的就很長了mybatis

8,新建註解類,以下

package com.aop.writeAndRead.config;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadDataSource {
String description() default "";
}

package com.aop.writeAndRead.config;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface WriteDataSource {
String description() default "";
}
這樣在對須要控制數據源的方法前加上這個註解,springAOP就能控制這個方法,先選擇數據源再執行方法app

測試:

在方法上加入註解,以下

package com.aop.writeAndRead.service;

import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.aop.writeAndRead.config.ReadDataSource;
import com.aop.writeAndRead.config.WriteDataSource;
import com.aop.writeAndRead.entity.User;
import com.aop.writeAndRead.mapper.UserMapper;

@Service
public class UserService {

@Autowired UserMapper userMapper;

@WriteDataSource(description="WRITE")
public void writeUser(User user){
userMapper.writeUser(user);
}

@ReadDataSource(description="READ")
@Transactional(propagation=Propagation.REQUIRED,isolation=Isolation.DEFAULT,readOnly=true)
public Map<String, String> readUser(){
return userMapper.readUser();
}
}
接口分別調用writeUser跟readUser,以下

 


9,MySQL數據同步

 

修改主庫的配置文件my.ini,在末尾加上


#數據庫ID號, 爲1時表示爲Master,其中master_id必須爲1到232–1之間的一個正整數值;
server-id = 1
#啓用二進制日誌;
log-bin=mysql-bin
#須要同步的二進制數據庫名;
binlog-do-db=minishop
#不一樣步的二進制數據庫名,若是不設置能夠將其註釋掉;
binlog-ignore-db=information_schema
binlog-ignore-db=mysql
binlog-ignore-db=personalsite
binlog-ignore-db=test
#設定生成的log文件名;
log-bin="D:/Database/materlog"
#把更新的記錄寫到二進制文件中;
log-slave-updates
修改從庫的配置文件my.ini,在文件末尾加上

#若是須要增長Slave庫則,此id日後順延; server-id = 2 log-bin=mysql-bin #主庫host master-host = 192.168.168.253 #在主數據庫服務器中創建的用於該從服務器備份使用的用戶 master-user = forslave master-password = ****** master-port = 3306 #若是發現主服務器斷線,從新鏈接的時間差; master-connect-retry=60 #不須要備份的數據庫; replicate-ignore-db=mysql #須要備份的數據庫 replicate-do-db=minishop log-slave-update

相關文章
相關標籤/搜索