# dynamic-datasource **Repository Path**: sunwul/dynamic-datasource ## Basic Information - **Project Name**: dynamic-datasource - **Description**: 关于动态数据源的不同实现方式, 以及多数据源事务管理相关的实现 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-07-18 - **Last Updated**: 2023-07-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: Java, 读写分离, 数据库主从, 动态数据源, Spring ## README ## 动态数据源、多数据源切换 > 集成Mybatis框架及事务控制 ### 一,多数据源的典型使用场景 #### 1.业务复杂(数据量大) > 数据分布在不同的数据库中,根据业务拆分了数据,应用没有拆分. > > 一个公司多个子项目,不同业务各用各的数据库,涉及数据共享... ![](README.assets/001.png) #### 2.读写分离 > 为了解决数据库的**读**性能瓶颈(读比写性能更高, 写锁会影响读阻塞, 从而影响读的性能) > > 当读与写都放在一个数据库中操作, 在高并发的情况下, 会出现读锁与写锁互斥的情况, 因为写锁具有排他性, 当对一个数据进行写操作时, 会上一把写锁, 如果同时又对此数据进行读操作, 此时读操作的连接会被阻塞, 等待写操作完成, 写锁被释放后, 才会进行读操作.此时就需要对数据库进行读写分离 > > 很多数据库用主从架构, 即 一台主数据库服务器, 对外提供增删改业务, 一台或多台从数据库服务器, 主要进行读的操作. 在读写分离中, 主从库的数据是一致的. 数据更新操作都在主库上完成, 主库将数据变更信息同步给从库, 查询操作交给从库, 分担主库的压力 > > 实现读写分离可以借助中间件(ShardingSphere, mycat, mysql-proxy, TDDL....) , 应用将数据操作请求发送给中间件, 由中间件根据读写请求进行分发, 当没有搭建读写分离的基础设施时, 就需要自行实现 ![](README.assets/002.png) ### 二,如何实现多数据源 #### AbstractRoutingDataSource动态指定数据源 > spring框架中, spring-jdbc模块提供了AbstractRoutingDataSource, 其内部可以包含多个DataSource, 然后在运行时来动态的访问哪个数据库 > > 这种方式访问数据库的架构图如下所示: ![](README.assets/003.png) > 应用直接操作的是AbstractRoutingDataSource的子类, 子类重写特定方法, 装载配置, 设置标识,告诉AbstractRoutingDataSource访问哪个数据库, 然后由AbstractRoutingDataSource从配置的数据源(DataSource1, DataSource2...)中选择一个, 访问对应的数据库 > > 底层原理如下(方式二): ![](README.assets/动态数据源.png) ```java /***** * @author sunwul * @date 2022/2/15 9:53 * @description 数据库操作类型 */ public enum DatabaseOperation { Read(OperationConstant.OPERATION_TYPE_R, "读取"), Write(OperationConstant.OPERATION_TYPE_W, "写入"); private String name; private String desc; DatabaseOperation(String name, String desc) { this.name = name; this.desc = desc; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } } ``` ```java import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; import java.sql.SQLException; /***** * @author sunwul * @date 2022/2/14 19:32 * @description 数据源配置 */ @Configuration public class DatasourceConfig { @Bean("dataSource_write") @ConfigurationProperties(prefix = "spring.datasource.datasource-write") public DataSource dataSource_write() { // 通过绑定全局配置,拿到属性后自动创建一个DruidDataSource return DruidDataSourceBuilder.create().build(); } @Bean("dataSource_read") @ConfigurationProperties(prefix = "spring.datasource.datasource-read") public DataSource dataSource_read() throws SQLException { // 通过绑定全局配置,拿到属性后自动创建一个DruidDataSource return DruidDataSourceBuilder.create().build(); } } ``` ```yaml # 配置文件 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource # druid连接池 druid: initial-size: 1 min-idle: 1 max-active: 20 test-on-borrow: true validation-query: SELECT 1 FROM DUAL # 写库 datasource-write: url: jdbc:mysql://localhost:3306/write?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2b8 username: root password: 123 driver-class-name: com.mysql.cj.jdbc.Driver # 读库 datasource-read: url: jdbc:mysql://localhost:3306/read?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2b8 username: root password: 123 driver-class-name: com.mysql.cj.jdbc.Driver ``` ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Primary; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import org.springframework.stereotype.Component; import sunwul.datasource.emun.DatabaseOperation; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; /***** * @author sunwul * @date 2022/2/21 16:20 * @description 动态数据源 * 继承AbstractRoutingDataSource抽象类 * - 重写determineCurrentLookupKey()方法, 返回当前数据源标识 * - 重写afterPropertiesSet()方法, 在Bean初始化时装载配置[设置数据源列表,默认数据源], 调用父类方法, 使配置生效 * * 此方法继承了Spring框架的抽象类, 只需要重写determineCurrentLookupKey()与afterPropertiesSet()方法 * 其它方法使用Spring的默认实现, 与Spring框架更契合 */ @Component @Primary // 作为主要注入Bean, 当出现相同类型的Bean时,会使用含此注解的Bean public class DynamicDatasource extends AbstractRoutingDataSource { // 当前使用的数据源标识 线程安全(多线程对这个name进行同时读写时,会出现单独或篡改的情况) public static ThreadLocal name = new ThreadLocal<>(); // 写 @Autowired DataSource dataSource_write; // 读 @Autowired DataSource dataSource_read; /** * 返回当前数据源标识 */ @Override protected Object determineCurrentLookupKey() { return name.get(); } /** * 初始化设置 * 1. 设置数据源列表 * 2. 设置默认的数据源 * 3. 调用父类的afterPropertiesSet方法, 使设置生效 * (具体可以查看父类afterPropertiesSet方法,可以看到此方法中判断了数据源列表[必要,否则会异常]和默认数据源[非必要]) */ @Override public void afterPropertiesSet() { // 所有数据源Map 将数据源标识与数据源put进来 Map targetDataSources = new HashMap<>(); targetDataSources.put(DatabaseOperation.Write.getName(), dataSource_write); targetDataSources.put(DatabaseOperation.Read.getName(), dataSource_read); // targetDataSources 初始化所有数据源 super.setTargetDataSources(targetDataSources); // defaultTargetDataSource 设置默认的数据源 super.setDefaultTargetDataSource(dataSource_write); // 调用父类的 afterPropertiesSet 方法, 使设置生效 super.afterPropertiesSet(); } } ``` ##### 动态数据源切换方式 ###### Mybatis插件(适用于读写分离) > mybatis会通过Executor来执行数据库操作, 那么我们只需要拦截Executor中的方法,就可以在mybatis执行操作的时候进行增强 ![image-20220222101802030](README.assets/image-20220222101802030.png) ```java import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.plugin.*; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.springframework.stereotype.Component; import sunwul.datasource.db.DynamicDatasource; import sunwul.datasource.emun.DatabaseOperation; import java.util.Properties; /***** * @author sunwul * @date 2022/2/21 21:21 * @description mybatis插件的动态数据源切换 * * @Intercepts 标明这是一个mybatis插件(固定写法,插件是基于动态代理的) * * @Signature 设置要代理的对象, 方法, 参数列表(参数列表可以参考Executor源码中对应方法的参数列表) * 在mybatis执行操作的时候进行增强, mybatis会通过Executor来执行数据库操作(增删改/查 对应Executor中的update/query方法) * 因此,只需要代理Executor中的update与query方法 */ // @Component // 只需要成为SpringBean, 在MybatisAutoConfiguration实例化时,会扫描到此Bean,自动注入 (这里不使用注解的方式,新建一个配置类创建Bean,便于管理) @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 DynamicDataSourcePlugin implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 获取当前方法的参数列表 Object[] objects = invocation.getArgs(); // 根据Executor类的源码可知, 不管是update还是query方法,参数列表中第一个参数都是MappedStatement // MappedStatement中封装了一个完整的SQL所对应的元素(参考我们平时写的**Mapper.xml, 有命令类型,id,返回结果类型...) MappedStatement ms = (MappedStatement) objects[0]; // 根据SQL类型改变数据源标识,这样在执行数据库命令时,动态数据源就会根据数据源标识切换到对应数据源 if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) { // 读 DynamicDatasource.name.set(DatabaseOperation.Read.getName()); } else { // 写 DynamicDatasource.name.set(DatabaseOperation.Write.getName()); } return invocation.proceed(); } /** * 在新建可代理对象时,会通过此方法来决定是返回目标对象本身还是代理对象 */ @Override public Object plugin(Object target) { if (target instanceof Executor) { // 参考Interceptor类源码 return Plugin.wrap(target, this); } return target; } /** * 此方法会在Configuration初始化当前的Interceptor时执行 */ @Override public void setProperties(Properties properties) { } } ``` ```java import org.apache.ibatis.plugin.Interceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import sunwul.datasource.common.plugin.DynamicDataSourcePlugin; /***** * @author sunwul * @date 2022/2/21 21:53 * @description */ @Configuration public class MybatisConfig { /** * 自定义的Bean会在MybatisAutoConfiguration实例化钱创建完成 * 在MybatisAutoConfiguration实例化时,会自动扫描并注入所用到的所有Bean,此时就注入了我们自定义的Bean */ @Bean public Interceptor dynamicDataSourcePlugin() { return new DynamicDataSourcePlugin(); } } ``` ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import sunwul.datasource.entity.TAdmin; import sunwul.datasource.service.TAdminService; import java.util.List; /***** * @author sunwul * @date 2022/2/21 21:54 * @description */ @RestController public class TestController { @Autowired TAdminService tAdminServiceImpl; @RequestMapping("/sel") public List sel_admin() { // 切换数据源标识 - 不在需要手动切换数据源标识, 使用Mybatis插件代理Executor拦截特定方法来自动切换数据源标识 // DynamicDatasource.name.set(DatabaseOperation.Read.getName()); return tAdminServiceImpl.sel_admin(); } @RequestMapping("/selOne") public TAdmin selOne_admin() { // DynamicDatasource.name.set(DatabaseOperation.Read.getName()); return tAdminServiceImpl.sel_admin().get(0); } @RequestMapping("/add/{name}") public Integer add_admin(@PathVariable String name) { // DynamicDatasource.name.set(DatabaseOperation.Write.getName()); return tAdminServiceImpl.add_admin(name); } } ``` ###### AOP(适用于业务复杂) > 当一个项目中有多个子模块, 但数据库根据业务进行拆分了, 此时,可以使用AOP+自定义注解的方式, 根据注解信息来判断当前数据库操作使用的数据源标识, 提供给动态数据源切换对应数据源 ```java /***** * @author sunwul * @date 2022/2/23 9:16 * @description 操作类型常量 */ public class OperationConstant { // 读 public static final String OPERATION_TYPE_R = "R"; // 写 public static final String OPERATION_TYPE_W = "W"; } ``` ```java import sunwul.datasource.emun.OperationConstant; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /***** * @author sunwul * @date 2022/2/22 17:11 * @description 自定义注解 数据源注解 * * @Target 注解目标,指定注解可以声明在方法与类上 * * @Retention 保留方式,设置为保留策略中的"运行时",这样就可以通过反射来判断注解是否存在 */ @Target({ElementType.METHOD,ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface DataSource { // 用于保存数据源标识, 提供给动态数据源使用 默认写库 String value() default OperationConstant.OPERATION_TYPE_W; } ``` ```java import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; import sunwul.datasource.annotaion.DataSource; import sunwul.datasource.db.DynamicDatasource; /***** * @author sunwul * @date 2022/2/23 9:23 * @description AOP * * 不管声明在方法还是类上, 有一个核心问题, 切换数据源的时机必须要在数据库操作前 * * 由此可知, 只能使用前置通知或者环绕通知 */ @Component @Aspect public class DynamicDataSourceAspect { // 前置 @Before("within(sunwul.datasource.controller.*) && @annotation(dataSource)") public void before(JoinPoint point, DataSource dataSource){ String value = dataSource.value(); DynamicDatasource.name.set(value); System.out.println("注解获取的数据源标识: " + value); } } ``` ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import sunwul.datasource.annotaion.DataSource; import sunwul.datasource.emun.OperationConstant; import sunwul.datasource.entity.TAdmin; import sunwul.datasource.service.TAdminService; import java.util.List; /***** * @author sunwul * @date 2022/2/21 21:58 * @description */ @RestController public class TestController { @Autowired TAdminService tAdminServiceImpl; @DataSource(OperationConstant.OPERATION_TYPE_R) @RequestMapping("/sel") public List sel_admin() { // 切换数据源标识 - 不在需要手动切换数据源, 使用AOP拦截特定注解及属性切换数据源 // DynamicDatasource.name.set(DatabaseOperation.Read.getName()); return tAdminServiceImpl.sel_admin(); } @DataSource(OperationConstant.OPERATION_TYPE_R) @RequestMapping("/selOne") public TAdmin selOne_admin() { // DynamicDatasource.name.set(DatabaseOperation.Read.getName()); return tAdminServiceImpl.sel_admin().get(0); } @DataSource(OperationConstant.OPERATION_TYPE_W) @RequestMapping("/add/{name}") public Integer add_admin(@PathVariable String name) { // DynamicDatasource.name.set(DatabaseOperation.Write.getName()); return tAdminServiceImpl.add_admin(name); } } ``` #### SpringBoot集成多个Mybatis框架 > 通过集成多个Mybatis框架来实现多数据源, 这种方式不涉及到数据源的切换 > > 其原理如下: ![](README.assets/004.png) ```java import org.apache.ibatis.logging.stdout.StdOutImpl; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import javax.sql.DataSource; /***** * @author sunwul * @date 2022/2/28 10:12 * @description 工厂工具类 */ public class FactoryUtil_manyMybatis { /** * 创建Mybatis SqlSessionFactory * * @param dataSource 数据源 * @param mapperPath mapper.xml文件存放路径 * @param isPrintLog 是否开启控制台日志 true-是 false-否 * @return SqlSessionFactory * @throws Exception ex */ public static SqlSessionFactory createFactory(DataSource dataSource, String mapperPath, boolean isPrintLog) throws Exception { final SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); // 指定数据源 sqlSessionFactoryBean.setDataSource(dataSource); System.out.println("指定数据源:" + dataSource.getConnection().getMetaData().getURL()); // 指定Mapper.xml文件 sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources(mapperPath)); System.out.println("扫描并装载Mapper.xml文件: " + mapperPath); // 启用控制台日志 if (isPrintLog) { org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); configuration.setLogImpl(StdOutImpl.class); System.out.println("启用控制台日志..."); sqlSessionFactoryBean.setConfiguration(configuration); } return sqlSessionFactoryBean.getObject(); } } ``` ```java import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; import org.apache.ibatis.session.SqlSessionFactory; 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 sunwul.datasource.common.FactoryUtil_manyMybatis; import javax.sql.DataSource; import java.sql.SQLException; /***** * @author sunwul * @date 2022/2/25 9:22 * @description * * 指定扫描的包 * * 指定使用的sqlSessionFactory */ @Configuration @MapperScan(basePackages = "sunwul.datasource.mapper.read", sqlSessionFactoryRef = "sqlSessionFactory_r") public class Read_MybatisConfig_manyMybatis { /** * 读库的数据源 * * @return DataSource */ @Bean("dataSource_read") @ConfigurationProperties(prefix = "spring.datasource.datasource-read") public DataSource dataSource_read() { System.out.println("==============读取配置信息,创建DataSource============="); // 通过绑定全局配置,拿到属性后自动创建一个DruidDataSource return DruidDataSourceBuilder.create().build(); } /** * 配置读库的 SqlSessionFactory * * @return SqlSessionFactory * @throws Exception ex */ @Bean public SqlSessionFactory sqlSessionFactory_r() throws Exception { return FactoryUtil_manyMybatis.createFactory(dataSource_read(), "classpath:mapper/read/*Mapper_manyMybatis.xml", true); } } ``` ```java import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; import org.apache.ibatis.session.SqlSessionFactory; 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 sunwul.datasource.common.FactoryUtil_manyMybatis; import javax.sql.DataSource; /***** * @author sunwul * @date 2022/2/25 11:27 * @description */ @Configuration @MapperScan(basePackages = "sunwul.datasource.mapper.write", sqlSessionFactoryRef = "sqlSessionFactory_w") public class Write_MybatisConfig_manyMybatis { /** * 写库的数据源 * * @return DataSource */ @Bean("dataSource_write") @ConfigurationProperties(prefix = "spring.datasource.datasource-write") public DataSource dataSource_write() { System.out.println("==============读取配置信息,创建DataSource============="); // 通过绑定全局配置,拿到属性后自动创建一个DruidDataSource return DruidDataSourceBuilder.create().build(); } /** * 配置写库的 SqlSessionFactory * * @return SqlSessionFactory * @throws Exception ex */ @Bean @Primary public SqlSessionFactory sqlSessionFactory_w() throws Exception { return FactoryUtil_manyMybatis.createFactory(dataSource_write(), "classpath:mapper/write/*Mapper_manyMybatis.xml", true); } } ``` ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import sunwul.datasource.entity.TAdmin_manyMybatis; import java.util.List; import java.util.stream.Collectors; /***** * @author sunwul * @date 2022/2/28 9:33 * @description */ @RestController public class TestController_manyMybatis { @Autowired // @Qualifier("TAdminMapper_R") sunwul.datasource.mapper.read.TAdminMapper_manyMybatis mapper_r; @Autowired // @Qualifier("TAdminMapper_W") sunwul.datasource.mapper.write.TAdminMapper_manyMybatis mapper_w; @RequestMapping("/sel") public List toSel(){ return mapper_r.sel_admin(); } @RequestMapping("/selOne/{id}") public TAdmin_manyMybatis toSelOne(@PathVariable("id") Integer id){ return mapper_r.sel_admin().stream().filter(s -> s.getId() == id).distinct().collect(Collectors.toList()).get(0); } @RequestMapping("/add/{name}") public Integer toAdd(@PathVariable("name") String name){ return mapper_w.add_admin(name); } } ``` ### 三,多数据源事务控制 > 在多数据源的情况下, 由于设计到多个数据库的读写. 一旦发生异常就可能会导致数据不一致的情况 > 在这种情况希望使用事务进行回退, 但是Spring的声明式事务在一次请求线程中只能使用一个数据源进行控制 > > 对于多源数据库来说: > > + 单一事务管理器(TransactionManager)无法切换数据源, 需要配置多个TransactionManager > + @TransactionManager 是无法管理多个数据源的, 如果想真正实现多源数据库的事务控制, 肯定需要分布式事务. > > **以下仅仅只是多数据源事务控制的一种变通方式** #### Spring编程式事务 > 编程式事务可以在一个方法中对不同的数据源进行事务控制,当存在异常时,对所有数据库的操作都会回滚 ```java import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.support.TransactionTemplate; import sunwul.datasource.common.FactoryUtil_springTran; import javax.sql.DataSource; /***** * @author sunwul * @date 2022/2/25 9:22 * @description * * 指定扫描的包 * * 指定使用的sqlSessionFactory */ @Configuration @MapperScan(basePackages = "sunwul.datasource.mapper.read", sqlSessionFactoryRef = "sqlSessionFactory_r") public class Read_MybatisConfig_springTran { /** * 读库的数据源 * * @return DataSource */ @Bean("dataSource_read") @ConfigurationProperties(prefix = "spring.datasource.datasource-read") public DataSource dataSource_read() { System.out.println("==============读取配置信息,创建DataSource============="); // 通过绑定全局配置,拿到属性后自动创建一个DruidDataSource return DruidDataSourceBuilder.create().build(); } /** * 配置读库的 SqlSessionFactory * * @return SqlSessionFactory * @throws Exception ex */ @Bean public SqlSessionFactory sqlSessionFactory_r() throws Exception { return FactoryUtil_springTran.createFactory(dataSource_read(), "classpath:mapper/read/*Mapper_springTran.xml", true); } /** * 读库的事务管理 * * @return DataSourceTransactionManager */ @Bean public DataSourceTransactionManager dataSourceTransactionManager_r() { DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(); dataSourceTransactionManager.setDataSource(dataSource_read()); return dataSourceTransactionManager; } /** * 事务模板 - Spring编程式事务 * * @return TransactionTemplate */ @Bean public TransactionTemplate transactionTemplate_r() { return new TransactionTemplate(dataSourceTransactionManager_r()); } } ``` ```java import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.annotation.MapperScan; 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.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.support.TransactionTemplate; import sunwul.datasource.common.FactoryUtil_springTran; import javax.sql.DataSource; /***** * @author sunwul * @date 2022/2/25 11:27 * @description * * 指定扫描的包 * * 指定使用的sqlSessionFactory */ @Configuration @MapperScan(basePackages = "sunwul.datasource.mapper.write", sqlSessionFactoryRef = "sqlSessionFactory_w") public class Write_MybatisConfig_springTran { /** * 写库数据源 * * @return DataSource */ @Bean("dataSource_write") @ConfigurationProperties(prefix = "spring.datasource.datasource-write") public DataSource dataSource_write() { System.out.println("==============读取配置信息,创建DataSource============="); // 通过绑定全局配置,拿到属性后自动创建一个DruidDataSource return DruidDataSourceBuilder.create().build(); } /** * 配置写库的 SqlSessionFactory * 整合 * * @return SqlSessionFactory * @throws Exception ex */ @Bean @Primary public SqlSessionFactory sqlSessionFactory_w() throws Exception { return FactoryUtil_springTran.createFactory(dataSource_write(), "classpath:mapper/write/*Mapper_springTran.xml", true); } /** * 写库的事务管理 * * @return DataSourceTransactionManager */ @Bean @Primary public DataSourceTransactionManager dataSourceTransactionManager_w() { DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(); dataSourceTransactionManager.setDataSource(dataSource_write()); return dataSourceTransactionManager; } /** * 事务模板 - Spring编程式事务 * * @return TransactionTemplate */ @Bean public TransactionTemplate transactionTemplate_w() { return new TransactionTemplate(dataSourceTransactionManager_w()); } } ``` ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; import sunwul.datasource.entity.TAdmin_springTran; import sunwul.datasource.service.TAdminService_springTran; import java.util.List; /** * @author sunwul * @date 2022/3/10 22:25:46 * @description */ @Service public class TAdminServiceImpl_springTran implements TAdminService_springTran { @Autowired sunwul.datasource.mapper.read.TAdminMapper_springTran TAdminMapper_R; @Autowired sunwul.datasource.mapper.write.TAdminMapper_springTran TAdminMapper_W; @Autowired TransactionTemplate transactionTemplate_w; @Autowired TransactionTemplate transactionTemplate_r; @Override public List sel_admin() { return TAdminMapper_R.sel_admin(); } /** * 添加数据, 制造异常,触发事务 * 由于此时写库的事务管理添加了@Primary注解, 因此此时只有写库的事务会生效 * 此时会因为数据库操作的顺序出现以下几种情况: * 1. 当写库操作或读库操作位于异常代码之前时: 读库正常插入数据, 写库回滚操作 * 2. 当读库操作位于异常代码之前,写库操作位于异常代码之后时: 读库正常插入数据, 不会执行到写库操作, 直接抛出异常 * 3. 当写库操作位于异常代码之前,读库操作位于异常代码之后时: 写库回滚操作, 不会执行到读库操作, 直接抛出异常 * 综上: * 开启了对应数据库的事务管理后, 当出现异常情况时, 会回滚对应的数据库操作 */ @Transactional // @Override public Integer add_admin_bak(String name) { TAdminMapper_W.add_admin(name); TAdminMapper_R.add_admin(name); int a = 1 / 0; return a; } /** * 通过Spring编程式事务对不同数据源进行事务控制 */ @Override public Integer add_admin(String name) { transactionTemplate_w.execute((status_w) -> { transactionTemplate_r.execute((status_r) -> { try { TAdminMapper_W.add_admin(name); TAdminMapper_R.add_admin(name); int a = 1 / 0; // 制造异常,触发事务 } catch (Exception e) { e.printStackTrace(); status_w.setRollbackOnly(); // 调用写库的回滚 status_r.setRollbackOnly(); // 调用读库的回滚 return false; } return true; }); return true; }); return 999; } } ``` #### Spring声明式事务 > 声明式事务通过注解来进行事务控制,但由于@Transactional注解无法同时使用多个,因此只能通过Spring的代理对象来解决这个问题 ![声明式事务](README.assets/声明式事务.png) ### 四,dynamic-datasource多数据源组件 ...等待探索