目录
- 1. 原理
- 1.1 实现多数据源的原理
- 1.2 多数据源本地事务的原理
- 2. Mysql数据准备
- 3. pom.xml依赖
- 4. application.properties配置
- 5. 创建两个SqlSessionFactory
- 5.1 DataSource1Config
- 5.1 DataSource2Config
- 6. 动态数据源测试
- 6.1 创建User类
- 6.2 Mapper接口实现
- 6.3 Mapper.xml实现
- 6.4 Service实现
- 6.5 测试
- 7. AOP + 自定义注解实现事务(未测试)
1. 原理
1.1 实现多数据源的原理
通过启动多个SqlSessionFactoryBean,每个SqlSessionFactoryBean对应一个datasource和指定位置的mapper.xml文件,就可以实现多个数据源了。而不用切换数据源,不用实现AbstractRoutingDataSource
1.2 多数据源本地事务的原理
在多数据源下,涉及到多个数据库的写入。Spring的声明式事务在一次请求线程中只能对一个数据源进行控制。因为一个DataSourceTransactionManager无法完成对多数据源的控制
需要多个DataSourceTransactionManager可以完成,但@Transactional注解无法管理多个数据源,这里我们通过变通的方式让@Transactional管理多个DataSourceTransactionManager
将每个datasource都分别和一个DataSourceTransactionManager进行绑定。然后可以通过嵌套事务的方式进行调用
2. Mysql数据准备
分别创建write_db1.user和write_db2.user
mysql> create database write_db1;
Query OK, 1 row affected (0.14 sec)mysql> create database write_db2;
Query OK, 1 row affected (0.01 sec)mysql> create table write_db1.user (-> id bigint(20) auto_increment not null comment '主键ID',-> name varchar(30) null default null comment '姓名',-> primary key (id)-> );
Query OK, 0 rows affected, 1 warning (0.29 sec)mysql>
mysql> create table write_db2.user (-> id bigint(20) auto_increment not null comment '主键ID',-> name varchar(30) null default null comment '姓名',-> primary key (id)-> );
Query OK, 0 rows affected, 1 warning (0.04 sec)mysql>
3. pom.xml依赖
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.31</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.15</version></dependency><!-- 支持spring 2.5.3 --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.2</version></dependency><!-- 用于编程式事务 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
4. application.properties配置
指定了datasource1和datasource2两个DataSource
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource# 用于读的数据库
spring.datasource.datasource1.url=jdbc:mysql://192.168.28.12:3306/write_db1
spring.datasource.datasource1.username=root
spring.datasource.datasource1.password=Root_123
spring.datasource.datasource1.driver-class-name=com.mysql.cj.jdbc.Driver# 用于写的数据库
spring.datasource.datasource2.url=jdbc:mysql://192.168.28.12:3306/write_db2
spring.datasource.datasource2.username=root
spring.datasource.datasource2.password=Root_123
spring.datasource.datasource2.driver-class-name=com.mysql.cj.jdbc.Driver
5. 创建两个SqlSessionFactory
5.1 DataSource1Config
其实就是将datasource1和指定的Mapper接口、Mapper.xml文件进行绑定。然后将datasource1TransactionManager和datasource1绑定,datasource1TransactionTemplate和datasource1TransactionManager进行绑定
说明:
- 通过@MapperScan注解,指定Mapper接口的位置和SqlSessionFactory的名称
- 指定了要连接的数据源datasource1
- 指定了Mapper.xml文件的位置
package com.hh.springboottest.config;import com.alibaba.druid.pool.DruidDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
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.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;import javax.sql.DataSource;@Configuration
@MapperScan(basePackages = {"com.hh.springboottest.mapper.datasource1"},sqlSessionFactoryRef = "datasource1SqlSessionFactory")
public class DataSource1Config {@ConfigurationProperties(prefix = "spring.datasource.datasource1")// 向IOC容器添加name = dataSource1的DataSource@Beanpublic DataSource dataSource1() {DruidDataSource druidDataSource = new DruidDataSource();return druidDataSource;}@Bean@Primarypublic SqlSessionFactory datasource1SqlSessionFactory(@Qualifier("dataSource1") DataSource dataSource) throws Exception {final SqlSessionFactoryBean datasource1SqlSessionFactoryBean = new SqlSessionFactoryBean();// 指定数据源datasource1SqlSessionFactoryBean.setDataSource(dataSource);// 指定数据源对应的mapper.xml文件datasource1SqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/datasource1/*.xml"));return datasource1SqlSessionFactoryBean.getObject();}@Bean@Primarypublic DataSourceTransactionManager datasource1TransactionManager() {DataSourceTransactionManager datasource1TransactionManager =new DataSourceTransactionManager();datasource1TransactionManager.setDataSource(dataSource1());return datasource1TransactionManager;}@Beanpublic TransactionTemplate datasource1TransactionTemplate() {return new TransactionTemplate(datasource1TransactionManager());}}
5.1 DataSource2Config
package com.hh.springboottest.config;import com.alibaba.druid.pool.DruidDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
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.support.TransactionTemplate;import javax.sql.DataSource;@Configuration
@MapperScan(basePackages = {"com.hh.springboottest.mapper.datasource2"},sqlSessionFactoryRef = "datasource2SqlSessionFactory")
public class DataSource2Config {@ConfigurationProperties(prefix = "spring.datasource.datasource2")// 向IOC容器添加name = dataSource2的DataSource@Beanpublic DataSource dataSource2() {DruidDataSource druidDataSource = new DruidDataSource();return druidDataSource;}@Beanpublic SqlSessionFactory datasource2SqlSessionFactory(@Qualifier("dataSource2") DataSource dataSource) throws Exception {final SqlSessionFactoryBean datasource2SqlSessionFactoryBean = new SqlSessionFactoryBean();// 指定数据源datasource2SqlSessionFactoryBean.setDataSource(dataSource);// 指定数据源对应的mapper.xml文件datasource2SqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/datasource2/*.xml"));return datasource2SqlSessionFactoryBean.getObject();}@Beanpublic DataSourceTransactionManager datasource2TransactionManager() {DataSourceTransactionManager datasource2TransactionManager =new DataSourceTransactionManager();datasource2TransactionManager.setDataSource(dataSource2());return datasource2TransactionManager;}@Beanpublic TransactionTemplate datasource2TransactionTemplate() {return new TransactionTemplate(datasource2TransactionManager());}}
6. 动态数据源测试
6.1 创建User类
package com.hh.springboottest.myController;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;@NoArgsConstructor
@AllArgsConstructor
@Data
@ToString
public class User {private Long id;private String name;}
6.2 Mapper接口实现
Datasource1UserMapper.java
package com.hh.springboottest.mapper.datasource1;import com.hh.springboottest.myController.User;
import org.apache.ibatis.annotations.Mapper;@Mapper
public interface Datasource1UserMapper {public void saveUser(User user);}
Datasource2UserMapper.java
package com.hh.springboottest.mapper.datasource2;import com.hh.springboottest.myController.User;
import org.apache.ibatis.annotations.Mapper;@Mapper
public interface Datasource2UserMapper {public void saveUser(User user);
}
6.3 Mapper.xml实现
resources/mapper/datasource1/Datasource1UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hh.springboottest.mapper.datasource1.Datasource1UserMapper"><!-- public void saveUser(User user); --><insert id="saveUser" parameterType="com.hh.springboottest.myController.User">INSERT INTO user(id, name) VALUES(#{id}, #{name})</insert></mapper>
resources/mapper/datasource2/Datasource2UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hh.springboottest.mapper.datasource2.Datasource2UserMapper"><!-- public void saveUser(User user); --><insert id="saveUser" parameterType="com.hh.springboottest.myController.User">INSERT INTO user(id, name) VALUES(#{id}, #{name})</insert></mapper>
6.4 Service实现
Service接口实现
package com.hh.springboottest.service;import com.hh.springboottest.myController.User;public interface UserService {// 向datasource1的数据库插入数据public void datasource1SaveUser(User user);// 向datasource2的数据库插入数据public void datasource2SaveUser(User user);// 用于调用datasource1SaveUser和datasource2SaveUserpublic void saveMultiUser();// 嵌套在saveMultiUser里面进行调用public void saveMultiUserInner();
}
ServiceImpl实现类
说明:
- 编程式事务:通过两层lambda表达式嵌套,实现两个数据源的统一事务管理,对异常进行捕获,然后手动进行回滚
- 声明式事务:通过两层事务注解方法进行嵌套,实现两个数据源的统一事务管理。不能进行异常捕获,自动进行回滚,然后抛出异常
package com.hh.springboottest.service.impl;import com.hh.springboottest.mapper.datasource1.Datasource1UserMapper;
import com.hh.springboottest.mapper.datasource2.Datasource2UserMapper;
import com.hh.springboottest.myController.User;
import com.hh.springboottest.service.UserService;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;@Service
public class UserServiceImpl implements UserService {@AutowiredDatasource1UserMapper datasource1UserMapper;@AutowiredDatasource2UserMapper datasource2UserMapper;@AutowiredTransactionTemplate datasource1TransactionTemplate;@AutowiredTransactionTemplate datasource2TransactionTemplate;/*** @Description: 向datasource1的数据库插入数据** @Params: [user]* @Return: void*/@Overridepublic void datasource1SaveUser(User user) {datasource1UserMapper.saveUser(user);}/*** @Description: 向datasource2的数据库插入数据** @Params: [user]* @Return: void*/@Overridepublic void datasource2SaveUser(User user) {datasource2UserMapper.saveUser(user);}/*** @Description: 用于调用datasource1SaveUser和datasource2SaveUser * * 编程式事务方式。被0除,回滚所以插入的数据** @Params: []* @Return: void*/
// public void saveMultiUser() {
// datasource1TransactionTemplate.execute((datasource1Status)->{
// datasource2TransactionTemplate.execute((datasource2Status)->{
// try {
// datasource1SaveUser(new User(1L, "read_name1"));
// datasource2SaveUser(new User(1L, "write_name1"));
// datasource1SaveUser(new User(2L, "read_name2"));
// datasource2SaveUser(new User(2L, "write_name2"));
// Integer d = 1 / 0;
// } catch (Exception e) {
// e.printStackTrace();
// datasource1Status.setRollbackOnly();
// datasource2Status.setRollbackOnly();
// return false;
// }
// return true;
// });
// return true;
// });
// }/*** @Description: 用于调用datasource1SaveUser和datasource2SaveUser** 声明式事务方式。被0除,回滚所以插入的数据** @Params: []* @Return: void*/@Transactional(transactionManager = "datasource1TransactionManager")public void saveMultiUser() {UserService currentUserService = (UserService) AopContext.currentProxy();currentUserService.saveMultiUserInner();}/*** @Description: 嵌套在saveMultiUser里面进行调用 * * @Params: []* @Return: void*/@Transactional(transactionManager = "datasource2TransactionManager")public void saveMultiUserInner() {datasource1SaveUser(new User(1L, "read_name1"));datasource2SaveUser(new User(1L, "write_name1"));datasource1SaveUser(new User(2L, "read_name2"));datasource2SaveUser(new User(2L, "write_name2"));Integer myDiv = 1 / 0;}}
6.5 测试
说明:
- EnableTransactionManagement:开启事务控制功能
- EnableAspectJAutoProxy注解表示开启AOP功能,exposeProxy为true表示让proxy被AOP框架当做ThreadLocal进行暴露,以便通过org.springframework.aop.framework.AopContext类进行获取,默认关闭,不能通过本类调用本类
package com.hh.springboottest;import com.hh.springboottest.myController.User;
import com.hh.springboottest.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.transaction.annotation.EnableTransactionManagement;@Slf4j
@SpringBootTest
@EnableTransactionManagement
@EnableAspectJAutoProxy(exposeProxy = true)
public class MyApplicationTest {@AutowiredUserService userService;@Testpublic void saveMultiUserTest() {userService.saveMultiUser();}}
运行程序,结果如下:
2022-12-13 22:04:29.857 INFO 21468 --- [ main] com.alibaba.druid.pool.DruidDataSource : {dataSource-2} initedjava.lang.ArithmeticException: / by zeroat com.hh.springboottest.service.impl.UserServiceImpl.saveMultiUserInner(UserServiceImpl.java:117)
......省略部分......
数据库未插入一条数据,因为回滚了
7. AOP + 自定义注解实现事务(未测试)
这里只做了一部分的记录,并未运行进行测试
自定注解,然后通过多线程的方式执行多个事务方法
package com.hh.springboottest.aop;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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component
@Aspect
public class MultiTransactionAop {// ComboTransaction类需要自己定义,还未定义private final ComboTransaction comboTransaction;@Autowiredpublic MultiTransactionAop(ComboTransaction comboTransaction) {this.comboTransaction = comboTransaction}@Pointcut("within(com.hh.springboottest.service.impl.*)")public void pointCut() {}@Around("pointCut() && @annotation(multiTransactional)")public Object inMultiTransactions(ProceedingJoinPoint pjp, MultiTransactional multiTransactional) {return comboTransaction.inCombinedTx(() -> {try {return pjp.proceed(); // 执行目标方法} catch (Throwable throwable) {if (throwable instanceof RuntimeException) {throw (RuntimeException) throwable;}throw new RuntimeException(throwable);}}, multiTransactional.value());}}