美文网首页程序员
Spring+MyBatis读写分离

Spring+MyBatis读写分离

作者: 三无架构师 | 来源:发表于2017-07-12 11:47 被阅读483次

    [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<HikariConfig> read;
        private HikariConfig write;
    
        public List<HikariConfig> getRead() {
            return read;
        }
    
        public void setRead(List<HikariConfig> 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<DataSource> getDataSource(List<HikariConfig> configs) {
            List<DataSource> 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<String, DataSourceType> cache = new ConcurrentHashMap<>();
        private static final ThreadLocal<DataSourceType> holder = new ThreadLocal<>();
        private static final ThreadLocal<String> 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<DataSource> readDataSource;
        private int readDataSourceSize;
        private Map<String, String> dataSourceMapping = new ConcurrentHashMap<>();
    
        @Override
        public void afterPropertiesSet() {
            if (this.writeDataSource == null) {
                throw new IllegalArgumentException("Property 'writeDataSource' is required");
            }
            setDefaultTargetDataSource(writeDataSource);
            Map<Object, Object> 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<DataSource> 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读写分离

        本文链接:https://www.haomeiwen.com/subject/gzqbhxtx.html