Spring+MyBatis读写分离

[TOC]

Spring Boot + MyBatis读写分离

其最终实现功能:

  1. 默认更新操作都使用写数据源
  2. 读操作都使用slave数据源
  3. 特殊设置:可以指定要使用的数据源类型及名称(如果有名称,则会根据名称使用相应的数据源)

其实现原理如下:

  1. 通过Spring AOP对dao层接口进行拦截,并对需要指定数据源的接口在ThradLocal中设置其数据源类型及名称
  2. 通过MyBatsi的插件,对根据更新或者查询操作在ThreadLocal中设置数据源(dao层没有指定的情况下)
  3. 继承AbstractRoutingDataSource类。

在此直接写死使用HikariCP作为数据源

其实现步骤如下:

  1. 定义其数据源配置文件并进行解析为数据源
  2. 定义AbstractRoutingDataSource类及其它注解
  3. 定义Aop拦截
  4. 定义MyBatis插件
  5. 整合在一起

1.配置及解析类

其配置参数直接使用HikariCP的配置,其具体参数可以参考HikariCP。

在此使用yaml格式,名称为datasource.yaml,内容如下:

dds:
  write:
    jdbcUrl: jdbc:mysql://localhost:3306/order
    password: liu123
    username: root
    maxPoolSize: 10
    minIdle: 3
    poolName: master
  read:
    - jdbcUrl: jdbc:mysql://localhost:3306/test
      password: liu123
      username: root
      maxPoolSize: 10
      minIdle: 3
      poolName: slave1
    - jdbcUrl: jdbc:mysql://localhost:3306/test2
      password: liu123
      username: root
      maxPoolSize: 10
      minIdle: 3
      poolName: slave2

定义该配置所对应的Bean,名称为DBConfig,内容如下:

@Component
@ConfigurationProperties(locations = "classpath:datasource.yaml", prefix = "dds")
public class DBConfig {
    private List read;
    private HikariConfig write;

    public List getRead() {
        return read;
    }

    public void setRead(List read) {
        this.read = read;
    }

    public HikariConfig getWrite() {
        return write;
    }

    public void setWrite(HikariConfig write) {
        this.write = write;
    }
}

把配置转换为DataSource的工具类,名称:DataSourceUtil,内容如下:

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.List;

public class DataSourceUtil {
    public static DataSource getDataSource(HikariConfig config) {
        return new HikariDataSource(config);
    }

    public static List getDataSource(List configs) {
        List result = null;
        if (configs != null && configs.size() > 0) {
            result = new ArrayList<>(configs.size());
            for (HikariConfig config : configs) {
                result.add(getDataSource(config));
            }
        } else {
            result = new ArrayList<>(0);
        }

        return result;
    }
}

2.注解及动态数据源

定义注解@DataSource,其用于需要对个别方法指定其要使用的数据源(如某个读操作需要在master上执行,但另一读方法b需要在读数据源的具体一台上面执行)

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataSource {
    /**
     * 类型,代表是使用读还是写
     * @return
     */
    DataSourceType type() default DataSourceType.WRITE;

    /**
     * 指定要使用的DataSource的名称
     * @return
     */
    String name() default "";
}

定义数据源类型,分为两种:READ,WRITE,内容如下:

public enum DataSourceType {
    READ, WRITE;
}

定义保存这此共享信息的类DynamicDataSourceHolder,在其中定义了两个ThreadLocal和一个map,holder用于保存当前线程的数据源类型(读或者写),pool用于保存数据源名称(如果指定),其内容如下:

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class DynamicDataSourceHolder {
    private static final Map cache = new ConcurrentHashMap<>();
    private static final ThreadLocal holder = new ThreadLocal<>();
    private static final ThreadLocal pool = new ThreadLocal<>();

    public static void putToCache(String key, DataSourceType dataSourceType) {
        cache.put(key,dataSourceType);
    }

    public static DataSourceType getFromCach(String key) {
        return cache.get(key);
    }

    public static void putDataSource(DataSourceType dataSourceType) {
        holder.set(dataSourceType);
    }

    public static DataSourceType getDataSource() {
        return holder.get();
    }

    public static void putPoolName(String name) {
        if (name != null && name.length() > 0) {
            pool.set(name);
        }
    }

    public static String getPoolName() {
        return pool.get();
    }

    public static void clearDataSource() {
        holder.remove();
        pool.remove();
    }
}

动态数据源类为DynamicDataSoruce,其继承自AbstractRoutingDataSource,可以根据返回的key切换到相应的数据源,其内容如下:

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;

public class DynamicDataSource extends AbstractRoutingDataSource {
    private DataSource writeDataSource;
    private List readDataSource;
    private int readDataSourceSize;
    private Map dataSourceMapping = new ConcurrentHashMap<>();

    @Override
    public void afterPropertiesSet() {
        if (this.writeDataSource == null) {
            throw new IllegalArgumentException("Property 'writeDataSource' is required");
        }
        setDefaultTargetDataSource(writeDataSource);
        Map targetDataSource = new HashMap<>();
        targetDataSource.put(DataSourceType.WRITE.name(), writeDataSource);
        String poolName = ((HikariDataSource)writeDataSource).getPoolName();
        if (poolName != null && poolName.length() > 0) {
            dataSourceMapping.put(poolName,DataSourceType.WRITE.name());
        }
        if (this.readDataSource == null) {
            readDataSourceSize = 0;
        } else {
            for (int i = 0; i < readDataSource.size(); i++) {
                targetDataSource.put(DataSourceType.READ.name() + i, readDataSource.get(i));
                poolName = ((HikariDataSource)readDataSource.get(i)).getPoolName();
                if (poolName != null && poolName.length() > 0) {
                    dataSourceMapping.put(poolName,DataSourceType.READ.name() + i);
                }
            }
            readDataSourceSize = readDataSource.size();
        }
        setTargetDataSources(targetDataSource);
        super.afterPropertiesSet();
    }


    @Override
    protected Object determineCurrentLookupKey() {
        DataSourceType dataSourceType = DynamicDataSourceHolder.getDataSource();
        String dataSourceName = null;
        if (dataSourceType == null ||dataSourceType == DataSourceType.WRITE || readDataSourceSize == 0) {
            dataSourceName = DataSourceType.WRITE.name();
        } else {
            String poolName = DynamicDataSourceHolder.getPoolName();
            if (poolName == null) {
                int idx = ThreadLocalRandom.current().nextInt(0, readDataSourceSize);
                dataSourceName = DataSourceType.READ.name() + idx;
            } else {
                dataSourceName = dataSourceMapping.get(poolName);
            }
        }
        DynamicDataSourceHolder.clearDataSource();
        return dataSourceName;
    }

    public void setWriteDataSource(DataSource writeDataSource) {
        this.writeDataSource = writeDataSource;
    }

    public void setReadDataSource(List readDataSource) {
        this.readDataSource = readDataSource;
    }
}

3.AOP拦截

如果在相应的dao层做了自定义配置(指定数据源),则在些处理。解析相应方法上的@DataSource注解,如果存在,并把相应的信息保存至上面的DynamicDataSourceHolder中。在此对
com.hfjy.service.order.dao包进行做拦截。内容如下:

import com.hfjy.service.order.anno.DataSource;
import com.hfjy.service.order.wr.DynamicDataSourceHolder;
import org.aspectj.lang.JoinPoint;
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;

/**
 * 使用AOP拦截,对需要特殊方法可以指定要使用的数据源名称(对应为连接池名称)
 */
@Aspect
@Component
public class DynamicDataSourceAspect {

    @Pointcut("execution(public * com.hfjy.service.order.dao.*.*(*))")
    public void dynamic(){}

    @Before(value = "dynamic()")
    public void beforeOpt(JoinPoint point) {
        Object target = point.getTarget();
        String methodName = point.getSignature().getName();
        Class[] clazz = target.getClass().getInterfaces();
        Class[] parameterType = ((MethodSignature)point.getSignature()).getMethod().getParameterTypes();
        try {
            Method method = clazz[0].getMethod(methodName,parameterType);
            if (method != null && method.isAnnotationPresent(DataSource.class)) {
                DataSource datasource = method.getAnnotation(DataSource.class);
                DynamicDataSourceHolder.putDataSource(datasource.type());
                String poolName = datasource.name();
                DynamicDataSourceHolder.putPoolName(poolName);
                DynamicDataSourceHolder.putToCache(clazz[0].getName() + "." + methodName, datasource.type());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @After(value = "dynamic()")
    public void afterOpt(JoinPoint point) {
        DynamicDataSourceHolder.clearDataSource();
    }
}

4.MyBatis插件

如果在dao层没有指定相应的要使用的数据源,则在此进行拦截,根据是更新还是查询设置数据源的类型,内容如下:

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.util.Properties;

@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
                RowBounds.class, ResultHandler.class})
})
public class DynamicDataSourcePlugin implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement ms = (MappedStatement)invocation.getArgs()[0];
        DataSourceType dataSourceType = null;
        if ((dataSourceType = DynamicDataSourceHolder.getFromCach(ms.getId())) == null) {
            if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
                dataSourceType = DataSourceType.READ;
            } else {
                dataSourceType = DataSourceType.WRITE;
            }
            DynamicDataSourceHolder.putToCache(ms.getId(), dataSourceType);
        }
        DynamicDataSourceHolder.putDataSource(dataSourceType);
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

5.整合

在里面定义MyBatis要使用的内容及DataSource,内容如下:

import com.hfjy.service.order.wr.DBConfig;
import com.hfjy.service.order.wr.DataSourceUtil;
import com.hfjy.service.order.wr.DynamicDataSource;
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.core.io.ClassPathResource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.annotation.Resource;
import javax.sql.DataSource;

@Configuration
@MapperScan(value = "com.hfjy.service.order.dao", sqlSessionFactoryRef = "sqlSessionFactory")
public class DataSourceConfig {
    @Resource
    private DBConfig dbConfig;

    @Bean(name = "dataSource")
    public DynamicDataSource dataSource() {
        DynamicDataSource dataSource = new DynamicDataSource();
        dataSource.setWriteDataSource(DataSourceUtil.getDataSource(dbConfig.getWrite()));
        dataSource.setReadDataSource(DataSourceUtil.getDataSource(dbConfig.getRead()));
        return dataSource;
    }

    @Bean(name = "transactionManager")
    public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier("dataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
        sessionFactoryBean.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
        sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath*:mapper/*.xml"));
        sessionFactoryBean.setDataSource(dataSource);
        return sessionFactoryBean.getObject();
    }
}

如果不清楚,可以查看github上源码orderdemo

你可能感兴趣的:(Spring+MyBatis读写分离)