dynamic多数据源分析实现
文章开始前,推荐下博主自己的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注解指定的数据源
转载自:https://juejin.cn/post/7347165355586486309