万字详谈SpringBoot多数据源以及事务处理
背景
在高并发的项目中,单数据库已无法承载大数据量的访问,因此需要使用多个数据库进行对数据的读写分离,此外就是在微服化的今天,我们在项目中可能采用各种不同存储,因此也需要连接不同的数据库,居于这样的背景,这里简单分享实现的思路以及实现方案。 如何实现
多数据源实现思路有两种,一种是通过配置多个SqlSessionFactory实现多数据源;
另外一种是通过Spring提供的AbstractRoutingDataSource抽象了一个DynamicDataSource实现动态切换数据源;
实现方案准备
采用Spring Boot2.7.8框架,数据库Mysql,ORM框架采用Mybatis,整个Maven依赖如下: 8 8 2.7.8 5.1.46 2.0.0 3.5.1 org.springframework.boot spring-boot-dependencies ${spring-boot.version} pom import mysql mysql-connector-java ${mysql-connector-java.version} org.mybatis mybatis ${mybatis.version} org.mybatis.spring.boot mybatis-spring-boot-starter ${mybatis-spring-boot-starter.version} 指定数据源操作指定目录XML文件
该种方式需要操作的数据库的Mapper层和Dao层分别建立一个文件夹,分包放置,整体项目结构如下图:
Maven依赖如下: org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test com.zaxxer HikariCP 4.0.3 mysql mysql-connector-java mysql mysql-connector-java org.mybatis mybatis org.mybatis.spring.boot mybatis-spring-boot-starter junit junit test Yaml文件spring: datasource: user: jdbc-url: jdbc:mysql://127.0.0.1:3306/study_user?useSSL=false&useUnicode=true&characterEncoding=UTF-8 username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver type: com.zaxxer.hikari.HikariDataSource #hikari连接池配置 hikari: #pool name pool-name: user #最小空闲连接数 minimum-idle: 5 #最大连接池 maximum-pool-size: 20 #链接超时时间 3秒 connection-timeout: 3000 # 连接测试query connection-test-query: SELECT 1 soul: jdbc-url: jdbc:mysql://127.0.0.1:3306/soul?useSSL=false&useUnicode=true&characterEncoding=UTF-8 username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver type: com.zaxxer.hikari.HikariDataSource #hikari连接池配置 hikari: #pool name pool-name: soul #最小空闲连接数 minimum-idle: 5 #最大连接池 maximum-pool-size: 20 #链接超时时间 3秒 connection-timeout: 3000 # 连接测试query connection-test-query: SELECT 1 不同库的Mapper指定不同的SqlSessionFactory
针对不同的库分别放置对用不同的SqlSessionFactory @Configuration @MapperScan(basePackages = "org.datasource.demo1.usermapper", sqlSessionFactoryRef = "userSqlSessionFactory") public class UserDataSourceConfiguration { public static final String MAPPER_LOCATION = "classpath:usermapper/*.xml"; @Primary @Bean("userDataSource") @ConfigurationProperties(prefix = "spring.datasource.user") public DataSource userDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "userTransactionManager") @Primary public PlatformTransactionManager userTransactionManager(@Qualifier("userDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Primary @Bean(name = "userSqlSessionFactory") public SqlSessionFactory userSqlSessionFactory(@Qualifier("userDataSource") DataSource dataSource) throws Exception { final SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean(); sessionFactoryBean.setDataSource(dataSource); sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(UserDataSourceConfiguration.MAPPER_LOCATION)); return sessionFactoryBean.getObject(); } } @Configuration @MapperScan(basePackages = "org.datasource.demo1.soulmapper", sqlSessionFactoryRef = "soulSqlSessionFactory") public class SoulDataSourceConfiguration { public static final String MAPPER_LOCATION = "classpath:soulmapper/*.xml"; @Bean("soulDataSource") @ConfigurationProperties(prefix = "spring.datasource.soul") public DataSource soulDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "soulTransactionManager") public PlatformTransactionManager soulTransactionManager(@Qualifier("soulDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean(name = "soulSqlSessionFactory") public SqlSessionFactory soulSqlSessionFactory(@Qualifier("soulDataSource") DataSource dataSource) throws Exception { final SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean(); sessionFactoryBean.setDataSource(dataSource); sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(SoulDataSourceConfiguration.MAPPER_LOCATION)); return sessionFactoryBean.getObject(); } } 使用@Service public class AppAuthService { @Autowired private AppAuthMapper appAuthMapper; @Transactional(rollbackFor = Exception.class) public int getCount() { int a = appAuthMapper.listCount(); int b = 1 / 0; return a; } } @SpringBootTest @RunWith(SpringRunner.class) public class TestDataSource { @Autowired private AppAuthService appAuthService; @Autowired private SysUserService sysUserService; @Test public void test_dataSource1(){ int b=sysUserService.getCount(); int a=appAuthService.getCount(); } } 总结
此种方式使用起来分层明确,不存在任何冗余代码,不足地方就是每个库都需要对应一个配置类,该配置类中实现方式都基本类似,该种解决方案每个配置类中都存在事务管理器,因此不需要单独再去额外的关注。 AOP+自定义注解
关于采用Spring AOP方式实现原理就是把多个数据源存储在一个 Map中,当需要使用某个数据源时,从 Map中获取此数据源进行处理。
AbstractRoutingDataSource
在Spring中提供了AbstractRoutingDataSource来实现此功能,继承AbstractRoutingDataSource类并覆写其determineCurrentLookupKey()方法就可以完成数据源切换,该方法只需要返回数据源key即可,也就是存放数据源的Map的key,接下来我们来看一下AbstractRoutingDataSource整体的继承结构,看他是如何做到的。
在整体的继承结构上我们会发现AbstractRoutingDataSource最终是继承于DataSource,因此当我们继承AbstractRoutingDataSource是我们自身也是一个数据源,对于数据源必然有连接数据库的动作,如下代码: public Connection getConnection() throws SQLException { return this.determineTargetDataSource().getConnection(); } public Connection getConnection(String username, String password) throws SQLException { return this.determineTargetDataSource().getConnection(username, password); }
只是AbstractRoutingDataSource的getConnection()方法里实际是调用determineTargetDataSource()返回的数据源的getConnection()方法。 protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); Object lookupKey = this.determineCurrentLookupKey(); DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey); if (dataSource == null && (this.lenientFallback || lookupKey == null)) { dataSource = this.resolvedDefaultDataSource; } if (dataSource == null) { throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); } else { return dataSource; } }
该方法通过determineCurrentLookupKey()方法获取一个key,通过key从resolvedDataSources中获取数据源DataSource对象。determineCurrentLookupKey()是个抽象方法,需要继承AbstractRoutingDataSource的类实现;而resolvedDataSources是一个Map