SpringBoot+AOP构建多数据源的切换实践+公共库存储数据源信息+MyBatis

SpringBoot+AOP构建多数据源的切换实践+公共库存储数据源信息+MyBatis

【前言】

原本是考虑用baomidou实现的,但实际引用之后,发现不太适合我的场景,在看了一天源码和百度类似场景案例后,左抄抄,右改改,调试出适配我的案例,所以很多代码跟baomidou源码很像,也有一些做法跟百度大佬们类似,记录一下笔记,勿喷

【场景描述】

当全国业务量扩展,订单等表数据暴增之后,考虑到分库分表缓解数据库压力。

简单的说,分库分表分为两种水平分库和垂直分库,该场景很明显为水平分库,但由于分表的数目过多,且数据源多个(本文场景是按区分库,如华南区就一个分库,上海区就一个分库),所以,结合常规做法的基础上,将数据源信息存储在公共库中,在接口访问请求之后,通过aop拦截controller,获得参数值,通过该参数值转换成,以此扩展开来。

【知识储备】

1.了解AbstractRoutingDataSource

Spring boot提供了AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源,这样我们可以在执行查询之前,设置使用的数据源。实现可动态路由的数据源,在每次数据库查询操作前执行。它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源。更加具体的,大家还是去看源码或者参考其他博客,这一方面很详细。

在这里只提三个方法:

(1)determineTargetDataSource:该方法就是用来切换数据源的

(2)setTargetDataSources(Map<Object, Object> targetDataSources):这个方法大有用处,我们在启动时候,就可以通过查询公共库,将数据源的信息都查出,通过该方法进行设置

(3)afterPropertiesSet:重点列出该方法,是因为大家在调试数据源的时候,可以通过观察该方法来实现。resolvedDataSources值就是存储数据源的,也是在该方法中实现,该值还在determineTargetDataSource中用到,切数据源就是从该值中取出来的。

2.了解mybatis在springboot中的配置

在这里就不啰嗦,百度一大把。

【具体实现】

1.pom.xml

(1)数据库依赖:公司用sqlserver,mysql和oracle的话,自己百度,应该没啥难度

        <dependency>
            <groupId>com.microsoft.sqlserver</groupId>
            <artifactId>mssql-jdbc</artifactId>
            <scope>runtime</scope>
            <version>6.4.0.jre8</version>
        </dependency>
        <dependency>
            <groupId>com.microsoft.sqlserver</groupId>
            <artifactId>sqljdbc4</artifactId>
            <version>4.0</version>
            <scope>test</scope>
        </dependency>

(2)aop和mybatis

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>

2.bootstrap.yml

(1)mybtis配置:由于后面我写了MyBatisConfig配置类,所以这里的值就是一个参数而已,可以读取。当然,不写配置类,mybatis这样配置也是可以生效的

mybatis:
  mapper-locations: classpath:mapper/**/*Mapper.xml
  type-aliases-package: com.mylnet.purple.module.apply.entity

(2)数据库配置:同理,正常这样配置是可以使用的了,但是我们自己写配置,所以这里数据源信息就是参数的作用

#公共库
spring:
  datasource:
    driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
    url: jdbc:sqlserver://ip:端口;DatabaseName=公共库名
    username: *****
    password: *****
    #type: com.alibaba.druid.pool.DruidDataSource
    

3.DynamicDataSource动态数据源实现类:就是前面讲到的,实现AbstractRoutingDataSource,重写其方法,来完成我们的需求,比较重要的一步,但也是千篇一律的一步

package com.mylnet.purple.module.apply.dataSource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

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

/**
 * (切换数据源必须在调用service之前进行,也就是开启事务之前)
 * 动态数据源实现类
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    /**
     * 如果不希望数据源在启动配置时就加载好,可以定制这个方法,从任何你希望的地方读取并返回数据源
     * 比如从数据库、文件、外部接口等读取数据源信息,并最终返回一个DataSource实现类对象即可
     */
    @Override
    protected DataSource determineTargetDataSource() {
        return super.determineTargetDataSource();
    }
    /**
     * 如果希望所有数据源在启动配置时就加载好,这里通过设置数据源Key值来切换数据,定制这个方法
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceKey();
    }
    /**
     * 设置默认数据源
     * @param defaultDataSource
     */
    public void setDefaultDataSource(Object defaultDataSource) {
        super.setDefaultTargetDataSource(defaultDataSource);
    }
    /**
     * 设置数据源
     * @param dataSources
     */
    public void setDataSources(Map<Object, Object> dataSources) {
        super.setTargetDataSources(dataSources);
        // 将数据源的 key 放到数据源上下文的 key 集合中,用于切换时判断数据源是否有效
        DynamicDataSourceContextHolder.addDataSourceKeys(dataSources.keySet());
    }
}

4.DynamicDataSourceContextHolder动态上下文类:使用时候,调用这个类就行

package com.mylnet.purple.module.apply.dataSource;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * (切换数据源必须在调用service之前进行,也就是开启事务之前)
 * 动态数据源上下文
 */
public class DynamicDataSourceContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>() {
        /**
         * 将 master 数据源的 key作为默认数据源的 key
         */
        @Override
        protected String initialValue() {
            return "master";
        }
    };
    /**
     * 数据源的 key集合,用于切换时判断数据源是否存在
     */
    public static List<Object> dataSourceKeys = new ArrayList<>();
    /**
     * 切换数据源
     * @param key
     */
    public static void setDataSourceKey(String key) {
        contextHolder.set(key);
    }
    /**
     * 获取数据源
     * @return
     */
    public static String getDataSourceKey() {
        return contextHolder.get();
    }
    /**
     * 重置数据源
     */
    public static void clearDataSourceKey() {
        contextHolder.remove();
    }
    /**
     * 判断是否包含数据源
     * @param key 数据源key
     * @return
     */
    public static boolean containDataSourceKey(String key) {
        return dataSourceKeys.contains(key);
    }
    /**
     * 添加数据源keys
     * @param keys
     * @return
     */
    public static boolean addDataSourceKeys(Collection<? extends Object> keys) {
        return dataSourceKeys.addAll(keys);
    }
}

5.至此,我们就可以开始进行场景设计和开发了:结合我的场景,我需要写个MyBatisConfig配置类,去实现mybatis的配置,并在这个过程中,做一件最重要的事情,将我们的实现类DynamicDataSource通知给SqlSessionFactoryBean,这个点百度上有8成的文章没有讲,有点坑。

总结一下,该配置类做了哪些事情:

(1)将DynamicDataSource通知给SqlSessionFactoryBean

(2)配置事务管理

(3)自定义数据源,就是查询公共库,将所有分库信息查出,进而就可以通过DynamicDataSourceContextHolder进行添加数据源了

(4)配置mybatis,前面2(1)里面写的参数,就是在这里用到

(5)当然,结合我这边的场景,我要将一些数据进行缓存预热起来,便在这里处理了。

package com.mylnet.purple.module.apply;

import com.mylnet.purple.module.apply.dataSource.DynamicDataSource;
import com.mylnet.purple.module.apply.entity.DbProperties;
import com.mylnet.purple.module.apply.entity.SysDbShardingInfo;
import com.mylnet.purple.module.apply.entity.SysWahoShardingRelation;
import com.zaxxer.hikari.HikariDataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
 
/**
 * springboot集成mybatis的基本入口 1)创建数据源(如果采用的是默认的tomcat-jdbc数据源,则不需要)
 * 2)创建SqlSessionFactory 3)配置事务管理器,除非需要使用事务,否则不用配置
 * 
 * 通过读取application.properties文件生成两个数据源(myTestDbDataSource、myTestDb2DataSource)
使用以上生成的两个数据源构造动态数据源dataSource
@Primary:指定在同一个接口有多个实现类可以注入的时候,默认选择哪一个,而不是让@Autowire注解报错(一般用于多数据源的情况下)
@Qualifier:指定名称的注入,当一个接口有多个实现类的时候使用(在本例中,有两个DataSource类型的实例,需要指定名称注入)
@Bean:生成的bean实例的名称是方法名(例如上边的@Qualifier注解中使用的名称是前边两个数据源的方法名,而这两个数据源也是使用@Bean注解进行注入的)
通过动态数据源构造SqlSessionFactory和事务管理器(如果不需要事务,后者可以去掉)
 * 
 * 
 */
@Configuration
public class MyBatisConfig {
    @Autowired
    private Environment env;
    @Resource
    private DbProperties dbProperties;
    @Value("${mybatis.mapper-locations}")
    private String mapperLocations;
    @Value("${mybatis.type-aliases-package}")
    private String typeAliasesPackage;
    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 创建主数据源
     * @return
     */
    @Bean
    public DataSource master() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(dbProperties.getUrl());
        dataSource.setUsername(dbProperties.getUsername());
        dataSource.setPassword(dbProperties.getPassword());
        dataSource.setDriverClassName(dbProperties.getDriverClassName());
        return dataSource;
    }

    /**
     * 生成自定义的数据源
     * @return
     */
    @Bean("dynamicDataSource")
    @Primary
    public DataSource dynamicDataSource(){
        //数据关联关系进行缓存预热
        setDbRelationRedis();
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object,Object> mapDataSource = getBaseDataSource();
        mapDataSource.put("master", master());
        //将master数据源作为指定的数据源
        dynamicDataSource.setDefaultDataSource(master());
        dynamicDataSource.setDataSources(mapDataSource);
        return dynamicDataSource;
    }

    @Bean
    @Primary
    public SqlSessionFactoryBean sqlSessionFactory() throws IOException {
        SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
        //配置数据源,此处配置为关键配置,如果没有将dynamicDataSource作为数据源则不能实现切换
        sessionFactoryBean.setDataSource(dynamicDataSource());
        //扫描model-entity的包
        sessionFactoryBean.setTypeAliasesPackage(typeAliasesPackage);
        //扫描映射文件
        PathMatchingResourcePatternResolver pathMatchingResourcePatternResolver = new PathMatchingResourcePatternResolver();
        sessionFactoryBean.setMapperLocations(pathMatchingResourcePatternResolver.getResources(mapperLocations));
        return sessionFactoryBean;
    }

    /**
     * 配置事务管理,使用事务时哎方法头部添加@Transactional注解即可
     * @return
     */
    @Bean
    public PlatformTransactionManager transactionManager(){
        return new DataSourceTransactionManager(dynamicDataSource());
    }

    /**
     * 自定义数据源
     * @return
     */
    @Bean
    public Map<Object,Object> getBaseDataSource() {
        System.out.println("-------------------------[MyBatisConfig]-------------------------");
        System.out.println("【公共库信息:】");
        System.out.println("url:" + dbProperties.getUrl());
        System.out.println("username:" + dbProperties.getUsername());
        System.out.println("password:" + dbProperties.getPassword());
        System.out.println("driver-class-name:" + dbProperties.getDriverClassName());
        System.out.println("sql:" + dbProperties.getSql());
        Map<Object, Object> mapDataSource = new HashMap<Object, Object>();
        JdbcTemplate jdbcTemplate = new JdbcTemplate(master());
        List<SysDbShardingInfo> sysDbShardingInfoList = jdbcTemplate.query(dbProperties.getSql(), new BeanPropertyRowMapper<SysDbShardingInfo>(SysDbShardingInfo.class));
        if (null != sysDbShardingInfoList && sysDbShardingInfoList.size() > 0) {
            for (SysDbShardingInfo sydb : sysDbShardingInfoList) {
                HikariDataSource dataSource = new HikariDataSource();
                dataSource.setDriverClassName(dbProperties.getDriverClassName());
                dataSource.setJdbcUrl(sydb.getShardingURL());
                dataSource.setUsername(sydb.getShardingUserName());
                dataSource.setPassword(sydb.getShardingPassword());
                mapDataSource.put(sydb.getShardingAlias(), dataSource);
            }
        }
        return mapDataSource;
    }

    /**
     * 预热缓存:将仓库id/sapid/组织对应数据源信息的关系缓存起来,每次接口访问的时候,通过缓存找到对应的数据源名称
     */
    @Bean
    public void setDbRelationRedis() {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(master());
        String sql = "select wahoID,wahoSapID,organId,shardingAlias from SysWahoShardingRelation";
        List<SysWahoShardingRelation> sysWahoShardingRelationList = jdbcTemplate.query(sql, new BeanPropertyRowMapper<SysWahoShardingRelation>(SysWahoShardingRelation.class));
        if (null != sysWahoShardingRelationList && sysWahoShardingRelationList.size() > 0) {
            for (SysWahoShardingRelation relation : sysWahoShardingRelationList) {
                redisTemplate.opsForValue().set("org-" + relation.getOrganId(), relation.getShardingAlias());
                redisTemplate.opsForValue().set("waho-" + relation.getWahoID(), relation.getShardingAlias());
                redisTemplate.opsForValue().set("sap-" + relation.getWahoSapID(), relation.getShardingAlias());
            }
        }
    }

}

6.启动类两个注解,别漏了,这里就不过多解释了

@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
@MapperScan(basePackages = { "com.mylnet.purple.module.apply.mapper" }, sqlSessionFactoryRef = "sqlSessionFactory")

7.最后一步,就是实战,也就是aop部分

备注说明,(1)我这里拦截controller(2)解析出来参数的值,因为我们这边有三个参数值是跟数据源有关的,任何一个查到对应的数据源名称即可,进而通过数据源名称就可以设置当前数据源了,这样便完成数据源的切换

package com.mylnet.purple.module.apply.dataSource;

import com.mylnet.purple.module.apply.entity.DbProperties;
import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
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.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

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

/**
 * 接口拦截aop,进行数据源设置
 * fangzw
 * 2021-04-26
 */
@Slf4j
@Aspect
@Component
@Order(1)
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class DynamicDataSourceAspect {
    @Resource
    private DbProperties dbProperties;
    @Resource
    private RedisTemplate redisTemplate;

    @Pointcut("execution(* com.mylnet.purple.module.apply.controller.wcs.*.*(..)) ")
    public void pointcut() {}

    @Before("pointcut()")
    public void doBefore(JoinPoint joinPoint) {
        // 参数值
        Object[] args = joinPoint.getArgs();
        // 参数名
        String[] argNames = ((MethodSignature)joinPoint.getSignature()).getParameterNames();
        if (null != args) {
            //仓库id索引
            int wahoIDIndex = ArrayUtils.indexOf(argNames, "wahoID");
            //sap的id索引
            int wahoSapIDIndex = ArrayUtils.indexOf(argNames, "wahoSapID");
            //所属组织id索引
            int organIdIndex = ArrayUtils.indexOf(argNames, "organId");
            //通过仓库id/sapid/organid查询对应所属区,该区名为数据源名
            StringBuffer queryDataSourceNameSql = new StringBuffer();
            String key = null;
            if (-1 != wahoIDIndex && StringUtils.isNotBlank((String)args[wahoIDIndex])) {
                //仓库id
                String wahoID = (String)args[wahoIDIndex];
                queryDataSourceNameSql.append("select shardingAlias from SysWahoShardingRelation where wahoID = '" + wahoID + "'");
                key ="waho-" + wahoID;
            } else if (-1 != wahoSapIDIndex && StringUtils.isNotBlank((String)args[wahoSapIDIndex])) {
                //sapid
                String wahoSapID = (String)args[wahoSapIDIndex];
                queryDataSourceNameSql.append("select shardingAlias from SysWahoShardingRelation where wahoSapID = '" + wahoSapID + "'");
                key = "sap-" + wahoSapID;
            } else if (-1 != organIdIndex && StringUtils.isNotBlank((String)args[organIdIndex])) {
                //sapid
                String organId = (String)args[organIdIndex];
                queryDataSourceNameSql.append("select shardingAlias from SysWahoShardingRelation where organId = '" + organId + "'");
                key = "org-" + organId;
            } else {
                throw new RuntimeException("接口不规范,必须仓库id/sapid/organid三个参数必须至少有一个值!");
            }
            //执行查询:先通过缓存查询,查不到再进行数据查询
            if (key != null) {
                String dataSourceName = null;
                Object dataSourceNameRedis = redisTemplate.opsForValue().get(key);
                if (null != dataSourceNameRedis) {
                    dataSourceName = (String) dataSourceNameRedis;
                } else {
                    JdbcTemplate jdbcTemplate = new JdbcTemplate(pubDataSource());
                    //数据源名称
                    dataSourceName = jdbcTemplate.queryForObject(queryDataSourceNameSql.toString(), String.class);
                    if (null == dataSourceName || !DynamicDataSourceContextHolder.containDataSourceKey(dataSourceName)) {
                        throw new RuntimeException("数据源不存在!dataSourceName为:" + dataSourceName);
                    }
                }
                log.info("------------------设置当前数据源------------------------------");
                log.info(dataSourceName);
                DynamicDataSourceContextHolder.setDataSourceKey(dataSourceName);
            }
        }
    }

    @After("pointcut()")
    public void after(JoinPoint point) {
        //清理掉当前设置的数据源,让默认的数据源不受影响
        DynamicDataSourceContextHolder.clearDataSourceKey();
    }

    /**
     * 公共库数据源
     * @return
     */
    public DataSource pubDataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(dbProperties.getUrl());
        dataSource.setUsername(dbProperties.getUsername());
        dataSource.setPassword(dbProperties.getPassword());
        dataSource.setDriverClassName(dbProperties.getDriverClassName());
        return dataSource;
    }

}

最后,项目不能对外提供,有疑问请留言!

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
Spring Boot 是一个用于快速构建 Java 应用程序的框架。它可以与多种其他框架和组件进行整合,以实现更丰富的功能。在这里,我们将讨论如何使用 Spring Boot 整合 Druid、MyBatis、JTA 分布式事务以及多数据源,同时使用 AOP 注解实现动态切换。 首先,我们可以在 Spring Boot 中集成 Druid 数据源。Druid 是一个高性能的 JDBC 连接池,可以提供监控和统计功能。我们可以通过在 pom.xml 文件中添加相关的依赖,并在 application.properties 文件中配置数据源信息,来实现 Druid 的集成。 接下来,我们可以整合 MyBatis 框架,它是一种优秀的持久化解决方案。我们可以使用 MyBatis 来操作数据,并将其与 Druid 数据源进行整合。为此,我们需要在 pom.xml 文件中添加 MyBatisMyBatis-Spring 的依赖,并配置 MyBatis 的相关配置文件。 此外,我们还可以使用 JTA(Java Transaction API)实现分布式事务。JTA 可以在分布式环境中协调多个参与者的事务操作。我们可以在 pom.xml 文件中添加 JTA 的依赖,并在 Spring Boot 的配置文件中配置 JTA 的相关属性,以实现分布式事务的支持。 在实现多数据源时,我们可以使用 Spring Boot 的 AbstractRoutingDataSource 来实现动态切换数据源。这个类可以根据当前线程或其他条件选择不同的数据源来进行数据操作。我们可以通过继承 AbstractRoutingDataSource 并实现 determineCurrentLookupKey() 方法来指定当前数据源的 key。然后,在配置文件中配置多个数据源,并将数据源注入到 AbstractRoutingDataSource 中,从而实现动态切换。 最后,我们可以使用 AOP(Aspect Oriented Programming)注解来实现动态切换AOP 是一种编程范式,可以通过在代码中插入特定的切面(Aspect)来实现横切关注点的处理。我们可以在代码中使用注解来标记需要切换数据源的方法,然后使用 AOP 技术来拦截这些方法,并根据注解中指定的数据源信息来进行数据源切换。 综上所述,通过整合 Druid、MyBatis、JTA 分布式事务以及多数据源,并使用 AOP 注解实现动态切换,我们可以在 Spring Boot 中实现强大而灵活的应用程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值