zoukankan      html  css  js  c++  java
  • 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=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<? 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;
    }
    }

    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();
    }
    }

    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<? 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还未注册,所以娶不到,如果有知道更详细原因的朋友,请留言告知

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

    ---------------------
    作者:猴样鬼相
    来源:CSDN
    原文:https://blog.csdn.net/wsbgmofo/article/details/79260896
    版权声明:本文为博主原创文章,转载请附上博文链接!

  • 相关阅读:
    .htaccess是什么?.htaccess几个简单应用
    php提高效率
    php require和include区别
    excel的常用公式
    php时间日期处理
    json详解
    python列表-增强的赋值操作
    python列表-使用
    python列表-简单操作
    python列表-定义
  • 原文地址:https://www.cnblogs.com/xiaohouzai/p/10375450.html
Copyright © 2011-2022 走看看