首页 > 开发 > Java > 正文

Spring+MyBatis实现数据读写分离的实例代码

2024-07-13 10:09:49
字体:
来源:转载
供稿:网友

本文介绍了Spring Boot + MyBatis读写分离,有需要了解Spring+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@Componentpublic 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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持VeVb武林网。


注:相关教程知识阅读请移步到JAVA教程频道。
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表