Springboot入门系列教程(4)-数据库操作之mybatis(含多数据源的自动切换方案)

7 篇文章 1 订阅
1 篇文章 0 订阅

一、在介绍mybatis的使用之前,先接续上一篇JPA的使用,进行两者的相关简单对比。

1、 mybatis有个优势是,如果接收结果集中的属性没有找到相应的返回数据库字段,不会报错,将赋一个空值,而JPA会报错。

2、 mybatis可以单独的通过@Restult进行结果集中数据库字段与类对象属性的映射;

3、 mybatis不能像JPA一样,在接收对象里面再写一个对象属性来接收其他表的数据;

4、 mybatis的缺点则是返回结果集是数据库的原生字段,接收的结果集属性必须是直接对应数据库原生字段,当然可以为驼峰命名、或者sql返回别名以及map字段映射的方式。

5、 mybatis接受数据的类所有属性都必须是基础数据或者是基础数据的包装类,不能是自定义的对象

6、 如果要让insert ignore into 属性生效,那需要为不能重复的字段添加索引的唯一性约束(unique)。

7、 @Resource是JDK原生的注解,默认是根据bean的类名来识别的,@Autowired是spring框架的注解,默认是通过类型来识别的。mybatis在注入mapper的时候要用@Resource进行引入。当然也可以再mapper上在加一个service的注解,然后就可以通过@Autowired进行类型注入。

 

二、配置双数据源或多数据源,一般有两种方式。一种是动态设置数据源,一种是分包的方式写死数据源和mapper文件。

1 双数据源设置后,默认配置的数据源会报错,因为会存在多个同类型的bean,程序无法识别,需要单独对数据源进行配置注解别名。
2 在新的数据源配置文件的注解上,必须要配置改数据源扫描的Mapper文件,否则mapper文件也不知道用那个数据源,会报错
3 在新的数据源配置文件的注解上,必须要配置sqlSessionFactory的bean名字和sqlTemplete的bean名字。不然也会报错,识别不了
4 注解中引用的sqlSessionFactory和sqlTemplete,只需要引用其中一个就可以了,当然引用两个也没有问题,日志会有个重复引用的告警信息。
5 多个框架同时使用的时候也要注意,DataSocurce这个Bean被实例化话了多个后,如果对数据源进行别名的注解,也会造成无法识别。比如我下面的例子中实际上同时进入了hibernate和mybatis的依赖,可以根据需要进行调用。
6 Springboot2.2.5以后的类,数据源改用了建造者模式,无法直接通过注解导入属性后映射到数据源对象,需要手动一个一个设置导入的属性。也可以直接返回一个hirika池的数据源后者druid池的数据源。这连各需要通过new 实例化对象的数据源可以直接导入注解配置的属性。
7 mybatis双数据源配置完成后,分别写对应的mapper文件,在数据源配置文件中mapperscan需要应用的mapper。然后再在应用中注入mapper进行业务调用
8 mybatis双数据配置后,会造成原来的mybatis的配置也失效了。这时候需要重新继承org.apache.ibatis.session.Configuration 并且导入原有的yml文件的配置。然后将这个配置注入到新的数据源配置文件中的sqlSessionFactory中,具体为sqlSessionFactoryBean.SetConfiguration方法。

 

三、 采用AOP的方式来创建动态数据源
1 注意,新创建的子线程并不能调用到主线程注入的bean,直接调用为报null错误。需要先集成thread内,在thread类进行注入,或者通过全局静态类的方式在线程内获取IOC容器中的bean。然后再run方法进行调用,这个错误是在跑多数据源切换时,new 了多个线程来模拟并发的时候发生的。

2 动态的数据源可以通过继承AbstractRoutingDataSource抽象类,通过determineCurrentLookupKey()返回当前要调用的数据源的key来实现动态的数据源调用。

3 继承AbstractRoutingDataSource类,需要注入两个关键的bean,一个是默认的数据源,一个是可选数据源的集合,这个集合需要通过HashMap的数据结构来保存。注入的Bean可以通过构造函数方式进行注入,因为注入后需要SET给原来抽象类的相应属性

4 动态的数据源写好了,因为使用了自定义的数据源配置,因此和上面双数据源配置一样,这个数据源还需要注入sessionFactory和事务的bean。当然factory和tempelte的bean可以二选一在数据源配置类文件上面进行MapperScan注解引用。@MapperScan(basePackages = "com.ywcai.demo.doubleDs.aop", sqlSessionFactoryRef = "aopSqlSessionFactory", sqlSessionTemplateRef = "aopSqlSessionTemplate")。当然两个都指定了也没错,只是会有个告警信息。其他就是在这个配置中需要注入你上面第3步写好的动态数据源。另外还有些关于mybatis的全局配置,也需要单独应用后,在工厂的bean中进行设置。

5 新建一个工厂,然后设置数据源和mybatis的全局配置即可。

  aopSqlSessionFactoryBean.setDataSource(aopRoutingDataSource);

  aopSqlSessionFactoryBean.setConfiguration(myBatisGlobalConfig);

6 接下来就是写一个调度数据源的类,这个也可以注解为一个bean,本例子bean类名为DBConetxtHolder。全局只要1个该类进行调度,但bean可以针对不同调用的线程保存独立的局部变量,以及保存所有线程调用数据源的总次数,进而可以动态的对数据源进行负载均衡。而要在这个bean里面保存个线程的局部变量,则需要利用ThreadLocal,为每个线程在ThreadLocal中保存一个自己的不变的枚举值。

7 通过AOP将Mapper作为切面,对每个包含get的方法前面切入数据源切换方法。既setSlave方法===>>>将DBConetxtHolder中的ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>()中的contextHolder变量设置为为DBTypeEnum.Slave。每个线程对这个contextHolder 的操作都将单独被保存,不会被其他并发的现成所影响。

8 其他的就是AOP的常规操作注解及切入方式。将aop包里面的mapper作为切入点。入参规则 ,第一个*号代表匹配所有返回结果类型。第二个*号代表匹配所有该包下的所有类。第三个*号代表匹配所有方法,()号里面的两个点代表所有参数。第三星号和第二个型号之间2个点则表示匹配这个包根目录及子目录所有的包和类

9 mapper接口文件上的注解,如果只进行mapper注解,bean是默认通过名字进行注册的,需要再注解@service或者@Commpanet等,按照type进行装配。然后再调用的类中使用@Autowired才能注入获取的到引用。

10 springboot 2.2.5默认使用了hikariPool的连接池,如果多线程同时调用,会涉及到数据库池的调用,因此必须完成池的配置。需要单独导入hikariPool的配置进行初始化,不然报错。

11 目前采用的方式是直接初始化为hikari作为数据源进行初始化

12 另外在做多线程测试时候,一定要注意阻塞线程,不然子线程启动完成后,主线程就直接退出虚拟机了。

13 测试的时候可以用countdownlatch来阻塞主线程,监测所有子线线程是否执行完。也可以对阻塞每个子线程,让子线程顺序执行。也可以通过CompletionService的子类ExecutorCompletionService提交相应的callable。 ExecutorCompletionService则可以通过take().get()方法取出来已经执行完成的线程。而不是按照启动顺序去取,造成不必要的阻塞,这样先执行完的可以先返回结果。
 
四、下面将结合AOP方式设置双数据源并进行hikari池配置,以及如何在本地测试类中进行方法调用来对mybatis的使用进行介绍。
1、先看下依赖的引入,主要是仍然是下面的依赖。
<dependency>

    <groupId>mysql</groupId>

    <artifactId>mysql-connector-java</artifactId>

</dependency>

<dependency>

    <groupId>org.mybatis.spring.boot</groupId>

    <artifactId>mybatis-spring-boot-starter</artifactId>

    <version>2.1.2</version>

</dependency>
 
2、yml文件的主要配置,因为本次是自定义配置了数据源,因此大部分的数据操作相关配置都需要重新进行导入和处理。
#配置通过mybatis的驼峰命名,但是本次示例是自定义了数据源,因此这个配置需要单独导入。 

mybatis:

  configuration:

    map-underscore-to-camel-case: true


#下面通过配置双数据的方式来访问进行主从访问。

#hikari数据库连接池配置.springboot2.0后默认使用了hikari连接池

#本次代码示例在注入数据源时直接将数据源配置为了hikari的数据源

ywcai:

  master:

    datasource:

      driver-class-name: com.mysql.cj.jdbc.Driver

      username: root

      password: xxxxxx

      jdbc-url: jdbc:mysql://xx.xx.xx.xx:3306/ywcai?characterEncoding=utf-8&serverTimezone=UTC
 #这里需要注意,hikari对应的数据库连接地址的属性是jdbcUrl,因此对应的配置属性是jdbc-url而不是用url

      maximum-pool-size: 20   #默认是-1 既没有创建池。

      auto-commit: true

      minimum-idle: 5

      pool-name: masterPools



  slave:

    datasource:

      driver-class-name: com.mysql.cj.jdbc.Driver

      username: root

      password: xxxxxxx

      jdbc-url: jdbc:mysql://xx.x.xx.xx:3306/ywcai?characterEncoding=utf-8&serverTimezone=UTC

      maximum-pool-size: 20   #默认是-1 既没有创建池。

      auto-commit: true

      minimum-idle: 5

      pool-name: slavePools
 
 
3、主数据源、重数据源的配置文件

主数据源配置类
package com.ywcai.demo.doubleDs;


@Slf4j

@Configuration

@MapperScan(basePackageClasses = {MasterMapper.class}

        , sqlSessionTemplateRef = "masterSqlSessionTemplate")

public class MasterDsConfig {

//这里将mybatis的配置单独导入后分别注入到master和salve的数据源配置中。
    @Autowired
    MyBatisGlobalConfig myBatisGlobalConfig;

    @Bean(name = "masterDataSource")
    @Qualifier(value = "masterDataSource")
    @ConfigurationProperties(prefix = "ywcai.master.datasource")
    public HikariDataSource dataSource() {
        return  new HikariDataSource();
    }


    //这是通过属性注入的方式
    @Bean(name = "masterSqlSessionFactory")
    @Qualifier(value = "masterSqlSessionFactory")
    public SqlSessionFactory masterSqlSessionFactory(@Qualifier("masterDataSource") DataSource masterDataSource)
            throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(masterDataSource);
        //导入了原来的mybatis的配置
        bean.setConfiguration(myBatisGlobalConfig);
        //使用注解方式,这里就不用注入本地xml映射文件了。类上面已经导入了mapper包或者类
//        bean.setMapperLocations(
//                // 设置mybatis的xml所在位置
//                new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/test01/*.xml"));
        return bean.getObject();
    }

    @Bean(name = "masterSqlSessionTemplate")
    @Qualifier(value = "masterSqlSessionTemplate")
    public SqlSessionTemplate masterSqlSessionTemplate(@Qualifier("masterSqlSessionFactory")
                                                               SqlSessionFactory masterSqlSessionFactory) {
        return new SqlSessionTemplate(masterSqlSessionFactory);
    }

    @Bean(name = "masterTransactionManager")
    @Qualifier(value = "masterTransactionManager")
    public DataSourceTransactionManager transactionManager
(@Qualifier("masterDataSource") DataSource masterDataSource) {
        return new DataSourceTransactionManager(masterDataSource);
    }
}
 
Slave数据源的配置类
package com.ywcai.demo.doubleDs;


@Slf4j
@Configuration
@MapperScan(basePackageClasses = {SlaveMapper.class}
        , sqlSessionFactoryRef = "slaveSqlSessionFactory")
public class SlaveDsConfig {


//导入mybatis的配置后,注入配置
    @Autowired
    MyBatisGlobalConfig myBatisGlobalConfig;

    @Bean(name = "slaveDataSource")
    @Qualifier(value = "slaveDataSource")
    @ConfigurationProperties(prefix = "ywcai.slave.datasource")
    public HikariDataSource dataSource() {
        return new HikariDataSource();
    }

    //这是通过属性注入的方式
    @Bean(name = "slaveSqlSessionFactory")
    @Qualifier(value = "slaveSqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("slaveDataSource") DataSource slaveDataSource)
            throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(slaveDataSource);
        bean.setConfiguration(myBatisGlobalConfig);
        //使用注解方式,这里就不用注入本地xml映射文件了。类上面已经导入了mapper包或者类
//        bean.setMapperLocations(
//                // 设置mybatis的xml所在位置
//                new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/test01/*.xml"));
        return bean.getObject();
    }


    @Bean(name = "slaveSqlSessionTemplate")
    @Qualifier(value = "slaveSqlSessionTemplate")
    public SqlSessionTemplate sqlSessionTemplate
(@Qualifier("slaveSqlSessionFactory") SqlSessionFactory slaveSqlSessionFactory) {
        return new SqlSessionTemplate(slaveSqlSessionFactory);
    }

    @Bean(name = "slaveTransactionManager")
    @Qualifier(value = "slaveTransactionManager")
    public DataSourceTransactionManager transactionManager
(@Qualifier("slaveDataSource") DataSource slaveDataSource) {
        return new DataSourceTransactionManager(slaveDataSource);
    }

}
单独配置一个自动实例化的MyBatisGlobalConfig.class
package com.ywcai.demo.doubleDs;


//只需要继承父类,导入配置,实例化bean即可
@Configuration
@ConfigurationProperties("mybatis.configuration")
public class MyBatisGlobalConfig extends org.apache.ibatis.session.Configuration {

    /**

     * @描述 采用自定义配置后,还是需要把yml文件里面的关于batis开头的配置导入过来。

     * @创建人 jimi

     * @参数

     * @返回值

     * @创建时间 2020/3/15

     */
}
动态数据源配置
package com.ywcai.demo.doubleDs.aop;


//动态数据源的配置,需要继承AbstractRoutingDataSource类,并重写determineCurrentLookupKey()
//这里需要实例化动态数据源
@Configuration
@Slf4j
public class AopRoutingDataSource extends AbstractRoutingDataSource {
    @Autowired
    DBContextHolder dbContextHolder;
    @Override
    protected Object determineCurrentLookupKey() {
        return dbContextHolder.get();
    }

    @Autowired
    public AopRoutingDataSource(
@Qualifier(value = "masterDataSource") HikariDataSource  masterDataSource,
@Qualifier(value = "slaveDataSource") HikariDataSource slaveDataSource) {
        setDefaultTargetDataSource(masterDataSource);
        log.info("masterDataSource  {}", masterDataSource.getHikariPoolMXBean());
        log.info("slaveDataSource  {}", slaveDataSource);
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DBTypeEnum.MASTER, masterDataSource);
        targetDataSources.put(DBTypeEnum.SLAVE, slaveDataSource);
        setTargetDataSources(targetDataSources);
    }

}
 
通过固定的两个静态方法来指定当前线程的数据源,利用threadLocal方法存储当前线程内的枚举值,保证数据源切换不被其他并发线程所影响。DBContextHolder.class
package com.ywcai.demo.doubleDs.aop;


@Service
@Slf4j
public class DBContextHolder {

    private ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>();
    //这个用来计算当前累加的访问数据库的数量,保证数据的有序增长。在Aop中进行自增加。
    private AtomicInteger counter = new AtomicInteger(-1);
    public void set(DBTypeEnum type) {
        contextHolder.set(type);
    }

    public DBTypeEnum get() {
        return contextHolder.get();
    }



   //如果是修改数据,则只会在主库做操作
    public void setMaster() {
        set(DBTypeEnum.MASTER);
    }

    //如果是查询数据,则是在主库查一次、从库查一次,做负载均衡
    public void setSlave() {
        //因为初始值为-1,因此这里先自增,然后再取2的模,将先再主库查询
        int a = counter.incrementAndGet();
        if (a % 2 == 0) {
            log.info("=============select master ============{}", a);
            set(DBTypeEnum.MASTER);
        } else {
            log.info("=============select slave ============{}", a);
            set(DBTypeEnum.SLAVE);
        }
    }
}
 
标记是那个数据源的枚举类
package com.ywcai.demo.doubleDs.aop;
public enum DBTypeEnum {
    MASTER, SLAVE;
}
 
测试的Mapper接口
package com.ywcai.demo.doubleDs.aop;
@Service
@Mapper
public interface AopMapper {

    @Select("SELECT * FROM user ")
    List<TestUser> getAllUserInfo();

    @Delete("delete from roles where roles.user_id!=#{userId}")
    int deleteRole(@Param(value = "userId") long userId);

}
 
对应的映射结果集的实体类
TestRole.class
package com.ywcai.demo.doubleDs.aop;
import lombok.Data;

@Data
public class TestRole {
    long id;
    String roleName;
    long userId;
}
 
TestUser.class
package com.ywcai.demo.doubleDs.aop;
import lombok.Data;

@Data
public class TestUser {
    long id;
    String username;
    String password;
}

 

AOP切面处理DBAspect.class
package com.ywcai.demo.doubleDs.aop;
@Aspect
@Component
@Slf4j
public class DBAspect {

    @Autowired
    DBContextHolder dbContextHolder;
    //将aop包里面的mapper作为切入点
    //入参规则 ,第一个*号代表匹配所有返回结果类型
    //第二个*号代表匹配所有改包的所有类
    //第三个*号代表匹配所有方法,()号里面的两个点代表所有参数

    @Pointcut(value = 
"(execution(public * com.ywcai.demo.doubleDs.aop.AopMapper.get*(..)))||
(execution(public * com.ywcai.demo.doubleDs.aop.AopMapper.select*(..)))")
    public void setSlave() {
    }

    //JoinPoint表示切入的具体方法信息,包括了方法名和形参。在切入点之前需要做的事
    //这里为了测试效果,可以将主数据源设置为错误的库路径,从库数据源路径设置为正确。
最终get相关的方法正常运行,其他方法报错。
    @Before("setSlave()")
    public void beforeSlave(JoinPoint joinPoint) {
        //如果方法名字包含了select或者get的,默认是读数据,走SLAVE,其他的方法则走主数据源
        //走SLAVE的方法,这里做的DEMO 也会进行选择,根据调用方法的次数,单数走SLAVE,双数走MASTER,次数从0开始。
        dbContextHolder.setSlave();
    }

}
 
 
调用测试的测试类,里面有几个不同的测试方法
package com.ywcai.demo.doubleDs.aop;

@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
class AopDsTest {

    @Autowired
    AopMapper aopMapper;

    @Test
    void testAopGet() {
        // CountDownLatch,可以用于等待所有线程结束,进行结果的同步.
        ExecutorService exec = Executors.newFixedThreadPool(5);
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            AopThread aopThread = new AopThread(countDownLatch);
            exec.submit(aopThread);
        }

        //这里注意,多线程,一定要在方法结束的时候阻塞线程,否则jvm退出会导致线程无法运行完。
        //因此用了countDownlatch来检测线程执行结束的情况
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("exe complete !");

    }

    //返回多个线程执行后返回的数据并对list长度进行加法处理
    @Test
    void testAopGet3() {
        LinkedBlockingDeque linkedBlockingDeque = new LinkedBlockingDeque(100);
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            Callable callable = new CallableTask();
            //可以直接执行后返回future的接口
//            Future future = executorService.submit(callable);
            //也可以将callable包装为FutureTask,然后提交执行,没有本质区别
            FutureTask futureTask = new FutureTask(callable);
            executorService.submit(futureTask);
//            futureTask.run();
            try {
                linkedBlockingDeque.putLast(futureTask);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //需要有个集合来把数据存一下,然后再主线程去遍历。不然这里使用 get会被阻塞
        }

        //这里注意,多线程,一定要在方法结束的时候阻塞线程,否则jvm退出会导致线程无法运行完。
        //这里遍历出结果,遍历结果的get方法时,会阻塞子线程,知道子线程执行完成返回结果
        //这里自定义了一个双端队列来获取当前处理完成的结果,避免被某一个耗时线程所阻塞
        while (linkedBlockingDeque.size() > 0) {
            try {
                log.info("this result {}", ((FutureTask) linkedBlockingDeque.pollFirst()).get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }





    //通过ExecutorCompletionService直接将完成的现成结果包装到了双端队列。然后可以take().get()已完成的结果
    @Test
    void testAopGet4() {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        CompletionService completionService = new ExecutorCompletionService(executorService);
        for (int i = 0; i < 10; i++) {
            Callable callable = new CallableTask();
            completionService.submit(callable);
            //需要有个集合来把数据存一下,然后再主线程去遍历。不然这里使用 get会被阻塞

        }
        //这里可以直接通过completionService打印出callable的结果。
        for (int i = 0; i < 10; i++) {
            try {
                log.info("the list size is {}", completionService.take().get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }

//测试一个delete方法,只会用master数据源操作。
    @Test
    void testAopDelete() {
        log.info("aopMapper delete role info {}", aopMapper.deleteRole(11l));
    }
}
 
另外测试方法中还需辅助的执行任务,对应不同的测试方法
 
 
Callable返回值的辅助线程
package com.ywcai.demo.doubleDs.aop;

@Slf4j
public class CallableTask implements Callable {


    @Override
    public Integer call() {
        AopMapper aopMapper = GetBeanUtil.getBean(AopMapper.class);
        int i = aopMapper.getAllUserInfo().size();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("Thread ID is {}", Thread.currentThread().getId());
        return i;
    }
}
 
通过countdowncatch阻塞主线程的辅助线程
AopThread.class
package com.ywcai.demo.doubleDs.aop;

@Slf4j
public class AopThread extends Thread {
    AopMapper aopMapper;
    CountDownLatch countDownLatch;
    public AopThread(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
        aopMapper = GetBeanUtil.getBean(AopMapper.class);
    }

    @Override
    public void run() {
        super.run();
        try {
            log.info("userinfo : {}", aopMapper.getAllUserInfo());
        } catch (Exception e) {
            log.error("AopThread run err : {}", e);
        } finally {
            countDownLatch.countDown();
        }
    }
}
最后附一个测试结果截图。
 
 
 
 
 
 
 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值