在springboot项目中,我们可能会碰到需要多数据源的场景。例如说:
- 读写分离:数据库主节点压力比较大,需要增加从节点提供读操作,以减少压力。
- 多数据源:一个复杂的单体项目,因为没有拆分成不同的服务,需要连接多个业务的数据源。
本质上,读写分离,仅仅是多数据源的一个场景,从节点是只提供读操作的数据源。所以只要实现了多数据源的功能,也就能够提供读写分离。
实现多数据源大体上有三种方案,下面逐个进行介绍。
一、基于 Spring AbstractRoutingDataSource]做拓展
1.1 概述
简单来说,通过继承 AbstractRoutingDataSource 抽象类,实现一个管理项目中多个 DataSource 的动态 DynamicRoutingDataSource 实现类。这样,Spring 在获取数据源时,可以通过 DynamicRoutingDataSource 返回实际的 DataSource 。
然后,我们可以自定义一个 @DS
注解,可以添加在 Service 方法、Dao 方法上,表示其实际对应的 DataSource 。
如此,整个过程就变成,执行数据操作时,通过配置
的 @DS
注解,使用 DynamicRoutingDataSource 获得对应的实际的 DataSource 。之后,在通过该 DataSource 获得 Connection 连接,最后发起数据库操作。
开源的项目中,比较好的是 baomidou 提供的 dynamic-datasource-spring-boot-starter
。
1.2 baomidou 多数据源案例详解
使用 test_orders
和 test_users
两个数据源作为两个数据源,然后实现在其上的 SQL 操作。
1.2.1 pom文件
<?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>
<artifactId>boot-start</artifactId>
<groupId>com.yyds</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>baomidou-multiple-db</artifactId>
<dependencies>
<!-- 实现对数据库连接池的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<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>
<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>
<!--小辣椒-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
1.2.2 yml配置
server:
port: 6789
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: root
# 用户 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: root
# mybatis 配置内容
mybatis:
config-location: classpath:mybatis-config.xml # 配置 MyBatis 配置文件路径
mapper-locations: classpath:mapper/*.xml # 配置 Mapper XML 地址
type-aliases-package: com.yyds.springboot.domain # 配置数据库实体包路径
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 使用驼峰命名法转换字段。 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<typeAliases>
<typeAlias alias="Integer" type="java.lang.Integer"/>
<typeAlias alias="Long" type="java.lang.Long"/>
<typeAlias alias="HashMap" type="java.util.HashMap"/>
<typeAlias alias="LinkedHashMap" type="java.util.LinkedHashMap"/>
<typeAlias alias="ArrayList" type="java.util.ArrayList"/>
<typeAlias alias="LinkedList" type="java.util.LinkedList"/>
</typeAliases>
</configuration>
1.2.3 主启动类
package com.yyds.springboot;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@SpringBootApplication
@MapperScan(basePackages = "com.yyds.springboot.mapper")
@EnableAspectJAutoProxy(exposeProxy = true) // http://www.voidcn.com/article/p-zddcuyii-bpt.html
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
System.out.println("***********Application***************");
}
}
1.2.4 业务类
1.2.4.1 mapper层
OrderMapper
package com.yyds.springboot.mapper;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.yyds.springboot.commons.DBConstants;
import com.yyds.springboot.domain.OrderDO;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
@Repository
@DS(DBConstants.DATASOURCE_ORDERS)
public interface OrderMapper {
OrderDO selectById(@Param("id") Integer id);
}
UserMapper
package com.yyds.springboot.mapper;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.yyds.springboot.commons.DBConstants;
import com.yyds.springboot.domain.UserDO;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
@Repository
@DS(DBConstants.DATASOURCE_USERS)
public interface UserMapper {
UserDO selectById(@Param("id") Integer id);
}
-
@DS
注解,是dynamic-datasource-spring-boot-starter
提供,可添加在 Service 或 Mapper 的类/接口上,或者方法上。在其value属性,填写数据源的名字。
- OrderMapper 接口上,我们添加了
@DS(DBConstants.DATASOURCE_ORDERS)
注解,访问orders
数据源。 - UserMapper 接口上,我们添加了
@DS(DBConstants.DATASOURCE_USERS)
注解,访问users
数据源。
package com.yyds.springboot.commons; public class DBConstants { public static final String DATASOURCE_ORDERS = "orders"; public static final String DATASOURCE_USERS = "users"; }
- OrderMapper 接口上,我们添加了
1.2.4.2 mapper.xml配置
OrderMapper.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="com.yyds.springboot.mapper.OrderMapper">
<sql id="FIELDS">
id, user_id
</sql>
<select id="selectById" parameterType="Integer" resultType="OrderDO">
SELECT
<include refid="FIELDS" />
FROM orders
WHERE id = #{id}
</select>
</mapper>
UserMapper.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="com.yyds.springboot.mapper.UserMapper">
<sql id="FIELDS">
id, username
</sql>
<select id="selectById" parameterType="Integer" resultType="UserDO">
SELECT
<include refid="FIELDS" />
FROM users
WHERE id = #{id}
</select>
</mapper>
1.2.5 实体类
-- 在 `test_orders` 库中。
CREATE TABLE `orders` (
`id` int(11) DEFAULT NULL COMMENT '订单编号',
`user_id` int(16) DEFAULT NULL COMMENT '用户编号'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='订单表';
-- 在 `test_users` 库中。
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户编号',
`username` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '账号',
`password` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密码',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
OrderDO
package com.yyds.springboot.domain;
import lombok.Data;
/**
* 订单 DO
*/
@Data
public class OrderDO {
/**
* 订单编号
*/
private Integer id;
/**
* 用户编号
*/
private Integer userId;
}
UserDO
package com.yyds.springboot.domain;
import lombok.Data;
/**
* 用户 DO
*/
@Data
public class UserDO {
/**
* 用户编号
*/
private Integer id;
/**
* 账号
*/
private String username;
}
项目结构如下:
1.2.6 简单测试
package com.yyds;
import com.yyds.springboot.Application;
import com.yyds.springboot.domain.OrderDO;
import com.yyds.springboot.domain.UserDO;
import com.yyds.springboot.mapper.OrderMapper;
import com.yyds.springboot.mapper.UserMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(classes = Application.class)
class ApplicationTests {
@Autowired
private OrderMapper orderMapper;
@Test
public void testSelectOrderById() {
OrderDO order = orderMapper.selectById(1);
System.out.println(order);
}
@Autowired
private UserMapper userMapper;
@Test
public void testSelectUserById() {
UserDO user = userMapper.selectById(1);
System.out.println(user);
}
}
如果跑通,说明配置就算成功了。
1.2.7 详细测试
1.2.7.1 场景一(method01)
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserMapper userMapper;
private OrderService self() {
return (OrderService) AopContext.currentProxy();
}
public void method01() {
// 查询订单
OrderDO order = orderMapper.selectById(1);
System.out.println(order);
// 查询用户
UserDO user = userMapper.selectById(1);
System.out.println(user);
}
}
package com.yyds;
import com.yyds.springboot.Application;
import com.yyds.springboot.service.OrderService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(classes = Application.class)
class ApplicationServiceTests {
@Autowired
private OrderService orderService;
@Test
public void testSelectOrderById() {
orderService.method01();
}
}
- 方法未使用
@Transactional
注解,不会开启事务。 - 对于 OrderMapper 和 UserMapper 的查询操作,分别使用其接口上的
@DS
注解,找到对应的数据源,执行操作。 - 在未开启事务的情况下,我们已经能够自由的使用多数据源落。
1.2.7.2 场景二(method02)
@Transactional
public void method02() {
// 查询订单
OrderDO order = orderMapper.selectById(1);
System.out.println(order);
// 查询用户
UserDO user = userMapper.selectById(1);
System.out.println(user);
}
报错如下:
### SQL: SELECT id, user_id FROM orders WHERE id = ?
### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table 'test_users.orders' doesn't exist
; bad SQL grammar []; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table 'test_users.orders' doesn't exist
at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:239)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:70)
原因分析:
- 因为方法添加了
@Transactional
注解,Spring 事务就会生效。此时,Spring TransactionInterceptor 会通过 AOP 拦截该方法,创建事务。而创建事务,势必就会获得数据源。那么,TransactionInterceptor 会使用 Spring DataSourceTransactionManager 创建事务,并将事务信息通过 ThreadLocal 绑定在当前线程。
- 而事务信息,就包括事务对应的 Connection 连接。
那也就意味着,还没走到 OrderMapper 的查询操作,Connection 就已经被创建出来了。并且,因为事务信息会和当前线程绑定在一起,在 OrderMapper 在查询操作需要获得 Connection 时,就直接拿到当前线程绑定的 Connection ,而不是 OrderMapper 添加
@DS注解所对应的 DataSource 所对应的 Connection 。
- 对于每个 DataSourceTransactionManager 数据库事务管理器,创建时都会传入其需要管理的 DataSource 数据源。在使用
dynamic-datasource-spring-boot-starter
时,它创建了一个 DynamicRoutingDataSource ,传入到 DataSourceTransactionManager 中。 - 而 DynamicRoutingDataSource 负责管理我们配置的多个数据源。例如说,本示例中就管理了
orders
、users
两个数据源,并且默认使用users
数据源。那么在当前场景下,DynamicRoutingDataSource 需要基于@DS
获得数据源名,从而获得对应的 DataSource ,结果因为我们在 Service 方法上,并没有添加@DS
注解,所以它只好返回默认数据源,也就是users
。故此,就发生了Table 'test_users.orders' doesn't exist
的异常。
1.2.7.3 场景三(method03)
public void method03() {
// 查询订单
self().method031();
// 查询用户
self().method032();
}
@Transactional // 报错,因为此时获取的是 primary 对应的 DataSource ,即 users 。
public void method031() {
OrderDO order = orderMapper.selectById(1);
System.out.println(order);
}
@Transactional
public void method032() {
UserDO user = userMapper.selectById(1);
System.out.println(user);
}
场景三和场景二是等价的。
将 #self()
代码替换成 this
之后,诶,结果就正常执行。这又是为什么呢?胖友在思考一波。
其实,这样调整后,因为
this
不是代理对象,所以#method031()
和#method032()
方法上的@Transactional
直接没有作用,Spring 事务根本没有生效。所以,最终结果和场景一是等价的。
1.2.7.4 场景四(method04)
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);
}
执行方法,正常结束,未抛出异常。
- 在执行
#method041()
方法前,因为有@Transactional
注解,所以 Spring 事务机制触发。DynamicRoutingDataSource 根据@DS
注解,获得对应的orders
的 DataSource ,从而获得 Connection 。所以后续 OrderMapper 执行查询操作时,即使使用的是线程绑定的 Connection ,也可能不会报错。实际上,此时 OrderMapper 上的@DS
注解,也没有作用。 - 对于
#method042()
,也是同理。在 Spring 事务机制中,在一个事务执行完成后,会将事务信息和当前线程解绑。所以,在执行#method042()
方法前,又可以执行一轮事务的逻辑。 - 【重要】总的来说,对于声明了
@Transactional
的 Service 方法上,也同时通过@DS
声明对应的数据源。
1.2.7.5 场景五(method05)
@Transactional
@DS(DBConstants.DATASOURCE_ORDERS)
public void method05() {
// 查询订单
OrderDO order = orderMapper.selectById(1);
System.out.println(order);
// 查询用户
self().method052();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
@DS(DBConstants.DATASOURCE_USERS)
public void method052() {
UserDO user = userMapper.selectById(1);
System.out.println(user);
}
- 和
@method04()
方法,差异在于,我们直接在#method05()
方法中,此时处于一个事务中,直接调用了#method052()
方法。 执行方法,正常结束,未抛出异常。
- 添加的
@Transactionl
注解,使用的事务传播级别是Propagation.REQUIRES_NEW
。此时,在执行#method052()
方法之前,TransactionInterceptor 会将原事务挂起,暂时性的将原事务信息和当前线程解绑。- 所以,在执行
#method052()
方法前,又可以执行一轮事务的逻辑。 - 之后,在执行
#method052()
方法完成后,会将原事务恢复,重新将原事务信息和当前线程绑定。
- 所以,在执行
总结:【基于 Spring AbstractRoutingDataSource 做拓展
,切换数据源,可能产生多个事务,就会碰到多个事务一致性的问题,也就是分布式事务。】
1.3 baomidou读写分离案例详解
1.3.1 pom文件
<?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>
<artifactId>boot-start</artifactId>
<groupId>com.yyds</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>baomidou-rw</artifactId>
<dependencies>
<!-- 实现对数据库连接池的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<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>
<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>
<!--小辣椒-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
1.3.2 yml配置
server:
port: 6789
spring:
datasource:
# dynamic-datasource-spring-boot-starter 动态数据源的配置内容
dynamic:
primary: master # 设置默认的数据源或者数据源组,默认值即为 master
datasource:
# 订单 orders 主库的数据源配置
master:
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: root
# 订单 orders 从库数据源配置(模拟)
slave:
url: jdbc:mysql://127.0.0.1:3306/test_orders_01?useSSL=false&useUnicode=true&characterEncoding=UTF-8
driver-class-name: com.mysql.jdbc.Driver
username: root
password: root
# mybatis 配置内容
mybatis:
config-location: classpath:mybatis-config.xml # 配置 MyBatis 配置文件路径
mapper-locations: classpath:mapper/*.xml # 配置 Mapper XML 地址
type-aliases-package: com.yyds.springboot.domain # 配置数据库实体包路径
-
我们配置了订单库的多个数据源:
master
:订单库的主库。slave
:订单库的1个从库。
-
在dynamic-datasource-spring-boot-starter中,多个相同角色的数据源可以形成一个数据源组。
例如说,slave_1和slave_2形成了slave组。
- 我们可以使用
@DS("slave_1")
或@DS("slave_2")
注解,明确访问数据源组的指定数据源。 - 也可以使用
@DS("slave")
注解,此时会负载均衡,选择分组中的某个数据源进行访问。负载均衡默认采用轮询的方式。
- 我们可以使用
-
本地并未搭建 MySQL 一主多从的环境,所以是通过创建了
test_orders_01
库,手动模拟作为test_orders
的从库。
1.3.3 主启动类
package com.yyds.springboot;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@SpringBootApplication
@MapperScan(basePackages = "com.yyds.springboot.mapper")
@EnableAspectJAutoProxy(exposeProxy = true) // http://www.voidcn.com/article/p-zddcuyii-bpt.html
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
System.out.println("***********Application***************");
}
}
1.3.4 业务类
package com.yyds.springboot.mapper;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.yyds.springboot.commons.DBConstants;
import com.yyds.springboot.domain.OrderDO;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
@Repository
@DS(DBConstants.DATASOURCE_ORDERS)
public interface OrderMapper {
@DS(DBConstants.DATASOURCE_SLAVE)
OrderDO selectById(@Param("id") Integer id);
@DS(DBConstants.DATASOURCE_MASTER)
int insert(OrderDO entity);
}
- 对
#selectById(Integer id)
读操作,我们配置了@DS(DBConstants.DATASOURCE_SLAVE)
,访问从库。 - 对
#insert(OrderDO entity)
写操作,我们配置了@DS(DBConstants.DATASOURCE_MASTER)
,访问主库。
OrderMapper.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="com.yyds.springboot.mapper.OrderMapper">
<sql id="FIELDS">
id, user_id
</sql>
<select id="selectById" parameterType="Integer" resultType="OrderDO">
SELECT
<include refid="FIELDS" />
FROM orders
WHERE id = #{id}
</select>
<insert id="insert" parameterType="OrderDO" useGeneratedKeys="true" keyProperty="id">
INSERT INTO orders (
user_id
) VALUES (
#{userId}
)
</insert>
</mapper>
mybatis-config.xml配置
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 使用驼峰命名法转换字段。 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<typeAliases>
<typeAlias alias="Integer" type="java.lang.Integer"/>
<typeAlias alias="Long" type="java.lang.Long"/>
<typeAlias alias="HashMap" type="java.util.HashMap"/>
<typeAlias alias="LinkedHashMap" type="java.util.LinkedHashMap"/>
<typeAlias alias="ArrayList" type="java.util.ArrayList"/>
<typeAlias alias="LinkedList" type="java.util.LinkedList"/>
</typeAliases>
</configuration>
1.3.5 实体类
package com.yyds.springboot.domain;
import lombok.Data;
/**
* 订单 DO
*/
@Data
public class OrderDO {
/**
* 订单编号
*/
private Integer id;
/**
* 用户编号
*/
private Integer userId;
}
package com.yyds.springboot.commons;
public class DBConstants {
public static final String DATASOURCE_MASTER = "master";
public static final String DATASOURCE_SLAVE = "slave";
public static final String DATASOURCE_ORDERS = "orders";
}
1.3.6 简单测试
package com.yyds.springboot;
import com.yyds.springboot.domain.OrderDO;
import com.yyds.springboot.mapper.OrderMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(classes = Application.class)
public class OrderMapperTest {
@Autowired
private OrderMapper orderMapper;
@Test
public void testSelectById() {
for (int i = 0; i < 10; i++) {
OrderDO order = orderMapper.selectById(1);
System.out.println(order);
}
}
@Test
public void testInsert() {
OrderDO order = new OrderDO();
order.setUserId(10);
orderMapper.insert(order);
}
}
如果跑通,说明配置就算成功了。
1.3.7 详细测试
service层
package com.yyds.springboot.service;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.yyds.springboot.commons.DBConstants;
import com.yyds.springboot.domain.OrderDO;
import com.yyds.springboot.mapper.OrderMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Transactional
@DS(DBConstants.DATASOURCE_MASTER)
public void add(OrderDO order) {
// 这里先模拟读取一下
orderMapper.selectById(order.getId());
// 插入订单
orderMapper.insert(order);
}
public OrderDO findById(Integer id) {
return orderMapper.selectById(id);
}
}
- 对于
#add(OrderDO order)
方法,我们希望在@Transactional
声明的事务中,读操作也访问主库,所以声明了@DS(DBConstants.DATASOURCE_MASTER)
。因此,后续的所有 OrderMapper 的操作,都访问的是订单库的MASTER
数据源。 - 对于
#findById(Integer id)
方法,读取指定订单信息,使用 OrderMapper 的#selectById(Integer id)
配置的SLAVE
数据源即可。
测试
package com.yyds.springboot;
import com.yyds.springboot.domain.OrderDO;
import com.yyds.springboot.service.OrderService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(classes = Application.class)
public class OrderServiceTest {
@Autowired
private OrderService orderService;
@Test
public void testAdd() {
OrderDO order = new OrderDO();
order.setUserId(20);
orderService.add(order);
}
@Test
public void testFindById() {
OrderDO order = orderService.findById(1);
System.out.println(order);
}
}
如果跑通,说明配置就算成功了。
二、不同操作类,固定数据源
以 MyBatis 举例子,假设有 orders
和 users
两个数据源。 那么我们可以创建两个 SqlSessionTemplate ordersSqlSessionTemplate
和 usersSqlSessionTemplate
,分别使用这两个数据源。然后,配置不同的 Mapper 使用不同的 SqlSessionTemplate 。
如此,整个过程就变成,执行数据操作时,通过 Mapper 可以对应到其 SqlSessionTemplate ,使用 SqlSessionTemplate 获得对应的实际的 DataSource 。之后,在通过该 DataSource 获得 Connection 连接,最后发起数据库操作。
这种方式在结合 Spring 事务的时候,也会存在无法切换数据源的问题。
2.1 mybatis多数据源案例详解
2.1.1 pom文件
<?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>
<artifactId>boot-start</artifactId>
<groupId>com.yyds</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>mybatis-multiple-db</artifactId>
<dependencies>
<!-- 实现对数据库连接池的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
<!-- MyBatis 相关依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<!-- 保证 Spring AOP 相关的依赖包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
2.1.2 yml配置
spring:
# datasource 数据源配置内容
datasource:
# 订单数据源配置
orders:
jdbc-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: root
# 用户数据源配置
users:
jdbc-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: root
# 注意此时并不使用它实现对 MyBatis 的自动化配置。这么引入,只是单纯方便,实际只要引入 mybatis 和 mybatis-spring 即可。
2.1.3 主启动类
package com.yyds.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
System.out.println("***********Application***************");
}
}
2.1.4 业务类
package com.yyds.springboot.mapper.orders;
import com.yyds.springboot.domain.OrderDO;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
@Repository
public interface OrderMapper {
OrderDO selectById(@Param("id") Integer id);
}
package com.yyds.springboot.mapper.users;
import com.yyds.springboot.domain.UserDO;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
@Repository
public interface UserMapper {
UserDO selectById(@Param("id") Integer id);
}
2.1.5 实体类及配置类
实体类和之前一样。
MyBatisOrdersConfig
package com.yyds.springboot.config;
import com.yyds.springboot.commons.DBConstants;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
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.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
@Configuration
@MapperScan(basePackages = "com.yyds.springboot.mapper.orders", sqlSessionTemplateRef = "ordersSqlSessionTemplate")
public class MyBatisOrdersConfig {
/**
* 创建 orders 数据源
*/
@Bean(name = "ordersDataSource")
@ConfigurationProperties(prefix = "spring.datasource.orders")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
/**
* 创建 MyBatis SqlSessionFactory
*/
@Bean(name = "ordersSqlSessionFactory")
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
// <2.1> 设置 orders 数据源
bean.setDataSource(this.dataSource());
// <2.2> 设置 entity 所在包
bean.setTypeAliasesPackage("com.yyds.springboot.domain");
// <2.3> 设置 config 路径
bean.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("classpath:mybatis-config.xml"));
// <2.4> 设置 mapper 路径
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
return bean.getObject();
}
/**
* 创建 MyBatis SqlSessionTemplate
*/
@Bean(name = "ordersSqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate() throws Exception {
return new SqlSessionTemplate(this.sqlSessionFactory());
}
/**
* 创建 orders 数据源的 TransactionManager 事务管理器
*/
@Bean(name = DBConstants.TX_MANAGER_ORDERS)
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(this.dataSource());
}
}
MyBatisUsersConfig和MyBatisOrdersConfig基本一致。
2.1.6 简单测试
package com.yyds.springboot;
import com.yyds.springboot.domain.OrderDO;
import com.yyds.springboot.domain.UserDO;
import com.yyds.springboot.mapper.orders.OrderMapper;
import com.yyds.springboot.mapper.users.UserMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(classes = Application.class)
public class MapperTest {
@Autowired
private OrderMapper orderMapper;
@Test
public void testSelectById() {
OrderDO order = orderMapper.selectById(1);
System.out.println(order);
}
@Autowired
private UserMapper userMapper;
@Test
public void testSelectByUserId() {
UserDO user = userMapper.selectById(1);
System.out.println(user);
}
}
如果跑通,说明配置就算成功了。
2.1.7 详细测试
2.1.7.1 场景一(method01)
package com.yyds.springboot.service;
import com.yyds.springboot.commons.DBConstants;
import com.yyds.springboot.domain.OrderDO;
import com.yyds.springboot.domain.UserDO;
import com.yyds.springboot.mapper.orders.OrderMapper;
import com.yyds.springboot.mapper.users.UserMapper;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserMapper userMapper;
private OrderService self() {
return (OrderService) AopContext.currentProxy();
}
public void method01() {
// 查询订单
OrderDO order = orderMapper.selectById(1);
System.out.println(order);
// 查询用户
UserDO user = userMapper.selectById(1);
System.out.println(user);
}
}
- 方法未使用
@Transactional
注解,不会开启事务。 - 对于 OrderMapper 和 UserMapper 的查询操作,分别使用其接口对应的 SqlSessionTemplate ,找到对应的数据源,执行操作。
2.1.7.2 场景二(method02)
@Transactional // 报错,找不到事务管理器
public void method02() {
// 查询订单
OrderDO order = orderMapper.selectById(1);
System.out.println(order);
// 查询用户
UserDO user = userMapper.selectById(1);
System.out.println(user);
}
执行方法,抛出异常:
- 在
@Transactional
注解上,如果未设置使用的事务管理器,它会去选择一个事务管理器。但是,我们这里创建了ordersTransactionManager
和usersTransactionManager
两个事务管理器,它就不知道怎么选了。此时,它只好抛出 NoUniqueBeanDefinitionException 异常。
2.1.7.3 场景三(method03)
public void method03() {
// 查询订单
self().method031();
// 查询用户
self().method032();
}
@Transactional(transactionManager = DBConstants.TX_MANAGER_ORDERS)
public void method031() {
OrderDO order = orderMapper.selectById(1);
System.out.println(order);
}
@Transactional(transactionManager = DBConstants.TX_MANAGER_USERS)
public void method032() {
UserDO user = userMapper.selectById(1);
System.out.println(user);
}
- 执行方法,正常结束,未抛出异常。
#method031()
和#method032()
方法上,声明的事务管理器,和后续 Mapper 操作是同一个 DataSource 数据源,从而保证不报错。
2.1.7.4 场景四(method04)
@Transactional(transactionManager = DBConstants.TX_MANAGER_ORDERS)
public void method04() {
// 查询订单
OrderDO order = orderMapper.selectById(1);
System.out.println(order);
// 查询用户
self().method041();
}
@Transactional(transactionManager = DBConstants.TX_MANAGER_USERS,
propagation = Propagation.REQUIRES_NEW)
public void method041() {
UserDO user = userMapper.selectById(1);
System.out.println(user);
}
- 执行方法,正常结束,未抛出异常。
- 所以,在执行
#method041()
方法前,又可以执行一轮事务的逻辑。 - 之后,在执行
#method041()
方法完成后,会将原事务恢复,重新将原事务信息和当前线程绑定。
- 所以,在执行
2.2 Spring Data JPA 多数据源案例详解
2.2.1 pom文件
<?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>
<artifactId>boot-start</artifactId>
<groupId>com.yyds</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jpa-multiple-db</artifactId>
<dependencies>
<!-- 实现对数据库连接池的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
<!-- JPA 相关依赖 -->
<!--对于 spring-boot-starter-data-jpa 依赖,这里并不使用它实现对 JPA 的自动化配置。
这么引入,只是单纯方便,不然需要引入 spring-data-jpa 和 hibernate-core 等依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--小辣椒-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
2.2.2 yml配置
spring:
# datasource 数据源配置内容
datasource:
# 订单数据源配置
orders:
jdbc-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: root
# 用户数据源配置
users:
jdbc-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: root
jpa:
show-sql: true # 打印 SQL
# Hibernate 配置内容,对应 HibernateProperties 类
hibernate:
ddl-auto: none
2.2.3 主启动类
package com.yyds.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
System.out.println("***********Application***************");
}
}
2.2.4 业务类
OrderRepository
package com.yyds.springboot.repository.orders;
import com.yyds.springboot.domain.OrderDO;
import org.springframework.data.repository.CrudRepository;
public interface OrderRepository extends CrudRepository<OrderDO, Integer> {
}
UserRepository
package com.yyds.springboot.repository.users;
import com.yyds.springboot.domain.UserDO;
import org.springframework.data.repository.CrudRepository;
public interface UserRepository extends CrudRepository<UserDO, Integer> {
}
2.2.5 实体类及配置类
OrderDO
package com.yyds.springboot.domain;
import lombok.Data;
/**
* 订单 DO
*/
import javax.persistence.*;
@Entity
@Table(name = "orders")
@Data
public class OrderDO {
/**
* 订单编号
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY, // strategy 设置使用数据库主键自增策略;
generator = "JDBC") // generator 设置插入完成后,查询最后生成的 ID 填充到该属性中。
private Integer id;
/**
* 用户编号
*/
@Column(name = "user_id")
private Integer userId;
}
UserDO
package com.yyds.springboot.domain;
import lombok.Data;
import javax.persistence.*;
/**
* 用户 DO
*/
@Data
@Entity
@Table(name = "users")
public class UserDO {
/**
* 用户编号
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY, // strategy 设置使用数据库主键自增策略;
generator = "JDBC") // generator 设置插入完成后,查询最后生成的 ID 填充到该属性中。
private Integer id;
/**
* 账号
*/
private String username;
}
DBConstants
package com.yyds.springboot.common;
public class DBConstants {
/**
* 事务管理器 - 订单库
*/
public static final String TX_MANAGER_ORDERS = "ordersTransactionManager";
/**
* 事务管理器 - 用户库
*/
public static final String TX_MANAGER_USERS = "usersTransactionManager";
/**
* 实体管理器工厂 - 订单库
*/
public static final String ENTITY_MANAGER_FACTORY_ORDERS = "ordersEntityManagerFactory";
/**
* 实体管理器工厂 - 用户库
*/
public static final String ENTITY_MANAGER_FACTORY_USERS = "usersEntityManagerFactory";
}
HibernateConfig
package com.yyds.springboot.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateSettings;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
import java.util.Map;
@Configuration
public class HibernateConfig {
@Resource
private JpaProperties jpaProperties;
@Resource
private HibernateProperties hibernateProperties;
/**
* 获取 Hibernate Vendor 相关配置
*/
@Bean(name = "hibernateVendorProperties")
public Map<String, Object> hibernateVendorProperties() {
return hibernateProperties.determineHibernateProperties(
jpaProperties.getProperties(), new HibernateSettings()
);
}
}
JpaOrdersConfig
- JpaOrdersConfig 配置类,配置使用
orders
数据源的 Spring Data JPA 配置。 - JpaUsersConfig 配置类,配置使用
users
数据源的 Spring Data JPA 配置。
package com.yyds.springboot.config;
import com.yyds.springboot.common.DBConstants;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.Map;
@Configuration
@EnableJpaRepositories(
entityManagerFactoryRef = DBConstants.ENTITY_MANAGER_FACTORY_ORDERS,
transactionManagerRef = DBConstants.TX_MANAGER_ORDERS,
basePackages = {"com.yyds.springboot.repository.orders"}) // 设置接口所在包
public class JpaOrdersConfig {
@Resource(name = "hibernateVendorProperties")
private Map<String, Object> hibernateVendorProperties;
/**
* 创建 orders 数据源
*/
@Bean(name = "ordersDataSource")
@ConfigurationProperties(prefix = "spring.datasource.orders")
@Primary // 需要特殊添加,否则初始化会有问题
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
/**
* 创建 LocalContainerEntityManagerFactoryBean
*/
@Bean(name = DBConstants.ENTITY_MANAGER_FACTORY_ORDERS)
public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder) {
return builder
.dataSource(this.dataSource()) // 数据源
.properties(hibernateVendorProperties) // 获取并注入 Hibernate Vendor 相关配置
.packages("com.yyds.springboot.domain") // 数据库实体 domain 所在包
.persistenceUnit("ordersPersistenceUnit") // 设置持久单元的名字,需要唯一
.build();
}
/**
* 创建 PlatformTransactionManager
*/
@Bean(name = DBConstants.TX_MANAGER_ORDERS)
public PlatformTransactionManager transactionManager(EntityManagerFactoryBuilder builder) {
return new JpaTransactionManager(entityManagerFactory(builder).getObject());
}
}
2.2.6 简单测试
package com.yyds.springboot;
import com.yyds.springboot.domain.OrderDO;
import com.yyds.springboot.domain.UserDO;
import com.yyds.springboot.repository.orders.OrderRepository;
import com.yyds.springboot.repository.users.UserRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Optional;
@SpringBootTest(classes = Application.class)
public class MapperTest {
@Autowired
private OrderRepository orderRepository;
@Test
public void testSelectById() {
Optional<OrderDO> orderDO = orderRepository.findById(1);
System.out.println(orderDO.get());
}
@Autowired
private UserRepository userRepository;
@Test
public void testSelectByUserId() {
Optional<UserDO> userDO = userRepository.findById(1);
System.out.println(userDO.get());
}
}
2.2.7 详细测试
package com.yyds.springboot.service;
import com.yyds.springboot.common.DBConstants;
import com.yyds.springboot.domain.OrderDO;
import com.yyds.springboot.domain.UserDO;
import com.yyds.springboot.repository.orders.OrderRepository;
import com.yyds.springboot.repository.users.UserRepository;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private UserRepository userRepository;
private OrderService self() {
return (OrderService) AopContext.currentProxy();
}
public void method01() {
// 查询订单
OrderDO order = orderRepository.findById(1).orElse(null);
System.out.println(order);
// 查询用户
UserDO user = userRepository.findById(1).orElse(null);
System.out.println(user);
}
@Transactional // 报错,找不到事务管理器
public void method02() {
// 查询订单
OrderDO order = orderRepository.findById(1).orElse(null);
System.out.println(order);
// 查询用户
UserDO user = userRepository.findById(1).orElse(null);
System.out.println(user);
}
public void method03() {
// 查询订单
self().method031();
// 查询用户
self().method032();
}
@Transactional(transactionManager = DBConstants.TX_MANAGER_ORDERS)
public void method031() {
OrderDO order = orderRepository.findById(1).orElse(null);
System.out.println(order);
}
@Transactional(transactionManager = DBConstants.TX_MANAGER_USERS)
public void method032() {
UserDO user = userRepository.findById(1).orElse(null);
System.out.println(user);
}
@Transactional(transactionManager = DBConstants.TX_MANAGER_ORDERS)
public void method04() {
// 查询订单
OrderDO order = orderRepository.findById(1).orElse(null);
System.out.println(order);
// 查询用户
self().method041();
}
@Transactional(transactionManager = DBConstants.TX_MANAGER_USERS,
propagation = Propagation.REQUIRES_NEW)
public void method041() {
UserDO user = userRepository.findById(1).orElse(null);
System.out.println(user);
}
}
2.3 JdbcTemplate 多数据源案例详解
2.3.1 pom文件
<?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>
<artifactId>boot-start</artifactId>
<groupId>com.yyds</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jdbctemplate-multiple-db</artifactId>
<dependencies>
<!-- 实现对数据库连接池的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
<!-- 保证 Spring AOP 相关的依赖包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
<!-- 方便等会写单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
2.3.2 yml配置
spring:
# datasource 数据源配置内容
datasource:
# 订单数据源配置
orders:
jdbc-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: root
# 用户数据源配置
users:
jdbc-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: root
2.3.3 主启动类
package com.yyds.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
System.out.println("***********Application***************");
}
}
2.3.4 业务类
dao层
package com.yyds.springboot.dao;
import com.yyds.springboot.constant.DBConstants;
import com.yyds.springboot.dataobject.OrderDO;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
@Repository
public class OrderDao {
@Resource(name = DBConstants.JDBC_TEMPLATE_ORDERS)
private JdbcTemplate template;
public OrderDO selectById(Integer id) {
return template.queryForObject("SELECT id, user_id FROM orders WHERE id = ?",
new BeanPropertyRowMapper<>(OrderDO.class), // 结果转换成对应的对象
id);
}
}
package com.yyds.springboot.dao;
import com.yyds.springboot.constant.DBConstants;
import com.yyds.springboot.dataobject.UserDO;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
@Repository
public class UserDao {
@Resource(name = DBConstants.JDBC_TEMPLATE_USERS)
private JdbcTemplate template;
public UserDO selectById(Integer id) {
return template.queryForObject("SELECT id, username FROM users WHERE id = ?",
new BeanPropertyRowMapper<>(UserDO.class), // 结果转换成对应的对象
id);
}
}
2.3.5 实体类及配置类
package com.yyds.springboot.constant;
/**
* 数据库枚举类
*/
public class DBConstants {
/**
* 事务管理器 - 订单库
*/
public static final String TX_MANAGER_ORDERS = "ordersTransactionManager";
/**
* 事务管理器 - 用户库
*/
public static final String TX_MANAGER_USERS = "usersTransactionManager";
/**
* JdbcTemplate - 订单库
*/
public static final String JDBC_TEMPLATE_ORDERS = "ordersJdbcTemplate";
/**
* JdbcTemplate - 用户库
*/
public static final String JDBC_TEMPLATE_USERS = "usersJdbcTemplate";
}
- JdbcTemplateOrdersConfig 配置类,配置使用
orders
数据源的 MyBatis 配置。 - JdbcTemplateUsersConfig配置类,配置使用
users
数据源的 MyBatis 配置。
package com.yyds.springboot.config;
import com.yyds.springboot.constant.DBConstants;
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.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
@Configuration
public class JdbcTemplateOrdersConfig {
/**
* 创建 orders 数据源
*/
@Bean(name = "ordersDataSource")
@ConfigurationProperties(prefix = "spring.datasource.orders")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
/**
* 创建 orders JdbcTemplate
*/
@Bean(name = DBConstants.JDBC_TEMPLATE_ORDERS)
public JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(this.dataSource());
}
/**
* 创建 orders 数据源的 TransactionManager 事务管理器
*/
@Bean(name = DBConstants.TX_MANAGER_ORDERS)
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(this.dataSource());
}
}
package com.yyds.springboot.config;
import com.yyds.springboot.constant.DBConstants;
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.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
@Configuration
public class JdbcTemplateUsersConfig {
/**
* 创建 users 数据源
*/
@Bean(name = "usersDataSource")
@ConfigurationProperties(prefix = "spring.datasource.users")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
/**
* 创建 users JdbcTemplate
*/
@Bean(name = DBConstants.JDBC_TEMPLATE_USERS)
public JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(this.dataSource());
}
/**
* 创建 users 数据源的 TransactionManager 事务管理器
*/
@Bean(name = DBConstants.TX_MANAGER_USERS)
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(this.dataSource());
}
}
2.3.6 简单测试
package com.yyds.springboot;
import com.yyds.springboot.dao.OrderDao;
import com.yyds.springboot.dao.UserDao;
import com.yyds.springboot.dataobject.OrderDO;
import com.yyds.springboot.dataobject.UserDO;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(classes = Application.class)
public class DaoTest {
@Autowired
private OrderDao orderDao;
@Test
public void testSelectById() {
OrderDO order = orderDao.selectById(1);
System.out.println(order);
}
@Autowired
private UserDao userDao;
@Test
public void testSelectByUserId() {
UserDO user = userDao.selectById(1);
System.out.println(user);
}
}
2.3.7 详细测试
package com.yyds.springboot.service;
import com.yyds.springboot.constant.DBConstants;
import com.yyds.springboot.dao.OrderDao;
import com.yyds.springboot.dao.UserDao;
import com.yyds.springboot.dataobject.OrderDO;
import com.yyds.springboot.dataobject.UserDO;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private UserDao userDao;
private OrderService self() {
return (OrderService) AopContext.currentProxy();
}
public void method01() {
// 查询订单
OrderDO order = orderDao.selectById(1);
System.out.println(order);
// 查询用户
UserDO user = userDao.selectById(1);
System.out.println(user);
}
@Transactional // 报错,找不到事务管理器
public void method02() {
// 查询订单
OrderDO order = orderDao.selectById(1);
System.out.println(order);
// 查询用户
UserDO user = userDao.selectById(1);
System.out.println(user);
}
public void method03() {
// 查询订单
self().method031();
// 查询用户
self().method032();
}
@Transactional(transactionManager = DBConstants.TX_MANAGER_ORDERS)
public void method031() {
OrderDO order = orderDao.selectById(1);
System.out.println(order);
}
@Transactional(transactionManager = DBConstants.TX_MANAGER_USERS)
public void method032() {
UserDO user = userDao.selectById(1);
System.out.println(user);
}
@Transactional(transactionManager = DBConstants.TX_MANAGER_ORDERS)
public void method04() {
// 查询订单
OrderDO order = orderDao.selectById(1);
System.out.println(order);
// 查询用户
self().method041();
}
@Transactional(transactionManager = DBConstants.TX_MANAGER_USERS,
propagation = Propagation.REQUIRES_NEW)
public void method041() {
UserDO user = userDao.selectById(1);
System.out.println(user);
}
}
三、分库分表中间件
对于分库分表的中间件,会解析我们编写的 SQL ,路由操作到对应的数据源。那么,它们天然就支持多数据源。如此,我们仅需配置好每个表对应的数据源,中间件就可以透明的实现多数据源或者读写分离。
目前,Java 最好用的分库分表中间件之一,就是 Apache ShardingSphere 。
那么,这种方式在结合 Spring 事务的时候,会不会存在无法切换数据源的问题呢?答案是不会。在上述的方案一和方案二中,在 Spring 事务中,会获得对应的 DataSource ,再获得 Connection 进行数据库操作。而获得的 Connection 以及其上的事务,会通过 ThreadLocal 的方式和当前线程进行绑定。这样,就导致我们无法切换数据源。
难道分库分表中间件不也是需要 Connection 进行这些事情么?答案是的,但是不同的是分库分表中间件返回的 Connection 返回的实际是动态的 DynamicRoutingConnection ,它管理了整个请求(逻辑)过程中,使用的所有的 Connection ,而最终执行 SQL 的时候,DynamicRoutingConnection 会解析 SQL ,获得表对应的真正的 Connection 执行 SQL 操作。
后面会介绍分库分表中间件的案例详解。