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<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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,637评论 18 139
  • 1. 简介 1.1 什么是 MyBatis ? MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的...
    笨鸟慢飞阅读 5,461评论 0 4
  • 操作类型枚举 自定义注解 上下文工具 AOP拦截 动态数据源 测试 配置 测试方法 DAO 调用
    donglq阅读 213评论 0 0
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,778评论 6 342
  • 想当初/分手/的时候, 我是/苦苦的/哀求, 你/不要/走, 留下/我一个/好难受。 没有你的/日子 我哭个够 我...
    康真阅读 102评论 0 0