使用Spring AbstractRoutingDataSource实现读写分离,实现垂直分库和水平分表

1.使用场景

分库分表的使用场景:对大数据量的表或者库进行分割,将数据依据逻辑规则分别放置不同的库,减轻单库单表的压力。具体又分为垂直分库,水平分库,垂直分表,水平分表。

读写分离的应用场景:应用程序在进行读取数据库数据和写入数据库数据时,分别操作不同的数据库,从数据库及时数据同主数据库保持一致,就可以提高数据库的访问能力。

2.实现方式

a. 可以使用mycat(对此不熟,仅仅听说过)
b. 使用sharding-jdbc中间件,使用demo 参考:https://segmentfault.com/a/11...:https://github.com/sharding-sphere/ShardingSphereDemo
c. 使用spring AbstractRoutingDataSource实现。

3.使用AbstractRoutingDataSource实现分库分表

创建自有的XiayuDataSource,继承AbtstractRoutingDataSource

package com.xiayu.config;

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

public class XiayuDataSource extends AbstractRoutingDataSource {
    //重写获取lookupkey方法
    @Override
    protected Object determineCurrentLookupKey() {
        return RequestThreadLocalHolder.getDatabasIndex(); 
    }
}

定义ThreadLocal变量存储类

package com.xiayu.config;

import com.xiayu.constants.DataSourceConstants;

import java.util.Locale;

public class RequestThreadLocalHolder {

    private static ThreadLocal databaseIndex = new ThreadLocal(){
        @Override
        protected String initialValue() {
            return DataSourceConstants.MASTER_DATA_SOURCE;
        }
    }; //数据库的线程局部变量,可以基于此实现读写分离和垂直分库,默认值为主库名

    private static ThreadLocal tableIndex = new ThreadLocal(){
        @Override
        protected Integer initialValue() {
            return 1;
        }
    };//表名的线程局部变量,可以基于此实现水平分表

    private static ThreadLocal local = new ThreadLocal(){
        @Override
        protected Locale initialValue() {
            return Locale.CHINA;
        }
    };//国家的线程局部变量,在国际化时可以使用该变量,返回对应国家的message,默认值为Locale.CHINA

    public static void setDatabaseIndex(String databaseIndex){
        RequestThreadLocalHolder.databaseIndex.set(databaseIndex);
    } //设置

    public static String getDatabasIndex(){
        return RequestThreadLocalHolder.databaseIndex.get();
    }//获取

    public static void removeDatabasIndex(){
        RequestThreadLocalHolder.databaseIndex.remove();
    }//移除,下同

    public static void setTableIndex(Integer tableIndex){
        RequestThreadLocalHolder.tableIndex.set(tableIndex);
    }

    public static Integer getTableIndex(){
        return RequestThreadLocalHolder.tableIndex.get();
    }

    public static void removeTableIndex(){
        RequestThreadLocalHolder.tableIndex.remove();
    }

    public static void setLocal(Locale local){
        RequestThreadLocalHolder.local.set(local);
    }

    public static Locale getLocale(){
        return RequestThreadLocalHolder.local.get();
    }

    public static void removeLocale(){
        RequestThreadLocalHolder.local.remove();
    }
}

创建XiaYuDataSource
有三个数据源,分别为masterDataSource,slaveOneDataSource和slaveTwoDataSource,一主两从。

package com.xiayu.config;


import com.xiayu.constants.DataSourceConstants;
import lombok.Data;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

@Data
@Configuration
@MapperScan(basePackages = { DataSourceConstants.MAPPER_BASEPACKAGE }, sqlSessionFactoryRef = "sqlSessionFactory")
public class DatasourceConfig {

//    @Qualifier("masterDataSource")
    private DataSource masterDataSource;

//    @Qualifier("slaveOneDataSource")
    private DataSource slaveOneDataSource;

//    @Qualifier("slaveTwoDataSource")
    private DataSource slaveTwoDataSource;

    public DatasourceConfig() {

    }

    @Bean(name = "xiayuDataSource")
    @Primary
    public DataSource xiayuDataSource(){
        //这一块做的不行,我通过配置文件new,调用方法的dataSource,也可以在该类中自定义方法获取DataSource,我本想通过@ConfigurationProperties注解方便url,username,password的映射
        MasterDataSourceConfig masterDataSourceConfig = new MasterDataSourceConfig();
        this.masterDataSource = masterDataSourceConfig.dataSource();

        SlaveOneDataSourceConfig slaveOneDataSourceConfig = new SlaveOneDataSourceConfig();
        this.slaveOneDataSource = slaveOneDataSourceConfig.dataSource();

        SlaveTwoDataSourceConfig slaveTwoDataSourceConfig = new SlaveTwoDataSourceConfig();
        this.slaveTwoDataSource = slaveTwoDataSourceConfig.dataSource();

        XiayuDataSource xiayuDataSource = new XiayuDataSource();
        Map dataSourceMap = new HashMap<>(); //这儿的key是object,如果determineCurrentLookupKey()方法返回的值与此key相等则会路由到对应的数据源上,通过查看这块的源码可以查看到
        dataSourceMap.put("masterDataSource",masterDataSource); //如果lookupkey返回值和masterDataSource相同,就会返回masterDataSource
        dataSourceMap.put("slaveOneDataSource",slaveOneDataSource);
        dataSourceMap.put("slaveTwoDataSource",slaveTwoDataSource);
        xiayuDataSource.setTargetDataSources(dataSourceMap);
        xiayuDataSource.setDefaultTargetDataSource(masterDataSource); //默认的数据源
        return xiayuDataSource;
    }

//    @Bean(name = "transactionManager")
//    public DataSourceTransactionManager transactionManager(@Qualifier("xiayuDataSource")DataSource xiayuDataSource) throws SQLException{
//        return new DataSourceTransactionManager(xiayuDataSource);
//    }

    @Bean(name = "transactionManager")
    public DataSourceTransactionManager transactionManager(@Qualifier("xiayuDataSource")DataSource xiayuDataSource) throws SQLException{
        return new DataSourceTransactionManager(xiayuDataSource);
    }

    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactory primarySqlSessionFactory(@Qualifier("xiayuDataSource") DataSource xiayuDataSource) throws Exception{
        final SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
        sessionFactoryBean.setDataSource(xiayuDataSource);
        return sessionFactoryBean.getObject();
    }
}

定义数据库路由注解

@Target(ElementType.METHOD)   //注解作用的地方,此处定义为方法上
@Retention(RetentionPolicy.RUNTIME)  //在运行时候,生效
public @interface DBRouting {  
    String isRead() default "false";  //java注解成员不支持布尔类型
}

定义切面,改变线程的局部变量

package com.xiayu.aspect;

import com.xiayu.annotation.DBRouting;
import com.xiayu.config.RequestThreadLocalHolder;
import com.xiayu.constants.DataSourceConstants;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Component
@Aspect
@Slf4j
public class DBRoutingAspect {

    @Pointcut("@annotation(com.xiayu.annotation.DBRouting)")
    public void pointCut(){}; //连接点

    @Before("pointCut()")
    public void before(JoinPoint joinPoint) throws NoSuchMethodException { //前置通知
        log.info("start request");
        long startTime = System.currentTimeMillis();

        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod(); //获取到加入注解的方法
        DBRouting dbRouting = method.getAnnotation(DBRouting.class); //获取到DBRouting注解
        String isRead = dbRouting.isRead(); //获取到设置的值
        boolean read = Boolean.parseBoolean(isRead);
        if (read){
            RequestThreadLocalHolder.setDatabaseIndex(DataSourceConstants.SLAVE_ONE_DATA_SOURCE); 
            //如果read 为true的话,就将线程局部变量设置为从库,然后在通过与lookupkey进行比较,获取到从库的数据源
            //有两个从库,可以通过hash 取余2,分别访问两个从库
            //在此也可以实现垂直分库,依据传入的参数,例如参数中有省份参数,需要路由到对应省份的数据库,得到省份参数,然后再依据省份参数获取到对应的主库lookupkey
        }
        long endTime = System.currentTimeMillis();
        log.info("response end");
        log.info("time:" + (endTime - startTime));
    }

    @After("pointCut()")
    public void after(JoinPoint joinPoint){ //后置通知,将线程局部变量清除掉
        Method method = getMethod(joinPoint);
        DBRouting dbRouting = method.getAnnotation(DBRouting.class);
        String isRead = dbRouting.isRead();
        boolean read = Boolean.parseBoolean(isRead);
        if (read){
            RequestThreadLocalHolder.removeDatabasIndex(); //remove
        }
    }

    private Method getMethod(JoinPoint joinPoint){
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        return method;
    }

}

@DBRouting注解的使用

package com.xiayu.service.impl;

import com.xiayu.annotation.DBRouting;
import com.xiayu.entity.OrderEntity;
import com.xiayu.mapper.OrderMapper;
import com.xiayu.service.IOrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderServiceImpl implements IOrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Override
    public boolean insertOrder(OrderEntity orderEntity) {
        return orderMapper.insertOrder(orderEntity);
    }

    @DBRouting(isRead = "true") //设置为在从库中读取数据
    @Override
    public OrderEntity getOrder(Integer orderId) {
        return orderMapper.getOrderById(orderId);
    }
}

下图为数据库的设计,仅仅定义了三个库,没有实现从库同步主库数据
使用Spring AbstractRoutingDataSource实现读写分离,实现垂直分库和水平分表_第1张图片

OrderEntity和OrderMapping

OrderEntity
--------------------------
package com.xiayu.entity;

import lombok.Data;

@Data
public class OrderEntity {

    private Integer id;

    private String orderName;

    private String orderType;
}

OrderMapping
-----------------------------------------------

package com.xiayu.mapper;

import com.xiayu.entity.OrderEntity;
import org.apache.ibatis.annotations.*;

@Mapper
public interface OrderMapper {

    String COLUMN_LIST = " id,order_name,order_type ";

    @Insert("insert into t_order( " + COLUMN_LIST + ") values(#{id},#{orderName},#{orderType})")
    boolean insertOrder(OrderEntity orderEntity);

    @Results(id = "orderResultMap",value = {
            @Result(property = "id",column = "id"),
            @Result(property = "orderName",column = "order_name"),
            @Result(property = "orderType",column = "order_type"),
    })
    @Select("select " + COLUMN_LIST + " from t_order where id = #{id}")
    OrderEntity getOrderById(Integer id);

}

代码的目录结构
使用Spring AbstractRoutingDataSource实现读写分离,实现垂直分库和水平分表_第2张图片

你可能感兴趣的:(分库分表,读写分离,spring)