SpringBoot教程(二十四) | SpringBoot实现分布式定时任务之Quartz(多数据源配置)

SpringBoot教程(二十四) | SpringBoot实现分布式定时任务之Quartz(多数据源配置)

  • 前言
  • 多数据源配置
    • 引入aop依赖
    • [1. properties配置多数据源](#1. properties配置多数据源)
    • [2. 创建数据源枚举类](#2. 创建数据源枚举类)
    • [3. 线程参数配置类](#3. 线程参数配置类)
    • [4. 数据源动态切换类](#4. 数据源动态切换类)
    • [5. 多数据源配置类](#5. 多数据源配置类)
      • [HikariCP 版本](#HikariCP 版本)
      • [Druid 版本](#Druid 版本)
    • [6. 自定义多数据源切换注解](#6. 自定义多数据源切换注解)
    • [7. 数据源注解截面AOP](#7. 数据源注解截面AOP)
    • [8. 测试 多数据源 是否起效](#8. 测试 多数据源 是否起效)

前言

我这边的SpringBoot的版本为2.6.13,其中Quartz是使用的以下方式引入

xml 复制代码
<!--quartz定时任务-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

由于我目前需要使用@QuartzDataSource去指定数据源,所以就不通过@DS注解去实现多数据源

而是通过自定义DynamicDataSource 去实现了多数据源

多数据源配置

引入aop依赖

xml 复制代码
<!--spring切面aop依赖-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

1. properties配置多数据源

xml 复制代码
# 应用服务 WEB 访问端口
server.port=9081

# 主数据源
spring.datasource.db1.url=jdbc:mysql://localhost:3306/sharding?useSSL=false&serverTimezone=UTC
spring.datasource.db1.jdbc-url=jdbc:mysql://localhost:3306/sharding?useSSL=false&serverTimezone=UTC
spring.datasource.db1.username=root
spring.datasource.db1.password=root
spring.datasource.db1.driver-class-name=com.mysql.cj.jdbc.Driver

# 次数据源
spring.datasource.db2.url=jdbc:mysql://localhost:3306/sharding?useSSL=false&serverTimezone=UTC
spring.datasource.db2.jdbc-url=jdbc:mysql://localhost:3306/sharding?useSSL=false&serverTimezone=UTC
spring.datasource.db2.username=root
spring.datasource.db2.password=root
spring.datasource.db2.driver-class-name=com.mysql.cj.jdbc.Driver

# quartz 数据源
#spring.datasource.task.url=jdbc:mysql://localhost:3306/quartz
spring.datasource.task.jdbc-url=jdbc:mysql://localhost:3306/quartz
spring.datasource.task.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.task.username=root
spring.datasource.task.password=root

# Quartz Scheduler 配置
# 指定作业存储类型为JDBC,使用数据库来存储作业和调度信息
spring.quartz.job-store-type=jdbc
# 在关闭时等待作业完成
spring.quartz.wait-for-jobs-to-complete-on-shutdown=true
# 不初始化数据库架构,假设数据库架构已经存在
spring.quartz.jdbc.initialize-schema=never
# Quartz Scheduler 属性配置
spring.quartz.properties.org.quartz.jobStore.dataSource=quartz_jobs
spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
spring.quartz.properties.org.quartz.jobStore.tablePrefix=QRTZ_
# 启用集群模式
spring.quartz.properties.org.quartz.jobStore.isClustered=true
# 集群检查间隔时间(毫秒)
spring.quartz.properties.org.quartz.jobStore.clusterCheckinInterval=1000
# 不使用属性来存储作业数据,改为使用BLOB字段
spring.quartz.properties.org.quartz.jobStore.useProperties=false
# 指定作业存储的类,这里使用Spring提供的LocalDataSourceJobStore,但通常这个配置是隐式的,
# 除非你有特殊的实现需求,否则通常不需要显式设置这个属性。
# spring.quartz.properties.org.quartz.jobStore.class=org.springframework.scheduling.quartz.LocalDataSourceJobStore
# 注意:上面的行已被注释掉,因为通常不需要显式设置
# 调度器实例名称和ID(org.quartz.scheduler.instanceName 这个是保证属于同一个集群)
spring.quartz.properties.org.quartz.scheduler.instanceName=SC_Scheduler
spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO
# 线程池配置
# 线程池中线程的数量
spring.quartz.properties.org.quartz.threadPool.threadCount=25
# 线程优先级
spring.quartz.properties.org.quartz.threadPool.threadPriority=5
# 线程池实现类
spring.quartz.properties.org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool

2. 创建数据源枚举类

java 复制代码
package com.example.springbootfull.quartztest.enums;

/**
 * 数据源
 */
public enum DataSourceType {
    /**
     * 数据源1
     * */
    DB1,

    /**
     * 数据源2
     * */
    DB2
}

3. 线程参数配置类

定义一个工具类来设置当前线程的数据源枚举值

java 复制代码
package com.example.springbootfull.quartztest.datasource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 数据源切换处理
 */
public class DynamicDataSourceContextHolder {
    public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);

    /**
     * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
     * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
     */
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    /**
     * 设置数据源的变量
     */
    public static void setDataSourceType(String dsType) {
        log.info("切换到{}数据源", dsType);
        CONTEXT_HOLDER.set(dsType);
    }

    /**
     * 获得数据源的变量
     */
    public static String getDataSourceType() {
        return CONTEXT_HOLDER.get();
    }

    /**
     * 清空数据源变量
     */
    public static void clearDataSourceType() {
        CONTEXT_HOLDER.remove();
    }
}

4. 数据源动态切换类

java 复制代码
package com.example.springbootfull.quartztest.datasource;

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



/**
 * Spring的AbstractRoutingDataSource抽象类,实现动态数据源(他的作用就是动态切换数据源)
 * AbstractRoutingDataSource中的抽象方法determineCurrentLookupKey是实现数据源的route的核心,
 * 这里对该方法进行Override。【上下文DynamicDataSourceContextHolder为一线程安全的ThreadLocal】
 */
public class DynamicDataSource extends AbstractRoutingDataSource {


    /**
     * 取得当前使用哪个数据源
     * @return dbTypeEnum
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}

5. 多数据源配置类

HikariCP 版本

java 复制代码
package com.example.springbootfull.quartztest.config;

import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.example.springbootfull.quartztest.datasource.DynamicDataSource;
import com.example.springbootfull.quartztest.enums.DataSourceType;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.quartz.QuartzDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

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

/**
 * 多数据源配置
 */
@Configuration
public class DataSourceConfig {

    /**
     * 创建第一个数据源
     *
     * @return dataSource
     */
    @Bean(name = "dataSource1")
    @ConfigurationProperties(prefix = "spring.datasource.db1")
    public DataSource dataSource1() {
        return DataSourceBuilder.create().build();
    }

    /**
     * 创建第二个数据源
     *
     * @return dataSource
     */
    @Bean(name = "dataSource2")
    @ConfigurationProperties(prefix = "spring.datasource.db2")
    public DataSource dataSource2() {
        return DataSourceBuilder.create().build();
    }

    //quartz数据库 dataSourceTask数据源
    //使用@QuartzDataSource后,不需要动态配置
    @Bean(name = "dataSourceTask")
    @ConfigurationProperties(prefix = "spring.datasource.task")
    @QuartzDataSource
    public DataSource dataSourceTask() {
        return DataSourceBuilder.create().build();
    }

    /**
     * 动态数据源配置
     *
     * @return dataSource
     */
    @Primary
    @Bean("multipleDataSource")
    public DataSource multipleDataSource(@Qualifier("dataSource1") DataSource db1,
                                         @Qualifier("dataSource2") DataSource db2) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> dataSources = new HashMap<>();
        dataSources.put(DataSourceType.DB1, db1);
        dataSources.put(DataSourceType.DB2, db2);
        dynamicDataSource.setTargetDataSources(dataSources);
        //默认数据源
        dynamicDataSource.setDefaultTargetDataSource(db1);
        return dynamicDataSource;
    }

    @Bean("sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("multipleDataSource") DataSource multipleDataSource) throws Exception {
        // 导入mybatissqlsession配置
        MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
        // 指明数据源
        sessionFactory.setDataSource(multipleDataSource);
        // 设置mapper.xml的位置路径
        Resource[] resources = new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/**/*.xml");
        sessionFactory.setMapperLocations(resources);
        //指明实体扫描(多个package用逗号或者分号分隔)
        //sessionFactory.setTypeAliasesPackage("com.szylt.projects.project.entity");
        // 导入mybatis配置
        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setJdbcTypeForNull(JdbcType.NULL);
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setCacheEnabled(false);
        sessionFactory.setConfiguration(configuration);
        return sessionFactory.getObject();
    }


    //数据源事务配置
    @Bean
    public PlatformTransactionManager transactionManager(DataSource multipleDataSource) {
        return new DataSourceTransactionManager(multipleDataSource);
    }

}

Druid 版本

需要先引入 Druid 依赖,这里使用的是 Druid 官方的 Starter

bash 复制代码
<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>druid-spring-boot-starter</artifactId>
   <version>1.2.8</version>
</dependency>

然后,在properties配置文件中 为每个数据源配置DruidDataSour数据库连接池

bash 复制代码
spring.datasource.db1.type=com.alibaba.druid.pool.DruidDataSour
spring.datasource.db2.type=com.alibaba.druid.pool.DruidDataSour
spring.datasource.task.type=com.alibaba.druid.pool.DruidDataSour

接着把一下DataSourceConfig 类里面的

DataSource 对象 换成 DruidDataSource 对象

DataSourceBuilder 对象 换成 DruidDataSourceBuilder 对象

就好了

6. 自定义多数据源切换注解

java 复制代码
package com.example.springbootfull.quartztest.annotation;

import com.example.springbootfull.quartztest.enums.DataSourceType;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义多数据源切换注解
 * 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准
 */
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource
{
    /**
     * 切换数据源名称
     */
    public DataSourceType value() default DataSourceType.DB1;
}

7. 数据源注解截面AOP

java 复制代码
package com.example.springbootfull.quartztest.aspectj;

import java.util.Objects;

import com.example.springbootfull.quartztest.annotation.DataSource;
import com.example.springbootfull.quartztest.datasource.DynamicDataSourceContextHolder;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;


/**
 * 多数据源处理
 *
 */
@Aspect
@Order(1)
@Component
public class DataSourceAspect {
    protected Logger logger = LoggerFactory.getLogger(getClass());

    @Pointcut("@annotation(com.example.springbootfull.quartztest.annotation.DataSource)"
            + "|| @within(com.example.springbootfull.quartztest.annotation.DataSource)")
    public void dsPointCut() {

    }

    @Around("dsPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        DataSource dataSource = getDataSource(point);
        if (dataSource != null) {
            DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
        }
        try {
            return point.proceed();
        } finally {
            // 销毁数据源 在执行方法之后
            DynamicDataSourceContextHolder.clearDataSourceType();
        }
    }

    /**
     * 获取需要切换的数据源
     */
    public DataSource getDataSource(ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
        if (Objects.nonNull(dataSource)) {
            return dataSource;
        }

        return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
    }
}

8. 测试 多数据源 是否起效

创建了一个控制层测试类

bash 复制代码
package com.example.springbootfull.mybatisplustest.controller;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.springbootfull.mybatisplustest.entity.SysUser;
import com.example.springbootfull.mybatisplustest.mapper.SysUserMapper;
import com.example.springbootfull.quartztest.annotation.DataSource;
import com.example.springbootfull.quartztest.enums.DataSourceType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.stereotype.Controller;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author jiang
 * @since 2024-09-13
 */
@Controller
@RequestMapping("/mybatisplustest/sysUser")
public class SysUserController {

    @Autowired
    private SysUserMapper sysUserMapper;

    @RequestMapping("/selectAll")
    @DataSource(value = DataSourceType.DB1)
    public void contextLoads() {
        //第一页,10个
        Page<SysUser> page = new Page<>(1, 10);
        QueryWrapper<SysUser> wrapper = new QueryWrapper<>();
        //查询liek人1的
        //wrapper.eq("enabled", true).like("account", "小明机器人10号");
        //查询授权=true的;
        wrapper.like("account", "人1");
        IPage<SysUser> iPage = sysUserMapper.selectPage(page, wrapper);

        //总页数
        System.out.println("总页数:"+iPage.getPages());
        //总条数
        System.out.println("总条数:"+iPage.getTotal());
        //每页条数
        System.out.println("每页条数:"+iPage.getSize());
        //当前页的结果集
        System.out.println("当前页的结果集:"+iPage.getRecords());
        //当前页号
        System.out.println("当前页号:"+iPage.getCurrent());

//        // 创建实体类
//        SysUser sysUser = new SysUser();
//        sysUser.setAccount("mybtis呀");
//        sysUser.setEnabled(Boolean.TRUE);
//        sysUser.setCreateAt(new Date());
//        sysUserDao.insertSelective(sysUser);
//
//        // 根据自增ID检索实体
//        SysUser sysUser1 = sysUserDao.selectByPrimaryKey(sysUser.getId());

//        logger.info("user={}", sysUser1);
    }

}

运行如下截图:

两者都有触发

参考文章如下:

【1】Springboot+Mybatis+MySql整合多数据源及其使用

【2】Springboot定时任务quartz整合(多数据源+quartz持久化到数据库)

【3】springboot+MybatisPlus+HikariCP多数据源动态配置(实战篇)

【4】springboot+mybatis-plus+quartz多数据源操作,亲测可用

相关推荐
追逐时光者4 小时前
推荐 12 款开源美观、简单易用的 WPF UI 控件库,让 WPF 应用界面焕然一新!
后端·.net
Jagger_4 小时前
敏捷开发流程-精简版
前端·后端
苏打水com5 小时前
数据库进阶实战:从性能优化到分布式架构的核心突破
数据库·后端
西瓜er5 小时前
JAVA:Spring Boot 集成 FFmpeg 实现多媒体处理
java·spring boot·ffmpeg
间彧6 小时前
Spring Cloud Gateway与Kong或Nginx等API网关相比有哪些优劣势?
后端
间彧6 小时前
如何基于Spring Cloud Gateway实现灰度发布的具体配置示例?
后端
间彧6 小时前
在实际项目中如何设计一个高可用的Spring Cloud Gateway集群?
后端
间彧6 小时前
如何为Spring Cloud Gateway配置具体的负载均衡策略?
后端
间彧6 小时前
Spring Cloud Gateway详解与应用实战
后端
EnCi Zheng7 小时前
SpringBoot 配置文件完全指南-从入门到精通
java·spring boot·后端