• Spring Boot + Mybatis + Druid 动态切换多数据源


    在大型应用程序中,配置主从数据库并使用读写分离是常见的设计模式。

    在Spring应用程序中,要实现读写分离,最好不要对现有代码进行改动,而是在底层透明地支持。

    这样,就需要我们再一个项目中,配置两个,乃至多个数据源。

    今天,小编先来介绍一下自己配置动态多数据源的步骤

    项目简介:

      编译器:IDEA

      JDK:1.8

      框架:Spring Boot 2.1.0.RELEASES  + Mybatis + Druid

    一、配置数据库连接数据

    因为项目使用的是Spring Boot 框架,该框架会自动配置数据源,自动从application.properties中读取数据源信息,如果没有配置,启动时会报错,因此我们再配置自定义的数据源的时候,需要禁掉数据源的自动配置。

    但是小编在启动项目的时候,还是报错了,可是由于jdbcTemplate重复了,框架自动帮我们定义了一个jdbcTemplate,而小编自己又自定义了一个,因此,也要将这个自动配置禁止掉

    启动类方法如下:

    @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,JdbcTemplateAutoConfiguration.class})
    @MapperScan(sqlSessionTemplateRef = "jdbcTemplate")
    public class DynamicDatasourseApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(DynamicDatasourseApplication.class, args);
        }
    }

    下面开始配置自定义的数据源。

    新建jdbc.properties文件,配置数据库的连接,数据源1为写库,数据源2为读库

    jdbc.driverClassName.db=com.mysql.jdbc.Driver
    #主数据源
    jdbc.w.url=jdbc:mysql://localhost:3306/learning?characterEncoding=UTF-8&&serverTimezone=UTC
    jdbc.w.user=root
    jdbc.w.password=123456
    #从数据源
    jdbc.r.url=jdbc:mysql://localhost:3306/slave?characterEncoding=UTF-8&&serverTimezone=UTC
    jdbc.r.user=root
    jdbc.r.password=123456
    #连接池配置
    druid.initialSize=2
    druid.minIdle=30
    druid.maxActive=80
    druid.maxWait=60000
    druid.timeBetweenEvictionRunsMillis=60000
    druid.minEvictableIdleTimeMillis=300000
    druid.validationQuery=SELECT 'x'
    druid.testWhileIdle=true
    druid.testOnBorrow=false
    druid.testOnReturn=false
    druid.poolPreparedStatements=true
    druid.maxPoolPreparedStatementPerConnectionSize=20
    druid.filters=wall,stat

    建表语句:

    #数据库learning
    CREATE TABLE `a`  (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
      `age` int(11) NOT NULL,
      `gender` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
      `psw` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
      `seq` int(11) NOT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    
    
    INSERT INTO `a` VALUES (1, 'zsan', 30, 'f', '123456', 3);
    INSERT INTO `a` VALUES (2, 'lisi', 31, 'f', '123456', 5);
    INSERT INTO `a` VALUES (3, 'wangwu', 32, 'm', '123456', 1);
    INSERT INTO `a` VALUES (4, 'zhaoliu', 33, 'm', '123456', 4);
    INSERT INTO `a` VALUES (5, 'baiqi', 34, 'm', '123456', 6);
    INSERT INTO `a` VALUES (6, 'hongba', 35, 'f', '123456', 2);
    INSERT INTO `a` VALUES (7, 'zhuyl', 30, 'f', '123456', 7);
    
    #数据库slave
    CREATE TABLE `b`  (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
      `age` int(11) NOT NULL,
      `gender` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
      `psw` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
      `seq` int(11) NOT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    
    
    INSERT INTO `b` VALUES (1, 'szsan', 30, 'f', '123456', 3);
    INSERT INTO `b` VALUES (2, 'slisi', 31, 'f', '123456', 5);
    INSERT INTO `b` VALUES (3, 'swangwu', 32, 'm', '123456', 1);
    INSERT INTO `b` VALUES (4, 'szhaoliu', 33, 'm', '123456', 4);
    INSERT INTO `b` VALUES (5, 'sbaiqi', 34, 'm', '123456', 6);
    INSERT INTO `b` VALUES (6, 'shongba', 35, 'f', '123456', 2);
    INSERT INTO `b` VALUES (7, 'szhuyl', 30, 'f', '123456', 7);
    建表语句

    二、配置mybatis的属性

    在application.properties中配置mybatis的属性

    mybatis.type-aliases-package:实体类的位置,如果将实体类放到Application.java文件的同级包或者下级包时,这个属性可以不配置
    mybatis.mapper-locations:mapper.xml的位置
    mybatis.config-location:mybatis配置文件的位置,无则不填
    mybatis.type-aliases-package=cn.com.exercise.dynamicDatasourse.module.condition
    mybatis.mapper-locations=/mappers/**.xml
    mybatis.config-location=/config/sqlmap-config.xml

    三、使用Java文件读取资源数据

    1)配置主数据源(写库)

        @Bean(name = '写库名字')
        @Primary
        public DataSource master(){
            DruidDataSource source = new DruidDataSource();
            //使用source.setXxx(Yyy);进行配置
            //数据库基本属性driverClassName url、user、password配置
            //连接池基本属性配置
            return source;
        }    

    @Primary表示优先为注入的Bean,此处用来标识住数据源

    2)配置从数据源(读库),配置内容和主数据源相同

        @Bean(name = '读库名字')
        public DataSource master(){
            DruidDataSource source = new DruidDataSource();
            //使用source.setXxx(Yyy);进行配置
            //数据库基本属性driverClassName url、user、password配置
            //连接池基本属性配置
            return source;
        } 

    3)数据源支持,配置默认数据源

        @Bean(name = "dynamicDataSource")
        public DataSource dynamicDataSource(){
            DynamicDataSource dynamicRoutingDataSource = new DynamicDataSource();
            //配置多数据源
            Map<Object, Object> dataSourceMap = new HashMap<>(2);
            dataSourceMap.put("写库名字", master());
            dataSourceMap.put("读库名字", slave());
            // 将 master 数据源作为默认指定的数据源
            dynamicRoutingDataSource.setDefaultTargetDataSource(master());
            // 将 master 和 slave 数据源作为指定的数据源
            dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);
            return dynamicRoutingDataSource;
        }

    4)配置sqlSessionFactory和jdbcTemplate

    在sqlSessionFactory中,配置mybatis相关的三个内容:typeAliasesPackage,configLocation和mapperLocation,分别对应了application.properties中的三个内容,有则配置,无则省略。

        @Bean(name = "sqlSessionFactory")
        public SqlSessionFactoryBean sqlSessionFactoryBean() throws Exception{
            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setVfs(SpringBootVFS.class);
            sqlSessionFactoryBean.setTypeAliasesPackage(typeAlias);
            sqlSessionFactoryBean.setConfigLocation( new ClassPathResource(sqlmapConfigPath));
            PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
            String packageSearchPath = PathMatchingResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX+mapperLocation;
            sqlSessionFactoryBean.setMapperLocations(resolver.getResources(packageSearchPath));
            sqlSessionFactoryBean.setDataSource(dynamicDataSource());
            return  sqlSessionFactoryBean;
        }
    
        @Bean(name = "jdbcTemplate")
        public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory){
            return new SqlSessionTemplate(sqlSessionFactory);
        }

    5)配置事务传播相关内容

        @Bean(name = "transactionManager")
        public PlatformTransactionManager transactionManager() {
            DataSourceTransactionManager manager = new DataSourceTransactionManager(dynamicDataSource());
            return manager;
        }
    
        /**
         * 配置事务的传播特性
         */
        @Bean(name = "txAdvice")
        public TransactionInterceptor txAdvice(){
            TransactionInterceptor interceptor = new TransactionInterceptor();
            interceptor.setTransactionManager(transactionManager());
            Properties transactionAttributes = new Properties();
            //使用transactionAttributes.setProperty()配置传播特性
            interceptor.setTransactionAttributes(transactionAttributes);
            return interceptor;
        }
    
        @Bean(name = "txAdviceAdvisor")
        public Advisor txAdviceAdvisor() {
            AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
            String transactionExecution = "execution(* cn.com.hiveview.springboot.demoapi..service.*.*(..))";
            pointcut.setExpression(transactionExecution);
            return new DefaultPointcutAdvisor(pointcut, txAdvice());
        }

    四、动态数据源支持

    在上面配置动态数据源支持的时候,我们使用了一个类“DynamicDataSource.java”。

    这个类是自定义的类,继承了抽象类AbstractRoutingDataSource,正是通过这个抽象类来实现动态数据源的选择的。

    来看下这个抽象类的成员变量:

    private Map<Object, Object> targetDataSources;
    private Object defaultTargetDataSource;

    以下介绍可以参考上一节(3)的内容。

    【1】targetDataSources:保存了key和数据库连接的映射关系

    【2】defaultTargetDataSource:表示默认的数据库连接

    接下来就是根据这个类,实现我们自己的类DynamicDataSource.java

    public class DynamicDataSource extends AbstractRoutingDataSource {
    
        @Autowired
        private DBHelper helper;
        
        @Override
        protected Object determineCurrentLookupKey() {
            return helper.getDBType();
        }
    }

    determineCurrentLookUpKey():决定需要使用哪个数据库,这个方法需要我们自己实现

    先看一下在抽象类中,是如何使用这个方法的

        protected DataSource determineTargetDataSource() {
            Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
            Object lookupKey = this.determineCurrentLookupKey();
            DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
            if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
                dataSource = this.resolvedDefaultDataSource;
            }
    
            if (dataSource == null) {
                throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
            } else {
                return dataSource;
            }
        }

    因此我们只需在determineCurrentLookUpKey方法中,返回数据库的标志即可。

    DBHelper类也是自定义的类,数据源持有类,存放了读、写库名字,以及设置数据源类型、获取数据源类型、清除数据源类型的方法

    @Component
    public class DBHelper {
         /** 
         * 线程独立 
         */  
        private ThreadLocal<String> contextHolder = new ThreadLocal<String>();  
        
        public static final String DB_TYPE_RW = "dataSource_db01";
        public static final String DB_TYPE_R = "dataSource_db02";
      
        public String getDBType() {
            String db = contextHolder.get();  
            if (db == null) {
                db = DB_TYPE_RW;
                // 默认是读写库
            }
            return db;  
        }
      
        public void setDBType(String str) {
            contextHolder.set(str);
        }
       
        public void clearDBType() {  
            contextHolder.remove();  
        } 
    }

    五、动态切换

    1)配置注解 DS.java

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE,ElementType.METHOD})
    public @interface DS {
        String value() default "主库名字";
    }

    2)使用AOP切换

    @Aspect
    @Component
    @Order(0)
    public class DynamicDataSourceAspect {
        private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class.getName());
    
        @Autowired
        DBHelper dbHelper;
    
        /**
         * 在Mapper层添加注解,实现切换数据源
         */
        @Pointcut("execution(* cn.com.exercise.dynamicDatasourse.module..mapper.*.*(..))")
        public void dataSourcePointCut(){
        }
    
        @Before("dataSourcePointCut()")
        public void before(JoinPoint joinPoint) {
            Object target = joinPoint.getTarget();
            String method = joinPoint.getSignature().getName();
            Class<?>[] clazz = target.getClass().getInterfaces();
            Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameterTypes();
            try {
                Method m = clazz[0].getMethod(method, parameterTypes);
                //如果方法上存在切换数据源的注解,则根据注解内容进行数据源切换
                if (m != null && m.isAnnotationPresent(DS.class)) {
                    DS data = m.getAnnotation(DS.class);
                    String dataSourceName = data.value();
                    dbHelper.setDBType(dataSourceName);
                    logger.debug("current thread " + Thread.currentThread().getName() + " add " + dataSourceName + " to ThreadLocal");
                } else {
                    logger.debug("switch datasource fail,use default");
                }
            } catch (Exception e) {
                logger.error("current thread " + Thread.currentThread().getName() + " add data to ThreadLocal error", e);
            }
        }
    
        @After("dataSourcePointCut()")
        public void after(JoinPoint joinPoint){
            dbHelper.clearDBType();
        }
    }

    完成以上内容后,就可以在mapper层的方法上,添加@DS注解,来实现数据源的切换了。

    六、使用

    mapper层代码

    @Mapper
    public interface DynamicMapper {
        List<DynamicCondition> getListFromSource1();
    
        @DS(DBHelper.DB_TYPE_R)
        List<DynamicCondition> getListFromSource2();
    }

    由于写库是默认数据源,因此当不使用@DS配置数据源,以及使用@DS(“写库名字”)时,使用的都是写库。

    依次访问地址:

    http://localhost:8082/dynamic/source1,

     http://localhost:8082/dynamic/source2

    运行结果如下:

     

    按照以上步骤,就可以完成动态切换数据源了,下面附上 完整代码连接

  • 相关阅读:
    Catalan数
    C# & LINQ 对象克隆
    Rotate Image
    反转链表
    QtCreator调试程序时GDB崩溃
    Regular Expression Matching
    Wildcard Matching
    DFA与NFA
    Set Matrix Zeroes
    PCA原理
  • 原文地址:https://www.cnblogs.com/wulisz/p/10078270.html
Copyright © 2020-2023  润新知