1、快速理解 Spring 多数据源操作

最近在调研 Spring 如何配置多数据源的操作,结果被媳妇吐槽,整天就坐在那打电脑,啥都不干。于是我灵光一现,跟我媳妇说了一下调研结果,第一版本原话如下:

Spring 提供了一套多数据源的解决方案,通过继承抽象 AbstractRoutingDataSource 定义动态路由数据源,然后可以通过AOP, 动态切换配置好的路由Key,来跳转不同的数据源。

Spring ?春天 ?我在这干活你还想有春天,还有那个什么什么抽象,我现在有点想抽你。好好说话 !

媳妇,莫急莫急,嗯… 等我重新组织一下语言先。我想了一会缓缓的对媳妇说:

生活中我们去景点买票或者购买火车票,一般都有军人和残疾人专门的售票窗口,普通人通过正常的售票窗口进行购票,军人和残疾人通过专门的优惠售票窗口进行购票。

为了防止有人冒充军人或残疾人去优惠售票窗口进行购票,这就需要你提供相关的证件来证明。没有证件的走正常售票窗口,有证件的走优惠售票窗口。

那如何判断谁有证件,谁没有证件呢? 这就需要辛苦检查证件的工作人员。默认情况下,正常售票窗口通道是打开的,大家可以直接去正常的售票窗口进行购票。

如果有军人或残疾人来购票,工作人员检查相关证件后,则关闭正常售票窗口通道,然后打开优惠窗口通道。

在理解了购票的流程后,我们在来看 Spring 动态数据源切换的解决方案就会容易很多。Spring 动态数据源解决方案与购票流程中的节点的对应如下:

  • 具体 Dao 访问数据库获取的数据 = 景点票或火车票
  • 具体 Dao = 购票人员。
  • 具体 Dao目标数据源注解类的value值 = 证明是否是军人或残疾人的证件。
  • Spring 动态数据源 = 售票点。
  • 不同的数据源 = 正常售票窗口和优惠售票窗口。
  • AOP 动态修改动态数据源状态类Key值 = 检查证件工作人员去关闭和打开正常售票窗口和优惠售票窗口通道。
  • 动态数据源状态类 = 不同购票窗口通道门打开或关闭状态。
  • 动态路由状态Key值的枚举类 = 不同购票窗口通道的门。

具体执行流程图如下:

java Spring Boot mybits 输出sql_数据源


你要这么说:我就明白了,媳妇这会的语气缓和了很多。但是你讲这么多有个毛线用 ! 拖地去 !

好嘞 ! 我拿起拖把疯狂的拖了起来。

到这里Spring 多数据源操作流程介绍完毕! 如果你想了解代码的实现请接着往下看,如果您就想看看操作流程那么感谢您的阅读。记得关注加点赞哈 😁

正所谓光说不练假把式,说了这么多操作流程的介绍,接下来开始正式的实战操作。在实战操作前我先说一下实战操作内容以及注意事项:

实战操作的主要内容介绍了如何在 SpringBoot 项目下,通过 MyBatis + Durid 数据库连接池来配置不同数据源的操作。

阅读本文需要你熟悉 SpringBoot 和 MyBatis 的基本操作即可,另外需要注意的是实战操作的代码环境如下:

  • SpringBoot:2.1.0.RELEASE
  • MyBatis:3.4.0 (mybatis-spring-boot-starter:1.1.1)
  • JDK:1.8.0_251
  • Durid:1.1.10 (druid-spring-boot-starter:1.1.10 )
  • Maven:3.6.2

按照本文进行操作的过程中,请尽量保持你的环境版本和上述一致,如出现问题可以查看本文末尾处的 GitHub 项目仓库的代码进行对比。

2、整合多数源实战操作

2.1、数据库准备

这里通过商品库的商品表和旅馆库的旅馆表来模拟多数据源的场景,具体建库以及建表 SQL 如下:

需要注意的是,我本地环境使用的是 MySql 5.6 。

创建商品库以及商品表的 Sql。

SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
--  Table structure for `product`
-- ----------------------------
DROP TABLE IF EXISTS `product`;
CREATE TABLE `product` (
  `id` bigint(10) NOT NULL AUTO_INCREMENT COMMENT '商品id',
  `product_name` varchar(25) DEFAULT NULL COMMENT '商品名称',
  `price` decimal(8,3) DEFAULT NULL COMMENT '价格',
  `product_brief` varchar(125) DEFAULT NULL COMMENT '商品简介',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

-- ----------------------------
--  Records of `product`
-- ----------------------------
BEGIN;
INSERT INTO `product` VALUES ('2', '苹果', '20.000', '好吃的苹果,红富士大苹果');
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;

创建旅馆库和旅馆表 Sql

SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
--  Table structure for `hotel`
-- ----------------------------
DROP TABLE IF EXISTS `hotel`;
CREATE TABLE `hotel` (
  `id` bigint(10) NOT NULL AUTO_INCREMENT COMMENT '旅馆id',
  `city` varchar(125) DEFAULT NULL COMMENT '城市',
  `name` varchar(125) DEFAULT NULL COMMENT '旅馆名称',
  `address` varchar(256) DEFAULT NULL COMMENT '旅馆地址',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

-- ----------------------------
--  Records of `hotel`
-- ----------------------------
BEGIN;
INSERT INTO `hotel` VALUES ('1', '北京', '汉庭', '朝阳区富明路112号');
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;

2.2、SpringBoot 项目多数据源yml 配置

搭建 SpringBoot 项目这里不在进行介绍,搭建好SpringBoot 项目的第一步就是进行项目的 yml 配置。

application.yml 的配置代码如下:

server:
  port: 8080

#mybatis:
  #config-location: classpath:mybatis-config.xml
  #mapper-locations: classpath*:mapper/**/*Mapper.xml


spring:
  datasource:
    #初始化时建立物理连接的个数
    #type: com.alibaba.druid.pool.DruidDataSource
    druid:
      product:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/product?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: 123456
        initial-size: 15
        #最小连接池数量
        min-idle: 10
        #最大连接池数量
        max-active: 50
        #获取连接时最大等待时间
        max-wait: 60000
        # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
        timeBetweenEvictionRunsMillis: 60000
        # 配置一个连接在池中最小生存的时间,单位是毫秒
        minEvictableIdleTimeMillis: 300000
        # 配置一个连接在池中最大生存的时间,单位是毫秒
        maxEvictableIdleTimeMillis: 9000000
        # 配置检测连接是否有效
        validationQuery: SELECT 1 FROM DUAL
        #配置监控页面访问登录名称
        stat-view-servlet.login-username: admin
        #配置监控页面访问密码
        stat-view-servlet.login-password: admin
        #是否开启慢sql查询监控
        filter.stat.log-slow-sql: true
        #慢SQL执行时间
        filter.stat.slow-sql-millis: 1000
      hotel:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/hotel?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: 123456
        initial-size: 15
        #最小连接池数量
        min-idle: 10
        #最大连接池数量
        max-active: 50
        #获取连接时最大等待时间
        max-wait: 60000
        # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
        timeBetweenEvictionRunsMillis: 60000
        # 配置一个连接在池中最小生存的时间,单位是毫秒
        minEvictableIdleTimeMillis: 300000
        # 配置一个连接在池中最大生存的时间,单位是毫秒
        maxEvictableIdleTimeMillis: 9000000
        # 配置检测连接是否有效
        validationQuery: SELECT 1 FROM DUAL
        #配置监控页面访问登录名称
        stat-view-servlet.login-username: admin
        #配置监控页面访问密码
        stat-view-servlet.login-password: admin
        #是否开启慢sql查询监控
        filter.stat.log-slow-sql: true
        #慢SQL执行时间
        filter.stat.slow-sql-millis: 1000

多数据源情况下,原先的 Mybatis 相关配置不会起作用。Mybaies 配置均在定义多个数据源的配置类进行。

2.3、自定义动态路由数据源

自定义动态路由数据源是整个操作中最为重要的环节,因为整个切换数据源过程都是通过操作它来完成的。

第一步,创建动态数据源状态类以及动态路由状态Key值的枚举类,具体代码如下:

动态路由状态Key值的枚举类

public enum DataSourceKeyEnum {
    HOTEL, PRODUCT
}

动态数据源状态类

public class DynamicDataSourceRoutingKeyState {

    private static Logger log = LoggerFactory.getLogger(DynamicDataSourceRoutingKeyState.class);
    // 使用ThreadLocal保证线程安全
    private static final ThreadLocal<DataSourceKeyEnum> TYPE = new ThreadLocal<DataSourceKeyEnum>();

    // 往当前线程里设置数据源类型
    public static void setDataSourceKey(DataSourceKeyEnum dataSourceKey) {
        if (dataSourceKey == null) {
            throw new NullPointerException();
        }
        log.info("[将当前数据源改为]:{}",dataSourceKey);
        TYPE.set(dataSourceKey);
    }

    // 获取数据源类型
    public static DataSourceKeyEnum getDataSourceKey() {
        DataSourceKeyEnum dataSourceKey = TYPE.get();
        log.info("[获取当前数据源的类型为]:{}",dataSourceKey);
        System.err.println("[获取当前数据源的类型为]:" + dataSourceKey);
        return dataSourceKey;
    }

    // 清空数据类型
    public static void clearDataSourceKey() {
        TYPE.remove();
    }
}

第二步,通过继承 Spring 提供的抽象类 AbstractRoutingDataSource 来创建动态数据源,具体代码如下:

public class DynamicDataSourceRouting extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        DataSourceKeyEnum dataSourceKey = DynamicDataSourceRoutingKeyState.getDataSourceKey();
        return dataSourceKey;
    }
}

2.4、定义多个数据源的配置类

第一步,配置商品库数据源、旅馆库数据源、动态数据源、MyBatis SqlSessionFactory 。

配置商品库数据源代码。

@Configuration
public class DataSourceConfig {

    /**
     * 商品库的数据源
     * @return
     */
    @Bean(name = "dataSourceForProduct")
    @ConfigurationProperties(prefix="spring.datasource.druid.product")
    public DruidDataSource dataSourceForProduct() {
        return DruidDataSourceBuilder.create().build();
    }
}

配置旅馆库的数据源代码。

/**
     * 旅馆库的数据源
     * @return
     */
    @Bean(name = "dataSourceForHotel")
    @ConfigurationProperties(prefix="spring.datasource.druid.hotel")
    public DruidDataSource dataSourceForHotel() {
        return DruidDataSourceBuilder.create().build();
    }

配置动态路由的数据源代码。

/**
     * 动态切换的数据源
     * @return
     */
    @Bean(name = "dynamicDataSource")
    public DataSource dynamicDataSource() {

        Map<Object, Object> targetDataSource = new HashMap<>();
        targetDataSource.put(DataSourceKeyEnum.PRODUCT, dataSourceForProduct());
        targetDataSource.put(DataSourceKeyEnum.HOTEL, dataSourceForHotel());
        //设置默认的数据源和以及多数据源的Map信息
        DynamicDataSourceRouting dataSource = new DynamicDataSourceRouting();
        dataSource.setTargetDataSources(targetDataSource);
        dataSource.setDefaultTargetDataSource(dataSourceForProduct());
        return dataSource;
    }

配置 MyBatis SqlSessionFactory 并指定动态数据源代码。

@Bean(name = "sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
            throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dynamicDataSource);
        //设置数据数据源的Mapper.xml路径
        bean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml"));
        //设置Mybaties查询数据自动以驼峰式命名进行设值
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session
                .Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        bean.setConfiguration(configuration);

        return bean.getObject();
    }

配置数据源注入事务 DataSourceTransactionManager 代码。

/**
     * 注入 DataSourceTransactionManager 用于事务管理
     */
    @Bean
    public PlatformTransactionManager transactionManager(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) {
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(dynamicDataSource);
        return new DataSourceTransactionManager(dynamicDataSource);
    }

配置商品库数据源、配置旅馆库的数据源、配置动态路由的数据源、配置MyBatis SqlSessionFactory 、配置数据源注入事务 代码均在 DataSourceConfig 配置类中。

第二步,配置数据源的事务 AOP 切面类。

添加事务AOP切面类,通过方法名前缀来配置其事务。

  • add、save、insert、update、delete、其他前缀的方法:读写事务。
  • get、find、query前缀的方法:只读事务。
@Aspect
@Configuration
public class TransactionConfiguration {
    private static final int TX_METHOD_TIMEOUT = 5;
    private static final String AOP_POINTCUT_EXPRESSION = "execution( * cn.lijunkui.service.*.*(..))";

    @Autowired
    private PlatformTransactionManager transactionManager;

    @Bean
    public TransactionInterceptor txAdvice() {
        NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
        /* 只读事务,不做更新操作 */
        RuleBasedTransactionAttribute readOnlyTx = new RuleBasedTransactionAttribute();
        readOnlyTx.setReadOnly(true);
        readOnlyTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED);
        /* 当前存在事务就使用当前事务,当前不存在事务就创建一个新的事务 */
        RuleBasedTransactionAttribute requiredTx = new RuleBasedTransactionAttribute();
        requiredTx.setRollbackRules(Collections.singletonList(new RollbackRuleAttribute(Exception.class)));
        requiredTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        //requiredTx.setTimeout(TX_METHOD_TIMEOUT);
        Map<String, TransactionAttribute> txMap = new HashMap<String, TransactionAttribute>();
        txMap.put("add*", requiredTx);
        txMap.put("save*", requiredTx);
        txMap.put("insert*", requiredTx);
        txMap.put("update*", requiredTx);
        txMap.put("delete*", requiredTx);
        txMap.put("get*", readOnlyTx);
        txMap.put("find*", readOnlyTx);
        txMap.put("query*", readOnlyTx);
        txMap.put("*", requiredTx);
        source.setNameMap(txMap);
        TransactionInterceptor txAdvice = new TransactionInterceptor(transactionManager, source);
        return txAdvice;
    }

    @Bean
    public Advisor txAdviceAdvisor() {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression(AOP_POINTCUT_EXPRESSION);
        return new DefaultPointcutAdvisor(pointcut, txAdvice());
    }
}

2.5、定义AOP动态切换配置数据源的操作

指定Dao目标数据源注解类。

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
    DataSourceKeyEnum value() default DataSourceKeyEnum.PRODUCT;
}

Dao 访问数据库进行拦截的 Aop 切面类。

@Aspect
@Component
public class DataSourceAop {

    Logger log = LoggerFactory.getLogger(DataSourceAop.class);

    @Pointcut("execution( * cn.lijunkui.dao.*.*(..))")
    public void daoAspect() {
    }
    @Before(value="daoAspect()")
    public void switchDataSource(JoinPoint joinPoint) throws NoSuchMethodException {
        log.info("开始切换数据源");

        //获取HotelMapper or ProductMapper 类上声明的TargetDataSource的数据源注解的值
        MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
        Class<?> declaringClass  = methodSignature.getMethod().getDeclaringClass();
        TargetDataSource annotation = declaringClass.getAnnotation(TargetDataSource.class);
        DataSourceKeyEnum value = annotation.value();
        log.info("数据源为:{}",value);

        //根据TargetDataSource的value设置要切换的数据源
        DynamicDataSourceRoutingKeyState.setDataSourceKey(value);
    }
}

到这里多数据源配置操作介绍完毕!

3、测试

正所谓没有测试的代码都是耍流氓,接下来通过分别定义访问商品库和旅馆的 Controller、Service、Dao。并进行验证上述配置是否有效。

旅馆 Controller

@RestController
public class HotelController {

    @Autowired
    private HotelService hotelService;

    /**
     * 查询所有的旅馆信息
     * @return
     */
    @GetMapping("/hotel")
    public List<Hotel> findAll(){
        List<Hotel> hotelList = hotelService.findAll();
        return hotelList;
    }
}

旅馆 Service

@Service
public class HotelService {

    @Autowired
    private HotelMapper hotelMapper;

    public List<Hotel> findAll(){
        List<Hotel> hotels = hotelMapper.selectList();
        return hotels;
    }
}

旅馆 Dao

@Mapper
@TargetDataSource(value = DataSourceKeyEnum.HOTEL )
public interface HotelMapper {


	/**
	 * 查询所有
	 * @return List<Hotel>
	 */
	List<Hotel> selectList();
}

旅馆 Mapper.xml 文件配置。

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.lijunkui.dao.HotelMapper">
 

	 
	<sql id="baseSelect">
        select id, city, name, address from hotel
    </sql>
    
    <select id="selectList"  resultType="cn.lijunkui.domain.Hotel">
        <include refid="baseSelect"/>
    </select>
</mapper>

商品 Controller

@RestController
public class ProductController {

	@Autowired
	private ProductService productService;

	/**
	 * 查询所有的商品信息
	 * @return
	 */
	@GetMapping("/product")
	public List<Product> findAll() {
        List<Product> productList = productService.findAll();
        return productList;
	}
}

商品Service

@Service
public class ProductService {

    @Autowired
    private ProductMapper productMapper;

    public List<Product> findAll(){
        return productMapper.selectList();
    }
}

商品Dao

@Mapper
@TargetDataSource(value = DataSourceKeyEnum.PRODUCT )
public interface ProductMapper {

    /**
     * 查询所有
     * @param
     * @return List<Hotel>
     */
    List<Product> selectList();
}

商品的Mapper.xml 配置

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.lijunkui.dao.ProductMapper">
    
    <select id="selectList"  resultType="cn.lijunkui.domain.Product">
        select * from product
    </select>
</mapper>

通过 http://localhost:8080/product 访问获取所有的商品信息,具体效果如下图

java Spring Boot mybits 输出sql_动态数据源_02


后台日志信息如下:

2020-06-20 11:44:07.927  INFO 1234 --- [nio-8080-exec-1] cn.lijunkui.config.DataSourceAop         : 开始切换数据源
2020-06-20 11:44:07.928  INFO 1234 --- [nio-8080-exec-1] cn.lijunkui.config.DataSourceAop         : 数据源为:PRODUCT
2020-06-20 11:44:07.930  INFO 1234 --- [nio-8080-exec-1] c.l.c.DynamicDataSourceRoutingKeyState   : [将当前数据源改为]:PRODUCT
2020-06-20 11:44:07.942  INFO 1234 --- [nio-8080-exec-1] c.l.c.DynamicDataSourceRoutingKeyState   : [获取当前数据源的类型为]:PRODUCT

通过 http://localhost:8080/hotel 访问获取所有的旅馆信息,具体效果如下图

java Spring Boot mybits 输出sql_动态数据源_03


后台日志信息如下:

[获取当前数据源的类型为]:PRODUCT
2020-06-20 11:44:08.252  INFO 1234 --- [nio-8080-exec-1] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
2020-06-20 11:45:15.256  INFO 1234 --- [nio-8080-exec-4] cn.lijunkui.config.DataSourceAop         : 开始切换数据源
2020-06-20 11:45:15.256  INFO 1234 --- [nio-8080-exec-4] cn.lijunkui.config.DataSourceAop         : 数据源为:HOTEL
2020-06-20 11:45:15.256  INFO 1234 --- [nio-8080-exec-4] c.l.c.DynamicDataSourceRoutingKeyState   : [将当前数据源改为]:HOTEL
2020-06-20 11:45:15.256  INFO 1234 --- [nio-8080-exec-4] [获取当前数据源的类型为]:HOTEL
c.l.c.DynamicDataSourceRoutingKeyState   : [获取当前数据源的类型为]:HOTEL

4、小结

本文介绍了 Spring 通过继承抽象 AbstractRoutingDataSource 定义动态路由来完成多数据源切换的实战以及代码执行流程。

Spring 提供的动态数据源的机制就是将多个数据源通过 Map 进行维护,具体使用哪个数据源通过 determineCurrentLookupKey 方法返回的 Key 来确定。通过 AOP 动态修改 determineCurrentLookupKey 方法返回的Key,来完成切换数据源的操作。

本文介绍了访问不同数据库业务的实现,通过这种方式也可以搭建相同业务多个一致的数据库的读写分离。你可以尝试使用这种方式来实现,欢迎大家在评论区说说你实现读写分离的方案。

5、代码示例

操作过程如出现问题可以在我的GitHub 仓库 springbootexamples 中模块名为 spring-boot-2.x-mybaties-multipleDataSource 项目中进行对比查看

GitHub:https://github.com/zhuoqianmingyue/springbootexamples

如果您对这些感兴趣,欢迎 star、或转发给予支持!转发请标明出处!