springboot+springAOP实现数据库读写分离及数据库同步(MySQL)

网上找了很多资料,发现根据获取bean的时候报空指针,不知道是哪里有问题,后来稍作修改就可以了

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=20

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 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 readDataSources() throws SQLException{  
        List dataSources=new ArrayList<>();  
        dataSources.add(readDataSourceOne());  
        return dataSources;  
    }  
}
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 local = new ThreadLocal();
    public static ThreadLocal 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();  
    }  
}
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;
    } 
}

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 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 targetDataSources = new HashMap();  
        //多个读数据库时  
        DataSource writeDataSource = (DataSource)ac.getBean("writeDataSource");
        List readDataSources = (List)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还未注册,所以娶不到,如果有知道更详细原因的朋友,请留言告知

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对方法的切入,在方法执行前判断使用哪个数据源

@Pointcut("@annotation(com.aop.writeAndRead.config.WriteDataSource)")

这里是对自定义注解作切点,双引号里面也可以换成对方法,但是个人觉得如果对方法作切点的话,如果方法多了这里写的就很长了

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就能控制这个方法,先选择数据源再执行方法

测试:

在方法上加入注解,如下

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 readUser(){
		return userMapper.readUser();
	}
}
接口分别调用writeUser跟readUser,如下

springboot+springAOP实现数据库读写分离及数据库同步(MySQL)_第1张图片

springboot+springAOP实现数据库读写分离及数据库同步(MySQL)_第2张图片springboot+springAOP实现数据库读写分离及数据库同步(MySQL)_第3张图片

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






你可能感兴趣的:(springboot-读写分离)