likes
comments
collection
share

dynamic多数据源分析实现

作者站长头像
站长
· 阅读数 2

文章开始前,推荐下博主自己的AI站:BoomAI

1 概述

在项目中,我们可能会碰到需要多数据源的场景。例如说:

  • 读写分离:数据库主节点压力比较大,需要增加从节点提供读操作,以减少压力。
  • 多数据源:一个复杂的单体项目,因为没有拆分成不同的服务,需要连接多个业务的数据源。
  • 多租户数据源切换:大型租户,每个租户有独立的数据源,需要根据当前租户切换对应的数据源

本质上,读写分离,仅仅是多数据源的一个场景,从节点是只提供读操作的数据源。所以只要实现了多数据源的功能,也就能够提供读写分离

2 具体实现

本文使用 dynamic-datasource 实现多数据源的切换,同时后续会写出dynamic-datasource的实现逻辑和源码分析

2.1引入依赖

在pom.xml文件中引入相关依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>lab-17-dynamic-datasource-baomidou-01</artifactId>

<dependencies>
<!-- 实现对数据库连接池的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency> <!-- 本示例,我们使用 MySQL -->
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.48</version>
</dependency>

<!-- 实现对 MyBatis 的自动化配置 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>

<!-- 实现对 dynamic-datasource 的自动化配置 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>2.5.7</version>
</dependency>
<!-- 不造为啥 dynamic-datasource-spring-boot-starter 会依赖这个 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator</artifactId>
</dependency>

<!-- 方便等会写单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

</dependencies>

</project>

2.2创建实体类和数据表

application.yml内容

spring:
  datasource:
    # dynamic-datasource-spring-boot-starter 动态数据源的配置内容
    dynamic:
      primary: users # 设置默认的数据源或者数据源组,默认值即为 master
      datasource:
        # 订单 orders 数据源配置
        orders:
          url: jdbc:mysql://127.0.0.1:3306/test_orders?useSSL=false&useUnicode=true&characterEncoding=UTF-8
          driver-class-name: com.mysql.jdbc.Driver
          username: root
          password:
        # 用户 users 数据源配置
        users:
          url: jdbc:mysql://127.0.0.1:3306/test_users?useSSL=false&useUnicode=true&characterEncoding=UTF-8
          driver-class-name: com.mysql.jdbc.Driver
          username: root
          password:
# mybatis的配置文件就不写了,配置一个xml的地址和别名扫描的实体类包

sql内容,创建两个数据库,一个放订单表,一个放用户表

-- 在 `test_orders` 库中。
CREATETABLE`orders` (
  `id`int(11) DEFAULTNULLCOMMENT'订单编号',
  `user_id`int(16) DEFAULTNULLCOMMENT'用户编号'
) ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='订单表';

-- 在 `test_users` 库中。
CREATETABLE`users` (
  `id`int(11) NOTNULL AUTO_INCREMENT COMMENT'用户编号',
  `username`varchar(64) COLLATE utf8mb4_bin DEFAULTNULLCOMMENT'账号',
  `password`varchar(32) COLLATE utf8mb4_bin DEFAULTNULLCOMMENT'密码',
  `create_time` datetime DEFAULTNULLCOMMENT'创建时间',
  PRIMARY KEY (`id`),
  UNIQUEKEY`idx_username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=4DEFAULTCHARSET=utf8mb4 COLLATE=utf8mb4_bin;
 

实体类内容

// OrderDO.java
/**
 * 订单 DO
 */
public class OrderDO {

    /**
     * 订单编号
     */
    private Integer id;
    /**
     * 用户编号
     */
    private Integer userId;

    // 省略 setting/getting 方法

}

// UserDO.java
/**
 * 用户 DO
 */
public class UserDO {

    /**
     * 用户编号
     */
    private Integer id;
    /**
     * 账号
     */
    private String username;

    // 省略 setting/getting 方法
}

3.3Mapper

在mapper包下创建订单和用户的mapper

// OrderMapper.java
@Repository
@DS(DBConstants.DATASOURCE_ORDERS)
public interface OrderMapper {

    OrderDO selectById(@Param("id") Integer id);

}

// UserMapper.java
@Repository
@DS(DBConstants.DATASOURCE_USERS)
public interface UserMapper {

    UserDO selectById(@Param("id") Integer id);

}

DBConstants 是自定义的常量类 对应的是配置文件中数据源的名字 @DS 注解是dynamic提供的注解,可写在service或mapper的类上或方法上,value值写对应的数据源名字,对于这个注解推荐写在serviceImpl的方法上或者mapper的类上

mapper对应的xml这里不写了,比较简单,就是对订单和用户表根据id的查询

3.4 简单测试

创建 UserMapperTest 和 OrderMapperTest 测试类,我们来测试一下

// OrderMapperTest.java
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class OrderMapperTest {

    @Autowired
    private OrderMapper orderMapper;

    @Test
    public void testSelectById() {
        OrderDO order = orderMapper.selectById(1);
        System.out.println(order);
    }

}

// UserMapperTest.java
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void testSelectById() {
        UserDO user = userMapper.selectById(1);
        System.out.println(user);
    }

}

运行测试代码,如果能查询出数据,没有报错,则说明多数据源配置成功

4 切换数据源以及事务相关问题

创建OrderService,下面在这个类中写多个测试方法模拟事务相关问题 场景一:

// OrderService.java

public void method01() {
    // 查询订单
    OrderDO order = orderMapper.selectById(1);
    System.out.println(order);
    // 查询用户
    UserDO user = userMapper.selectById(1);
    System.out.println(user);
}

方法没有开启事务,多数据源能正常使用

场景二:

// OrderService.java

@Transactional
public void method02() {
    // 查询订单
    OrderDO order = orderMapper.selectById(1);
    System.out.println(order);
    // 查询用户
    UserDO user = userMapper.selectById(1);
    System.out.println(user);
}

再发方法上加上事务注解,运行会报错: Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table 'test_users.orders' doesn't exist

这里就会感觉有些奇怪,我使用ordermapper按说使用的是order的数据源,但是这里却使用的是user的数据源,我们简单分析下:

  • 首先@Transactional事务注解是依赖于spring aop机制实现的,对方法切点进行环绕增强,在执行该方法前先开启事务,开启事务就会获取datasource数据源对象,因为开启使用需要获取到数据库的connection对象,而connection对象是从datasource中获取的
  • dynamic中是将DynamicRoutingDataSource对象注册到DataSourceTransactionManager 中的 事务开启时是从事务管理器DataSourceTransactionManager中拿到datasource的,DynamicRoutingDataSource是根据DS注解的value值去内部维护的map中拿到对应的数据源的
  • 在进入OrderService方法时,因为DS注解是在mapper上加的,那当进入service方法时,dynamic并没有发现存在DS注解,那么就会拿到默认的datasource让事务使用,而配置文件中默认使用的是用户的,因此这里拿到的也是用户数据源,那也就肯定不存在订单表的信息

场景三:

// OrderService.java
private OrderService self() {
    return (OrderService) AopContext.currentProxy();
}

public void method04() {
    // 查询订单
    self().method041();
    // 查询用户
    self().method042();
}

@Transactional
@DS(DBConstants.DATASOURCE_ORDERS)
public void method041() {
    OrderDO order = orderMapper.selectById(1);
    System.out.println(order);
}

@Transactional
@DS(DBConstants.DATASOURCE_USERS)
public void method042() {
    UserDO user = userMapper.selectById(1);
    System.out.println(user);
}

这里我单独在service中的方法上加上了DS注解,self()方法是用来获取代理对象的,我们应该知道aop增强是生成对应的代理类进行增强的,那希望使用到事务的增强那也一定需要使用代理类调用才会生效 这里可以去试下,调用是成功的,因为在service方法上也显示的使用DS注解指定了对应的数据源,那么事务开启时DynamicRoutingDataSource就知道给事务使用哪个数据源了,因此会拿到正确的数据源

场景四:

@Transactional
@DS(DBConstants.DATASOURCE_ORDERS)
public void method05() {
    // 查询订单
    OrderDO order = orderMapper.selectById(1);
    System.out.println(order);
    // 查询用户
    self().method052();
}

@Transactional
@DS(DBConstants.DATASOURCE_USERS)
public void method052() {
    UserDO user = userMapper.selectById(1);
    System.out.println(user);
}

这里是在一个开启事务的方法调用另一个开启事务的方法,会发现这里order查询成功,但是user报一个错误具体意思是order数据源找不到user表,这里什么原因呢:

  • 和事务的传播级别有关,默认不设置事务传播级别会使用默认的,也就是Propagation.REQUIRED 存在事务就加入当前事务,不存在就创建新事务,那么就没有使用到method052方法上面的DS注解中指定的数据源,使用的依旧是order的数据源
  • 怎么解决这个问题呢?在method052方法上的Transactional注解中设置传播级别Propagation.REQUIRES_NEW,也就是创建一个新的事务,并暂停当前事务(如果存在),这个时候使用的就是DS注解指定的数据源