文章目录
- 学习链接
- 一、多数据源实现方法
- 1. spring整合mybatis的核心思路
- 整合原理
- 2. spring整合mybatis最简单的整合步骤
- 简单介绍
- 1. 导入依赖
- 2. 准备基础类
- User
- UserMapper
- userMapper.xml
- MybatisConfig配置类
- 3. 测试
- TestMybatisSpring
- 3. mybatis插件实现切换数据源
- 简单介绍
- 1. DsPlugin插件
- 2. CustomizeRoutingDataSource
- 2. 修改MybatisConfig
- 3.测试
- TestMybatisSpring
- 4. Aop + 自定义注解实现数据源切换
- 简单介绍
- 1. 准备基础类
- Person
- PersonMapper
- CustomizeRoutingDataSource
- 2. 修改MyBatisConfig
- 3. 测试
- TestMybatisSpring
- 5. spring整合多份mybatis配置
- 简单介绍
- 1. MybatisConfig
- MybatisConfig1
- MybatisConfig2
- 2. 测试
- TestMybatisSpring
- 二、多数据源事务控制
- spring事务在多数据源情况下的问题
- 示例
- MybatisConfig1 mybatis+spring整合user库
- MybatisConfig2类 mybatis+spring整合user2库
- Config类 配置事务管理器
- UserService
- 测试
- 使用编程式事务解决
- 修改UserService
- 测试
- 使用声明式事务解决
- 修改UserService
- 测试
一、多数据源实现方法
1. spring整合mybatis的核心思路
整合原理
@MapperScan注解 -> 引入了MapperScannerRegistrar注册器 -> 该注册器向容器中MapperScannerConfigurer这个configurer是个BeanDefinitionRegistryPostProcessor,它在重写的postProcessBeanDefinitionRegistry方法中使用了ClassPathMapperScanner来扫描指定的包中的mapper接口 -> ClassPathMapperScanner(如果未指定sqlSessionTemplateBeanName,也没有指定sqlSessionFactoryBeanName,将会设置按类型自动注入SqlSessionFactory和SqlSessionTemplate(实现了SqlSession接口,委托给SqlSessionInterceptor,即每次调用SqlSession中的任何方法都会走SqlSessionInterceptor#invoke方法,这是为了控制事务),仅须其中一个即可) -> 扫包中的每一个beanDefinition都会设置beanClass为MapperFactoryBean,而MapperFactoryBean#getObject方法就会获取sqlSessionTemplate(它实现了SqlSession接口),并调用sqlSessionTemplate.getMapper(接口类)方法,来获取动态代理 -> 而每次使用动态代理的方法都会调用sqlSessionTemplate -> 而sqlSessionTemplate则也会委托给SqlSessionInterceptor-> 此时去从SqlSessionFactory获取sqlSession
源码详细流程,可参考:
zzhua-mybatis框架全面分析mybatis-spring源码详尽分析&Spring扫包
2. spring整合mybatis最简单的整合步骤
简单介绍
整合的原理,在上面已经详细分析了,这个仅仅是简单的spring 整合 mybatis 的一个示例
1. 导入依赖
<?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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zzhua</groupId>
<artifactId>mybatis-spring-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.17</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
2. 准备基础类
User
@Data
public class User {
private String userAccount;
}
UserMapper
public interface UserMapper {
List<User> findAll();
@Select("select * from `user` u where u.user_account = #{userAccount}")
User findOne(String userAccount);
}
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.zzhua.mapper.UserMapper">
<select id="findAll" resultType="user">
select * from `user`
</select>
</mapper>
MybatisConfig配置类
package com.zzhua.config;
import org.apache.ibatis.session.ExecutorType;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import java.io.IOException;
@Configuration
@MapperScan(basePackages = "com.zzhua.mapper")
public class MybatisConfig {
@Bean // 如果不配置该bean,将会抛出Caused by: java.lang.IllegalArgumentException: Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required异常
public SqlSessionFactoryBean sqlSessionFactoryBean() throws IOException { // 代码写法可参考SqlSessionFactoryBean#afterPropertiesSet
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
// 设置数据源
DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
driverManagerDataSource.setUrl("jdbc:mysql://127.0.0.1/user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true");
driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
driverManagerDataSource.setUsername("root");
driverManagerDataSource.setPassword("root");
sqlSessionFactoryBean.setDataSource(driverManagerDataSource);
// 自定义Configuration
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
configuration.getTypeAliasRegistry().registerAliases("com.zzhua.pojo");
sqlSessionFactoryBean.setConfiguration(configuration);
// 指定mapper.xml文件
PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
sqlSessionFactoryBean.setMapperLocations(resourcePatternResolver.getResources("classpath:com/zzhua/mapper/**.xml"));
return sqlSessionFactoryBean; // # 之后会调用SqlSessionFactoryBean#afterPropertiesSet()方法
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);
}
}
3. 测试
TestMybatisSpring
public class TestMybatisSpring {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MybatisConfig.class);
UserMapper userMapper = context.getBean(UserMapper.class);
List<User> userList = userMapper.findAll();
System.out.println(userList);
User user = userMapper.findOne("zzhua");
System.out.println(user);
}
}
3. mybatis插件实现切换数据源
简单介绍
读写分离的数据源:如果是mybatis可以结合插件实现读写分离动态切换数据源。
在mybatis提供的插件机制
中,可以拦截到当前执行sql的类型(是查询 或者 修改),从而实现读写分离
,查询使用指定的数据库,而涉及到修改使用另外的数据库。
1. DsPlugin插件
// Interceptor可拦截mybatis的四大对象中的方法
// 四大对象包括: Executor、StatementHandler、ParameterHandler、ResultSetHandler
@Intercepts({
@Signature(
type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class}
),
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
})
public class DsPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = ((MappedStatement) args[0]);
// 是查询操作的话,就走user这个数据库
if (SqlCommandType.SELECT.equals(ms.getSqlCommandType())) {
CustomizeRoutingDataSource.setDsKey("user");
} else {
// 是增删改操作的话,就走user2这个数据库
CustomizeRoutingDataSource.setDsKey("user2");
}
Object result = invocation.proceed();
CustomizeRoutingDataSource.clearDsKey();
return result;
}
}
2. CustomizeRoutingDataSource
public class CustomizeRoutingDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> DS_KEY = new ThreadLocal<>();
@Override
protected Object determineCurrentLookupKey() {
return DS_KEY.get();
}
public static void setDsKey(String dsKey) {
DS_KEY.set(dsKey);
}
public static void clearDsKey() {
DS_KEY.remove();
}
}
2. 修改MybatisConfig
@Configuration
@MapperScan(basePackages = "com.zzhua.dao.mapper", sqlSessionFactoryRef = "sqlSessionFactoryBean")
public class MybatisConfig {
@Bean
public DataSource userDataSource() {
DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
driverManagerDataSource.setUrl("jdbc:mysql://127.0.0.1/user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true");
driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
driverManagerDataSource.setUsername("root");
driverManagerDataSource.setPassword("root");
return driverManagerDataSource;
}
@Bean
public DataSource user2DataSource() {
DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
driverManagerDataSource.setUrl("jdbc:mysql://127.0.0.1/user2?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true");
driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
driverManagerDataSource.setUsername("root");
driverManagerDataSource.setPassword("root");
return driverManagerDataSource;
}
@Primary
@Bean
public DataSource primaryDataSource() {
CustomizeRoutingDataSource routingDataSource = new CustomizeRoutingDataSource();
// 指定默认数据源
routingDataSource.setDefaultTargetDataSource(userDataSource());
// 指定 标识->数据源 的map
HashMap<Object, Object> dsMap = new HashMap<>();
dsMap.put("user", userDataSource());
dsMap.put("user2", user2DataSource());
routingDataSource.setTargetDataSources(dsMap);
return routingDataSource;
}
@Bean // 如果不配置该bean,将会抛出Caused by: java.lang.IllegalArgumentException: Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required异常
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource primaryDataSource) throws IOException { // 代码写法可参考SqlSessionFactoryBean#afterPropertiesSet
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
// 设置数据源
sqlSessionFactoryBean.setDataSource(primaryDataSource);
// 自定义Configuration
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
configuration.getTypeAliasRegistry().registerAliases("com.zzhua.pojo");
// 添加插件
configuration.addInterceptor(new DsPlugin());
sqlSessionFactoryBean.setConfiguration(configuration);
// 指定mapper.xml文件
PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
sqlSessionFactoryBean.setMapperLocations(resourcePatternResolver.getResources("classpath:com/zzhua/mapper/**.xml"));
return sqlSessionFactoryBean; // # 之后会调用SqlSessionFactoryBean#afterPropertiesSet()方法
}
}
3.测试
TestMybatisSpring
public class TestMybatisSpring {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MybatisConfig.class);
PersonMapper personMapper = context.getBean(PersonMapper.class);
// insert属于修改操作,将会走user2数据库
personMapper.addPerson("zzhua11");
personMapper.addPerson("zj11");
// select属于查询操作,将会走user数据库
List<Person> persons = personMapper.getPersons();
System.out.println(persons);
}
}
4. Aop + 自定义注解实现数据源切换
简单介绍
不同业务的数据源:一般利用AOP,结合自定义注解动态切换数据源
使用AbstractRoutingDataSource作为数据源,暴露给spring使用,而当spring通过数据源 获取数据库连接时,再根据路由标识 确定 此时需要的数据源,从而实现数据源切换。
在user库中新建一张person表
1. 准备基础类
Person
@Data
public class Person {
private String name;
}
PersonMapper
public interface PersonMapper {
@Insert("insert into person values(#{name} )")
void addPerson(String name);
@Select("select * from person")
List<Person> getPersons();
}
CustomizeRoutingDataSource
public class CustomizeRoutingDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> DS_KEY = new ThreadLocal<>();
@Override
protected Object determineCurrentLookupKey() {
return DS_KEY.get();
}
public static void setDsKey(String dsKey) {
DS_KEY.set(dsKey);
}
public static void clearDsKey() {
DS_KEY.remove();
}
}
2. 修改MyBatisConfig
@Configuration
@MapperScan(basePackages = "com.zzhua.dao.mapper", sqlSessionFactoryRef = "sqlSessionFactoryBean")
public class MybatisConfig {
@Bean
public DataSource userDataSource() {
DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
driverManagerDataSource.setUrl("jdbc:mysql://127.0.0.1/user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true");
driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
driverManagerDataSource.setUsername("root");
driverManagerDataSource.setPassword("root");
return driverManagerDataSource;
}
@Bean
public DataSource user2DataSource() {
DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
driverManagerDataSource.setUrl("jdbc:mysql://127.0.0.1/user2?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true");
driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
driverManagerDataSource.setUsername("root");
driverManagerDataSource.setPassword("root");
return driverManagerDataSource;
}
@Primary
@Bean
public DataSource primaryDataSource() {
CustomizeRoutingDataSource routingDataSource = new CustomizeRoutingDataSource();
// 指定默认数据源
routingDataSource.setDefaultTargetDataSource(userDataSource());
// 指定 标识->数据源 的map
HashMap<Object, Object> dsMap = new HashMap<>();
dsMap.put("user", userDataSource());
dsMap.put("user2", user2DataSource());
routingDataSource.setTargetDataSources(dsMap);
return routingDataSource;
}
@Bean // 如果不配置该bean,将会抛出Caused by: java.lang.IllegalArgumentException: Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required异常
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource primaryDataSource) throws IOException { // 代码写法可参考SqlSessionFactoryBean#afterPropertiesSet
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
// 设置数据源
sqlSessionFactoryBean.setDataSource(primaryDataSource);
// 自定义Configuration
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
configuration.getTypeAliasRegistry().registerAliases("com.zzhua.pojo");
sqlSessionFactoryBean.setConfiguration(configuration);
// 指定mapper.xml文件
PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
sqlSessionFactoryBean.setMapperLocations(resourcePatternResolver.getResources("classpath:com/zzhua/mapper/**.xml"));
return sqlSessionFactoryBean; // # 之后会调用SqlSessionFactoryBean#afterPropertiesSet()方法
}
}
3. 测试
TestMybatisSpring
public class TestMybatisSpring {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MybatisConfig.class);
PersonMapper personMapper = context.getBean(PersonMapper.class);
// 可使用注解 + AOP的方式简化代码, 在执行数据库操作之前设置所要操作的数据源标识
CustomizeRoutingDataSource.setDsKey("user");
personMapper.addPerson("zzhua");
personMapper.addPerson("zj");
List<Person> persons = personMapper.getPersons();
System.out.println(persons);
CustomizeRoutingDataSource.clearDsKey();
CustomizeRoutingDataSource.setDsKey("user2");
personMapper.addPerson("zzhua2");
personMapper.addPerson("zj2");
List<Person> persons2 = personMapper.getPersons();
System.out.println(persons2);
CustomizeRoutingDataSource.clearDsKey();
}
}
5. spring整合多份mybatis配置
简单介绍
假设在一个项目中,我们需要操作多个数据源(也就是多个数据库),并且我们都想使用mybatis来操作它们。
现在演示示例:我们有2个数据库user库 和 user2库 这2个数据库,然后,我们分别使用mybatis来做一个简单的查询示例。
创建2个数据库,一个user库(里面有一张user表)和user2库(里面有一张user2表)。现在需要使用mybatis来操作这2个库中的表。
1. MybatisConfig
MybatisConfig1
这个负责扫描com.zzhua.dao.mapper这个包,数据源指定的使用的user库
@Configuration
@MapperScan(basePackages = "com.zzhua.dao.mapper", sqlSessionFactoryRef = "sqlSessionFactoryBean")
public class MybatisConfig1 {
@Bean // 如果不配置该bean,将会抛出Caused by: java.lang.IllegalArgumentException: Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required异常
public SqlSessionFactoryBean sqlSessionFactoryBean() throws IOException { // 代码写法可参考SqlSessionFactoryBean#afterPropertiesSet
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
// 设置数据源
DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
driverManagerDataSource.setUrl("jdbc:mysql://127.0.0.1/user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true");
driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
driverManagerDataSource.setUsername("root");
driverManagerDataSource.setPassword("root");
sqlSessionFactoryBean.setDataSource(driverManagerDataSource);
// 自定义Configuration
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
configuration.getTypeAliasRegistry().registerAliases("com.zzhua.pojo");
sqlSessionFactoryBean.setConfiguration(configuration);
// 指定mapper.xml文件
PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
sqlSessionFactoryBean.setMapperLocations(resourcePatternResolver.getResources("classpath:com/zzhua/mapper/**.xml"));
return sqlSessionFactoryBean; // # 之后会调用SqlSessionFactoryBean#afterPropertiesSet()方法
}
}
MybatisConfig2
这个负责扫描com.zzhua.dao.mapper2这个包,数据源指定的使用的user2库
@Configuration
@MapperScan(basePackages = "com.zzhua.dao.mapper2", sqlSessionFactoryRef = "sqlSessionFactoryBean2")
public static class MybatisConfig2 {
@Bean
// 如果不配置该bean,将会抛出Caused by: java.lang.IllegalArgumentException: Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required异常
public SqlSessionFactoryBean sqlSessionFactoryBean2() throws IOException { // 代码写法可参考SqlSessionFactoryBean#afterPropertiesSet
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
// 设置数据源
DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
driverManagerDataSource.setUrl("jdbc:mysql://127.0.0.1/user2?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true");
driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
driverManagerDataSource.setUsername("root");
driverManagerDataSource.setPassword("root");
sqlSessionFactoryBean.setDataSource(driverManagerDataSource);
// 自定义Configuration
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
configuration.getTypeAliasRegistry().registerAliases("com.zzhua.pojo");
sqlSessionFactoryBean.setConfiguration(configuration);
// 指定mapper.xml文件
PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
sqlSessionFactoryBean.setMapperLocations(resourcePatternResolver.getResources("classpath:com/zzhua/mapper2/**.xml"));
return sqlSessionFactoryBean; // # 之后会调用SqlSessionFactoryBean#afterPropertiesSet()方法
}
}
2. 测试
TestMybatisSpring
下面的测试,可以成功的分别查询到 user库 和 user2库 不同数据库中的数据,也即实现了使用mybatis操作多个数据源。
public class TestMybatisSpring {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MybatisConfig.class);
UserMapper userMapper = context.getBean(UserMapper.class);
List<User> userList = userMapper.findAll();
System.out.println(userList);
User user = userMapper.findOne("zzhua");
System.out.println(user);
System.out.println("--------------");
UserMapper2 userMapper2 = context.getBean(UserMapper2.class);
List<User2> userList2 = userMapper2.findAll2();
System.out.println(userList2);
User2 user2 = userMapper2.findOne2("zzhua2");
System.out.println(user2);
}
}
二、多数据源事务控制
在多数据源下,由于涉及到数据库的多个读写。一旦发生异常就可能会导致数据不一致的情况, 在这种情况希望使用事务进行回退。
Spring的声明式事务在一次请求线程中只能使用一个数据源进行控制
,可参考:spring事务源码学习。
但是是对于多源数据库:
- 单一事务管理器(TransactionManager)无法切换数据源,需要配置多个TransactionManager。
- @Transactionnal是无法管理多个数据源的。
如果想真正实现多源数据库事务控制,肯定是需要分布式事务
。这里讲解多源数据库事务控制的一种变通方式。
spring事务在多数据源情况下的问题
示例
以上面spring整合多分mybatis配置为基础
MybatisConfig1 mybatis+spring整合user库
@Configuration
@MapperScan(basePackages = "com.zzhua.dao.mapper", sqlSessionFactoryRef = "sqlSessionFactoryBean")
public class MybatisConfig1 {
@Bean
public DataSource dataSource1() {
// 设置数据源
DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
driverManagerDataSource.setUrl("jdbc:mysql://127.0.0.1/user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true");
driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
driverManagerDataSource.setUsername("root");
driverManagerDataSource.setPassword("root");
return driverManagerDataSource;
}
@Bean
// 如果不配置该bean,将会抛出Caused by: java.lang.IllegalArgumentException: Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required异常
public SqlSessionFactoryBean sqlSessionFactoryBean() throws IOException { // 代码写法可参考SqlSessionFactoryBean#afterPropertiesSet
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource1());
// 自定义Configuration
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
configuration.getTypeAliasRegistry().registerAliases("com.zzhua.pojo");
sqlSessionFactoryBean.setConfiguration(configuration);
// 指定mapper.xml文件
PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
sqlSessionFactoryBean.setMapperLocations(resourcePatternResolver.getResources("classpath:com/zzhua/mapper/**.xml"));
return sqlSessionFactoryBean; // # 之后会调用SqlSessionFactoryBean#afterPropertiesSet()方法
}
}
MybatisConfig2类 mybatis+spring整合user2库
@Configuration
@MapperScan(basePackages = "com.zzhua.dao.mapper2", sqlSessionFactoryRef = "sqlSessionFactoryBean2")
public class MybatisConfig2 {
@Bean
public DataSource dataSource2() {
// 设置数据源
DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
driverManagerDataSource.setUrl("jdbc:mysql://127.0.0.1/user2?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true");
driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
driverManagerDataSource.setUsername("root");
driverManagerDataSource.setPassword("root");
return driverManagerDataSource;
}
@Bean
// 如果不配置该bean,将会抛出Caused by: java.lang.IllegalArgumentException: Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required异常
public SqlSessionFactoryBean sqlSessionFactoryBean2(DataSource dataSource2) throws IOException { // 代码写法可参考SqlSessionFactoryBean#afterPropertiesSet
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource2());
// 自定义Configuration
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
configuration.getTypeAliasRegistry().registerAliases("com.zzhua.pojo");
sqlSessionFactoryBean.setConfiguration(configuration);
// 指定mapper.xml文件
PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
sqlSessionFactoryBean.setMapperLocations(resourcePatternResolver.getResources("classpath:com/zzhua/mapper2/**.xml"));
return sqlSessionFactoryBean; // # 之后会调用SqlSessionFactoryBean#afterPropertiesSet()方法
}
}
Config类 配置事务管理器
@Import({MybatisConfig1.class,MybatisConfig2.class, UserService.class})
public class Config {
@Autowired
private DataSource dataSource1;
@Autowired
private DataSource dataSource2;
@Bean
public PlatformTransactionManager transactionManager1(DataSource dataSource1) {
return new DataSourceTransactionManager(dataSource1);
}
@Bean
public PlatformTransactionManager transactionManager2(DataSource dataSource2) {
return new DataSourceTransactionManager(dataSource2);
}
}
UserService
@Service
@EnableTransactionManagement
public class UserService {
@Autowired
private UserMapper userMapper; // 数据库user
@Autowired
private UserMapper2 userMapper2; // 数据库user2
// 指定此事务注解所要使用的事务管理器(必须要配置了事务管理器,才可以使用这个注解)
// springboot下在检测到项目中只有一个数据源的情况下,会通过自动配置原理,
// 将此数据源注入,配置到一个DataSourceTransactionManager事务管理器中
@Transactional(transactionManager = "transactionManager1")
public void addUserTx() {
User user1 = new User();
user1.setUserAccount("user1");
userMapper.addUser(user1);
User user2 = new User();
user2.setUserAccount("user2");
userMapper2.addUser(user2);
int i = 1 / 0;
}
}
测试
发现只有user库回滚了,user2库仍然会添加1条数据,这样就说明事务控制是不彻底的,需要另外的方式去实现。
public class TestMybatisSpring {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
UserService userService = context.getBean(UserService.class);
userService.addUserTx();
System.out.println();
}
}
使用编程式事务解决
我们只需要在上面的基础上修改UserService即可
修改UserService
TransactionTemplate是spring提供的,它简化了对事务管理器的使用,可以将它们定义成bean,这里仅作演示,所以写在方法里了
@Service
@EnableTransactionManagement
public class UserService {
@Autowired
private UserMapper userMapper; // 数据库user
@Autowired
private UserMapper2 userMapper2; // 数据库user2
@Autowired
private PlatformTransactionManager transactionManager1;
@Autowired
private PlatformTransactionManager transactionManager2;
public void addUserTx() {
TransactionTemplate transactionTemplate1 = new TransactionTemplate(transactionManager1);
TransactionTemplate transactionTemplate2 = new TransactionTemplate(transactionManager2);
transactionTemplate1.execute(status1 -> {
transactionTemplate2.execute(status2 -> {
try {
User user1 = new User();
user1.setUserAccount("user1");
userMapper.addUser(user1);
User user2 = new User();
user2.setUserAccount("user2");
userMapper2.addUser(user2);
int i = 1 / 0;
return null;
} catch (Exception e) {
status1.setRollbackOnly();
status2.setRollbackOnly();
return null;
}
});
return null;
});
}
}
测试
没有任何改动,user库和user2库在发生异常时,都正常回滚了
public class TestMybatisSpring {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
UserService userService = context.getBean(UserService.class);
userService.addUserTx();
System.out.println();
}
}
使用声明式事务解决
修改UserService
- 将原来的addUserTx包到addUser方法中调用,在addUser上添加transactionManager1作为事务管理器
- 将当前的代理对象暴露到当前本地线程,需要设置exposeProxy为true,并且这里要使用代理对象这样去调用的原因是:aop责任链调用的末端是真实的对象,如果直接去调用(t例如:在addUser方法中,直接this.addUserTx())的话,就只会回滚transactionManager1的数据
- 当同时使用@EnableAspectJAutoProxy(添加的是:
AnnotationAwareAspectJAutoProxyCreator
)和@EnableTransactionManagement(添加的是:InfrastructureAdvisorAutoProxyCreator
)这2个注解时,spring会比较这2个代理创建器的优先级,索引越大的,越优先,默认的排序是:【InfrastructureAdvisorAutoProxyCreator、AspectJAwareAdvisorAutoProxyCreator、AnnotationAwareAspectJAutoProxyCreator】,所以最终只会添加1个自动代理创建器,就是AnnotationAwareAspectJAutoProxyCreator
,因此,exposeProxy的属性需要通过这个自动代理创建器的注解中设置。
@Service
@EnableAspectJAutoProxy(exposeProxy = true)
@EnableTransactionManagement
public class UserService {
@Autowired
private UserMapper userMapper; // 数据库user
@Autowired
private UserMapper2 userMapper2; // 数据库user2
@Autowired
private PlatformTransactionManager transactionManager1;
@Autowired
private PlatformTransactionManager transactionManager2;
@Transactional(transactionManager = "transactionManager1")
public void addUser() {
UserService userService = (UserService) AopContext.currentProxy();
userService.addUserTx();
}
@Transactional(transactionManager = "transactionManager2")
public void addUserTx() {
User user1 = new User();
user1.setUserAccount("user1");
userMapper.addUser(user1);
User user2 = new User();
user2.setUserAccount("user2");
userMapper2.addUser(user2);
int i = 1 / 0;
}
}
测试
改为调用addUser方法,当addUserTx发生异常时,user库和user2库都回滚了
public class TestMybatisSpring {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
UserService userService = context.getBean(UserService.class);
userService.addUser();
System.out.println();
}
}