数据库主从分离的常见实现方式,以及程序内部实现的两种思路

数据库主从分离通常有几类实现方式,一是在应用程序内区分主从,二是新增一层数据库代理服务器,在应用服务器和数据库服务器之间根据SQL区分主从。

前者的好处是可以不用新增额外的服务器开销,后者的好处是可以不用对项目程序逻辑做任何的改动。

而从应用程序内部区分主从,又有两种方式,一是根据包名区分,二是根据自定义注解区分

一、根据包名区分主从

根据包名区分大概就是把操作主库的Mapper和xml文件放到一类文件夹上,把查询从库的Mapper和xml文件放在另一个目录的文件夹下。

里面的关键点在于MasterDataSourceConfig和ClusterDataSourceConfig,这两个用于声明和扫描对于目录下的dao和mapper.xml文件,并且返回对于的数据库链接,具体代码如下:

package com.tlgg.druid;

import com.alibaba.druid.pool.DruidDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.sql.DataSource;

/**
 * 从库数据源配置
 */
@Configuration
// 扫描 Mapper 接口并容器管理
@MapperScan(basePackages = ClusterDataSourceConfig.PACKAGE, sqlSessionFactoryRef = "clusterSqlSessionFactory")
public class ClusterDataSourceConfig {
    /**
     * 精确到 cluster 目录,以便跟其他数据源隔离
     */
    static final String PACKAGE = "com.tlgg.dao.cluster";
    static final String MAPPER_LOCATION = "classpath:mapper/cluster/*.xml";

    @Value("${cluster.datasource.url}")
    private String url;

    @Value("${cluster.datasource.username}")
    private String user;

    @Value("${cluster.datasource.password}")
    private String password;

    @Value("${cluster.datasource.driverClassName}")
    private String driverClass;
    /**
    *获取数据库链接
    */
    @Bean(name = "clusterDataSource")
    public DataSource clusterDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driverClass);
        dataSource.setUrl(url);
        dataSource.setUsername(user);
        dataSource.setPassword(password);
        System.out.println("-------------cluster--------------------");
        return dataSource;
    }
    /**
    *事务管理
    */
    @Bean(name = "clusterTransactionManager")
    public DataSourceTransactionManager clusterTransactionManager() {
        return new DataSourceTransactionManager(clusterDataSource());
    }
    /**
    *初始化时,加载xml文件,生产sqlSessionFactory
    */
    @Bean(name = "clusterSqlSessionFactory")
    public SqlSessionFactory clusterSqlSessionFactory(@Qualifier("clusterDataSource") DataSource clusterDataSource)
            throws Exception {
        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(clusterDataSource);
        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources(ClusterDataSourceConfig.MAPPER_LOCATION));
        return sessionFactory.getObject();
    }
}

二、通过自定义注解区分主从

还有一种方式就是通过自定义注解,通过AOP扫描该注解然后往ThreadLocal里面塞对应的主库处理/从库处理的属性,然后重写AbstractRoutingDataSource来实现返回对于的主从数据源来实现。

自定义注解如下:

package com.tlgg.db;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 声明使用主库从库的注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSourceSelect {
    String dataSourceName() default "master";
}

往当前线程设置主库/从库处理的threadLocal类如下:

package com.tlgg.db;

public class HandleDataSource {

    public static final ThreadLocal holder = new ThreadLocal();

    /**
     * 绑定当前线程数据源路由的key
     * @param datasource
     */
    public static void putDataSource(String datasource) {
        holder.set(datasource);
    }

    /**
     * 获取当前线程的数据源路由的key
     * @return
     */
    public static String getDataSource() {
        return holder.get();
    }

    public static void close(){
        holder.remove();
    }
}

AOP切面类如下:

package com.tlgg.db;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.lang.reflect.Method;

@Aspect
@Component
@Slf4j
public class DataSourceAspect{

    @Around("execution(public * com.tlgg..*.service..impl..*.*(..)) && @annotation(org.springframework.transaction.annotation.Transactional)")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        try {
            //如果当前线程没有设置过主库处理或者从库处理,那么就判断当前执行方法是否包含DataSourceSelect枚举
            if (HandleDataSource.getDataSource() == null) {
                Method method = ((MethodSignature) pjp.getSignature()).getMethod();
                DataSourceSelect dataSourceSelect = method.getAnnotation(DataSourceSelect.class);
                //如果包含,则把当前线程的属性设为对于枚举的配置
                if (dataSourceSelect != null) {
                    HandleDataSource.putDataSource(dataSourceSelect.dataSourceName());
                } else {
                    //如果不包含,则默认为主库处理
                    HandleDataSource.putDataSource("master");
                }
            }
        } catch (Throwable throwable) {
            throw throwable;
        } finally {
            HandleDataSource.close();
        }
        return pjp.proceed();
    }
}

最终在执行sql,获取数据库链接时,会调用我们重写的AbstractRoutingDataSource的类,根据当前线程里的属性来返回对应主库或者从库的链接

package com.tlgg.db;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class MultipleDataSource extends AbstractRoutingDataSource  {
    /**
     * 获取与数据源相关的key
     * 此key是Map resolvedDataSources 中与数据源绑定的key值
     * 在通过determineTargetDataSource获取目标数据源时使用
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return HandleDataSource.getDataSource();
    }

}

最后我们在需要使用的Service方法上声明就可以了:

package com.tlgg.loan.service.test.impl;

import com.cardniu.db.DataSourceSelect;
import org.springframework.stereotype.Service;

/**
 * 测试service类
 */
@Service
public class DataSourceTestService {
    
    @DataSourceSelect(dataSourceName = "master")
    public void testMaster(){
        //查询主库   
    }
    @DataSourceSelect(dataSourceName = "slave")
    public void testSlave(){
        //查询从库   
    }
}

具体的数据库链接配置就省略了,有兴趣的同学可以留言探讨~

你可能感兴趣的:(java,spring,java,aop,mysql)