【mysql】使用AbstractRoutingDataSource实现多数据源 与 获取mapper上注解

使用AbstractRoutingDataSource实现多数据源 与 获取mapper上注解

背景

        随着业务发展速度越来越快,数据的增长也呈现倍数级别增长,数据库的压力,对于查询和写入等所有操作,都依赖于主库,其实有一些对于时效性要求不高的场景,无需使用主库查询,可以使用从库来分摊主库的压力,提升数据库集群整体的吞吐量

处理思路

其实处理思路共有两种

1. 我们创建两个数据源,不同的数据源扫描不同的包.
        也就是查询从库或者查询主库,是通过不同的mapper文件物理隔离的,优势就是开发简单,快速搭建,不用担心侵入原有逻辑。缺点也是显而易见的,如果一个sql在某些场景下需要主库查询,另外场景需要从库查询,那么我们要写两套sql,如果有新增字段等ddl操作时,我们要改两遍sql,由于比较简单,本文不做描述。

2. 使用动态数据源配置
        在多个数据源之上,抽象出来一个虚拟的动态数据源,等到执行sql的前一步,我们才会选择哪个数据源执行,优点是能够更精细化的管控sql执行,缺点的话就是开发稍微复杂些

动态数据源的执行逻辑

我们需要先了解下spring的AbstractRoutingDataSource ,spring提供了一套数据库路由方案,我们重写determineCurrentLookupKey 方法,将想要的数据库返回即可。

以spring boot 为例

一、 mysql的基本配置

  1. 首先配置主库和从库的数据源,这个是前提

    @Bean(name = "mysqlDataSource")
    @Primary
    public DataSource mysqlDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driverClass);
        dataSource.setUrl(url);
        dataSource.setUsername(user);
        dataSource.setPassword(password);
        dataSource.setMaxActive(300);
        dataSource.setKeepAlive(true);
        dataSource.setMaxWait(60000);
        dataSource.setValidationQuery("SELECT 'x'");
        dataSource.setMinEvictableIdleTimeMillis(300000);
        dataSource.setTestWhileIdle(true);
        dataSource.setTestOnBorrow(true);
        dataSource.setTestOnReturn(false);
        return dataSource;
    }
    /**
     * 从库主库数据源
     */
    @Bean(name = "mysqlSlaveDataSource")
    public DataSource mysqlSlaveDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driverClass);
        dataSource.setUrl(url);
        dataSource.setUsername(readUser);
        dataSource.setPassword(passwordRead);
        dataSource.setMaxActive(300);
        dataSource.setKeepAlive(true);
        dataSource.setMaxWait(60000);
        dataSource.setValidationQuery("SELECT 'x'");
        dataSource.setMinEvictableIdleTimeMillis(300000);
        dataSource.setTestWhileIdle(true);
        dataSource.setTestOnBorrow(true);
        dataSource.setTestOnReturn(false);
        return dataSource;
    }
    
  2. 配置动态数据源,将多数据源注入进动态数据源,并设置默认的数据源

    @Bean(name = "dynamicDataSource")
    public DynamicDataSource dynamicDataSource(@Qualifier("mysqlDataSource") DataSource mysqlDataSource,
                                               @Qualifier("mysqlSlaveDataSource") DataSource mysqlSlaveDataSource) {
        Map<Object, Object> targetDataSources = Maps.newHashMap();
        targetDataSources.put(DataSourceEnum.MASTER, mysqlDataSource);
        targetDataSources.put(DataSourceEnum.SLAVE, mysqlSlaveDataSource);
        return new DynamicDataSource(mysqlDataSource, targetDataSources);
    }
    
  3. 将动态数据源设置到sql的sessionFactory

    @Bean(name = "mysqlSqlSessionFactory")
    @Primary
    public SqlSessionFactory masterSqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) throws Exception {
    
        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dynamicDataSource);
        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MysqlDataSourceConfig.MAPPER_LOCATION));
        // 配置
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setDefaultStatementTimeout(30);
        configuration.addInterceptor(new MybatisUMPInterceptor());
        sessionFactory.setConfiguration(configuration);
        return sessionFactory.getObject();
    }
    

二、 新增的配置

  1. 新增加库的枚举定义
package com.common.enums;

/**
 * 数据源配置枚举
 *
 * @author wangzym
 */
public enum DataSourceEnum {

    /**
     * 使用主库
     */
    MASTER(1, "主库"),
    /**
     * 使用从库
     */
    SLAVE(2, "从库");
    /**
     * /**
     * 编码
     */
    private Integer code;
    /**
     * 描述
     */
    private String desc;

    DataSourceEnum(Integer code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public Integer getCode() {
        return this.code;
    }

    public String getDesc() {
        return this.desc;
    }

    public static DataSourceEnum getByCode(Integer code) {
        if (code == null) {
            return null;
        }
        for (DataSourceEnum dataSourceEnum : DataSourceEnum.values()) {
            if (dataSourceEnum.getCode().equals(code)) {
                return dataSourceEnum;
            }
        }
        return null;
    }
}
  1. 动态数据源的定义,需要继承spring的AbstractRoutingDataSource
package com.test.datasource;

import com.alibaba.fastjson.JSON;
import com.test.SettleCheckException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

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

/**
 * 动态切换数据源类
 *
 * @author wangzym
 */
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {

    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        //不符合配置条件直接中断启动流程
        if (defaultTargetDataSource == null || targetDataSources == null || targetDataSources.isEmpty()) {
            throw new SettleCheckException("默认数据源和可切换的数据源不允许为空!请检查 spring-config-datasource-druid.xml - dynamicDataSource 对应配置");
        }
        //设置默认数据源
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        //设置用于切换的数据源
        super.setTargetDataSources(targetDataSources);
        //初始化本地的数据源记录
        super.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        log.debug("当前线程使用SQL的数据库是:{}", JSON.toJSONString(DynamicDataSourceHolder.getDataSource()));
        return DynamicDataSourceHolder.getDataSource();
    }

}
  1. 新增注解

    package com.test.datasource;
    
    
    import com.test.DataSourceEnum;
    
    import java.lang.annotation.*;
    
    /**
     * 切换数据源注解,默认连接主库
     * 要在mapper上加此注解才可以
     * <p>
    
     */
    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    @Documented
    public @interface SwitchDataSource {
        DataSourceEnum name() default DataSourceEnum.MASTER;
    }
    
  2. 拦截器改造需要拦截sql,此方法可以直接拦截sql执行,此方法要重点理解,是mybatis的拦截方案,其中Intercepts可以通过不同的类型,来进行不同的拦截,此处拦截所有执行的sql,直接获取mapper上的注解会获取不到,需要通过反射的方式,来获取,需要重点注意,此处不是本文的重点,有兴趣可以了解mybatis源码。

    package com.test.interceptor;
    
    import com.test.DuccConstants;
    import com.test.enums.DataSourceEnum;
    import com.test.datasource.DynamicDataSourceHolder;
    
    @Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
            @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
            @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),})
    @Slf4j
    public class MybatisUMPInterceptor implements Interceptor {
    
        @Value("${test.current.env:pre}")
        private static String env;
    
        static {
            env = System.getProperty("current.env", "pre");
            log.info("当前切面的环境为:{}", env);
        }
    
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            CallerInfo callerInfo = null;
            try {
                //1.处理数据源切换
                SwitchDataSource chooseDataSource = getSwitchDbAnnotation((MappedStatement) invocation.getArgs()[0]);
                if (null != chooseDataSource && DuccConstants.allowedMultiDb()) {
                    //默认会使用MASTER数据源 如果开启事务了,而且当前数据源不是MASTER,会强制切换为MASTER
                    DynamicDataSourceHolder.setDataSource(chooseDataSource.name());
                    if (TransactionSynchronizationManager.isActualTransactionActive() && DataSourceEnum.SLAVE.equals(chooseDataSource.name())) {
                        DynamicDataSourceHolder.setDataSource(DataSourceEnum.MASTER);
                    }
                } else {
                    DynamicDataSourceHolder.setDataSource(DataSourceEnum.MASTER);
                }
                //2.埋点监控
                Object[] args = invocation.getArgs();
                MappedStatement ms = (MappedStatement) args[0];
                //ump key可以自定义
                String jKey = String.format("%s_MyBatis.%s", env, ms.getId());
                if (DynamicDataSourceHolder.getDataSource() != null && DynamicDataSourceHolder.getDataSource().equals(DataSourceEnum.SLAVE)) {
                    jKey = jKey + "_slave";
                }
                callerInfo = Profiler.registerInfo(jKey, false, true);
            } catch (Exception ignore) {
                //ignore
                log.warn("切面执行出现异常:{}", ignore.getStackTrace(), ignore);
            }
            try {
                return invocation.proceed();
            } catch (Throwable e) {
                try {
                    if (null != callerInfo) {
                        Profiler.functionError(callerInfo);
                    }
                } catch (Exception ignore) {
                    //ignore
                }
                throw e;
            } finally {
                try {
                    if (null != callerInfo) {
                        Profiler.registerInfoEnd(callerInfo);
                    }
                } catch (Exception ignore) {
                    //ignore
                }
            }
        }
        private SwitchDataSource getSwitchDbAnnotation(MappedStatement mappedStatement) {
            SwitchDataSource annotation = null;
            try {
                String id = mappedStatement.getId();
                String className = id.substring(0, id.lastIndexOf("."));
                String methodName = id.substring(id.lastIndexOf(".") + 1);
                final Method[] method = Class.forName(className).getMethods();
                for (Method me : method) {
                    if (me.getName().equals(methodName) && me.isAnnotationPresent(SwitchDataSource.class)) {
                        return me.getAnnotation(SwitchDataSource.class);
                    }
                }
            } catch (Exception ex) {
                log.error("", ex);
            }
            return annotation;
        }
    
    
        @Override
        public Object plugin(Object target) {
            return Plugin.wrap(target, this);
        }
    
        @Override
        public void setProperties(Properties properties) {
    
        }
    }
    
  3. 在mapper上打注解标记

    @SwitchDataSource(name = DataSourceEnum.SLAVE)
    Long queryTaskOfShopCount(GeneralFeeDetailDTO generalFeeDetailDTO);
    

    至此,mysql 动态路由到指定数据库就讲解完成。

下面是使用 AbstractRoutingDataSource 和 MyBatis 拦截器实现动态切换数据源的示例代码: 首先,需要自定义一个继承 AbstractRoutingDataSource 的类,并实现 determineCurrentLookupKey 方法,该方法用于返回当前数据源的 key: ```java public class DynamicDataSource extends AbstractRoutingDataSource { private static final ThreadLocal<String> dataSourceKey = new InheritableThreadLocal<>(); public static void setDataSourceKey(String key) { dataSourceKey.set(key); } @Override protected Object determineCurrentLookupKey() { return dataSourceKey.get(); } } ``` 在 Spring 配置文件中需要配置两个数据源,并将 DynamicDataSource 设置为默认数据源: ```xml <bean id="dataSource1" class="org.apache.commons.dbcp2.BasicDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/db1"/> <property name="username" value="root"/> <property name="password" value="123456"/> </bean> <bean id="dataSource2" class="org.apache.commons.dbcp2.BasicDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/db2"/> <property name="username" value="root"/> <property name="password" value="123456"/> </bean> <bean id="dynamicDataSource" class="com.example.DynamicDataSource"> <property name="defaultTargetDataSource" ref="dataSource1"/> <property name="targetDataSources"> <map> <entry key="db1" value-ref="dataSource1"/> <entry key="db2" value-ref="dataSource2"/> </map> </property> </bean> ``` 接下来,需要实现一个继承于 MyBatis 的 Interceptor 接口的拦截器类,该类用于在执行 SQL 语句前切换数据源: ```java @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}) public class DynamicDataSourceInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); MetaObject metaObject = SystemMetaObject.forObject(statementHandler); MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); String dataSourceKey = getDataSourceKey(mappedStatement); if (dataSourceKey != null) { DynamicDataSource.setDataSourceKey(dataSourceKey); } return invocation.proceed(); } private String getDataSourceKey(MappedStatement mappedStatement) { String dataSourceKey = null; // 从 Mapper 方法上获取数据源 key if (mappedStatement != null) { String id = mappedStatement.getId(); if (id.startsWith("com.example.mapper1")) { dataSourceKey = "db1"; } else if (id.startsWith("com.example.mapper2")) { dataSourceKey = "db2"; } } return dataSourceKey; } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { // do nothing } } ``` 最后,需要在 Spring 配置文件中配置该拦截器: ```xml <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dynamicDataSource"/> <property name="plugins"> <array> <bean class="com.example.DynamicDataSourceInterceptor"/> </array> </property> </bean> ``` 这样,就可以在 Mapper 方法上使用 @DataSource("db1") 或 @DataSource("db2") 注解来指定使用哪个数据源了。例如: ```java @DataSource("db1") List<User> getUserList(); @DataSource("db2") int addUser(User user); ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值