三、多数据源事务控制
在多数据源下,由于涉及到数据库的多个读写。一旦发生异常就可能会导致数据不一致的情况, 在这种情况希望使用事务进行回退。
Spring的声明式事务在一次请求线程中只能使用一个数据源进行控制。
但是是对于多源数据库:
(1)单一事务管理器(TransactionManager)无法切换数据源,需要配置多个TransactionManager。
(2)@Transactionnal是无法管理多个数据源的。 如果想真正实现多源数据库事务控制,肯定是需要分布式事务。这里讲解多源数据库事务控制的一种变通方式。
@Bean public DataSourceTransactionManager transactionManager1(DynamicDataSource dataSource){ DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(); dataSourceTransactionManager.setDataSource(dataSource); return dataSourceTransactionManager; } @Bean public DataSourceTransactionManager transactionManager2(DynamicDataSource dataSource){ DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(); dataSourceTransactionManager.setDataSource(dataSource); return dataSourceTransactionManager; }
1️⃣只使用主库TransactionManger
使用主库事务管理器,也就是说事务中产生异常时,只能回滚主库数据。但是因为数据操作顺序是先主后从,所以分一下三种情况:
(1)主库插入时异常,主库未插成功,这时候从库还没来及插入,主从数据是还是一致的
(2)主库插入成功,从库插入时异常,这时候在主库事务管理器监测到事务中存在异常,将之前插入的主库数据插入,主从数据还是一致的
(3)主库插入成功,从库插入成功,事务结束,主从数据一致
@Override @WR("W") public void save(Frend frend) { frendMapper.save(frend); //int a=1/0; 1.主库插入时异常,主库未插成功,这时候从库还没来及插入,主从数据是还是一致的 } @Override @WR("R") @Transactional(transactionManager = "transactionManager2",propagation= Propagation.REQUIRES_NEW) public void saveRead(Frend frend) { frend.setName("xushu"); frendMapper.save(frend); // int a=1/0; 2.主库插入成功,从库插入时异常,这时候在主库事务管理器监测到事务中存在异常,将之前插入的主库数据插入,主从数据还是一致的 } @Override @Transactional(transactionManager = "transactionManager1") public void saveAll(Frend frend) { // 3. 无异常情况:主库插入成功,从库插入成功,事务结束,主从数据一致。 FrendService self= (FrendService)AopContext.currentProxy(); self.save(frend); self.saveRead(frend); //int a=1/0; 从库插入之后出现异常, 只能回滚主库数据 ,从库数据是无法回滚的 , 数据将不一致 }
当然这只是理想情况,例外情况:
(4)从库插入之后出现异常, 只能回滚主库数据 ,从库数据是无法回滚的 , 数据将不一致
(5)从库数据插入成功后,主库提交,这时候主库崩溃了,导致数据没插入,这时候从库数据也是无法回滚的。这种方式可以简单实现多源数据库的事务管理,但是无法处理上述情况。
2️⃣一个方法开启2个事务
spring编程式事务 :
// 读‐‐ 写库 @Override public void saveAll(Frend frend) { wtransactionTemplate.execute(wstatus ‐> { rtransactionTemplate.execute(rstatus ‐> { try{ saveW(frend); saveR(frend); int a=1/0; return true; }catch (Exception e){ wstatus.setRollbackOnly(); rstatus.setRollbackOnly(); return false; } }); return true; }); }
spring声明式事务:
@Transactional(transactionManager = "wTransactionManager") public void saveAll(Frend frend) throws Exception { FrendService frendService = (FrendService) AopContext.currentProxy(); frendService.saveAllR(frend); } @Transactional(transactionManager = "rTransactionManager",propagation = Propagation.REQUIRES_NEW ) public void saveAllR(Frend frend) { saveW(frend); saveR(frend); int a = 1 / 0; }
四、dynamic-datasource多数源组件
两三个数据源、事务场景比较少,基于 SpringBoot 的多数据源组件,功能强悍,支持 Seata 分布式事务。
支持数据源分组,适用于多种场景纯粹多库 读写分离 一主多从 混合模式。
支持数据库敏感配置信息加密 ENC()。
支持每个数据库独立初始化表结构schema和数据库database。
支持无数据源启动,支持懒加载数据源(需要的时候再创建连接)。
支持自定义注解,需继承DS(3.2.0+)。
提供并简化对Druid,HikariCp,BeeCp,Dbcp2的快速集成。
提供对MybatisPlus,Quartz,ShardingJdbc,P6sy,Jndi等组件的集成方案。
提供自定义数据源来源方案(如全从数据库加载)。
提供项目启动后动态增加移除数据源方案。
提供Mybatis环境下的纯读写分离方案。
提供使用spel动态参数解析数据源方案。内置spel,session,header,支持自定义。
支持多层数据源嵌套切换 。(ServiceA >>> ServiceB >>> ServiceC)。
提供基于seata的分布式事务方案。
提供本地多数据源事务方案。 附:不能和原生spring事务混用。
🍀(1)约定
(1)本框架只做切换数据源 这件核心的事情,并不限制你的具体操作,切换了数据源可以做任何CRUD。
(2)配置文件所有以下划线 _ 分割的数据源 首部 即为组的名称,相同组名称的数据源会放在一个组下。
(3)切换数据源可以是组名,也可以是具体数据源名称。组名则切换时采用负载均衡算法切换,默认是轮询的。
(4)默认的数据源名称为 master ,你可以通过 spring.datasource.dynamic.primary 修改。
(5)方法上的注解优先于类上注解。
(6)DS支持继承抽象类上的DS,暂不支持继承接口上的DS。
🍀(2)使用方法
(1)引入dynamicdatasourcespringbootstarter。
<dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic‐datasource‐spring‐boot‐starter</artifactId> <version>${version}</version> </dependency>
(2)配置数据源。
spring: datasource: dynamic: #设置默认的数据源或者数据源组,默认值即为master primary: master #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源 strict: false datasource: master: url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver # 3.2.0开始支持SPI可省略此配置 slave_1: url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver slave_2: url: ENC(xxxxx) # 内置加密,使用请查看详细文档 username: ENC(xxxxx) password: ENC(xxxxx) driver‐class‐name: com.mysql.jdbc.Driver #......省略 #以上会配置一个默认库master,一个组slave下有两个子库slave_1,slave_2
# 多主多从 纯粹多库(记得设置primary) 混合配置 spring: spring: spring: datasource: datasource: datasource: dynamic: dynamic: dynamic: datasource: datasource: datasource: master_1: mysql: master: master_2: oracle: slave_1: slave_1: sqlserver: slave_2: slave_2: postgresql: oracle_1: slave_3: h2: oracle_2:
(3)使用@DS切换数据源。
@DS可以注解在方法上或类上,同时存在就近原则方法上注解优先于类上注解。
注解 | 结果 |
没有@DS | 默认数据源 |
@DS(“dsName”) | dsName可以为组名也可以为具体某个库的名称 |
@Service @DS("slave") public class UserServiceImpl implements UserService { @Autowired private JdbcTemplate jdbcTemplate; public List selectAll() { return jdbcTemplate.queryForList("select * from user"); } @Override @DS("slave_1") public List selectByCondition() { return jdbcTemplate.queryForList("select * from user where age >10"); } }
本地事务:
使用@DSTransactional即可, 不能和Spring@Transactional混用!
//在最外层的方法添加 @DSTransactional,底下调用的各个类该切数据源就正常使用DS切换数据源即可。 就是这么简单。~ //如AService调用BService和CService的方法,A,B,C分别对应不同数据源。 public class AService { @DS("a")//如果a是默认数据源则不需要DS注解。 @DSTransactional public void dosomething(){ BService.dosomething(); CService.dosomething(); } } public class BService { @DS("b") public void dosomething(){ //dosomething } } public class CService { @DS("c") public void dosomething(){ //dosomething } }
只要@DSTransactional注解下任一环节发生异常,则全局多数据源事务回滚。
如果BC上也有@DSTransactional会有影响吗?答:没有影响的。
动态添加删除数据源:
通过DynamicRoutingDataSource 类即可,它就相当于我们之前自定义的那个DynamicDataSource。
@RestController @RequestMapping("/datasources") @Api(tags = "添加删除数据源") public class DataSourceController { @Autowired private DataSource dataSource; // private final DataSourceCreator dataSourceCreator; //3.3.1及以下版本使用这个通用 @Autowired private DefaultDataSourceCreator dataSourceCreator; @Autowired private BasicDataSourceCreator basicDataSourceCreator; @Autowired private JndiDataSourceCreator jndiDataSourceCreator; @Autowired private DruidDataSourceCreator druidDataSourceCreator; @Autowired private HikariDataSourceCreator hikariDataSourceCreator; @Autowired private BeeCpDataSourceCreator beeCpDataSourceCreator; @Autowired private Dbcp2DataSourceCreator dbcp2DataSourceCreator; @GetMapping @ApiOperation("获取当前所有数据源") public Set<String> now() { DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource; return ds.getCurrentDataSources().keySet(); } //通用数据源会根据maven中配置的连接池根据顺序依次选择。 //默认的顺序为druid>hikaricp>beecp>dbcp>spring basic @PostMapping("/add") @ApiOperation("通用添加数据源(推荐)") public Set<String> add(@Validated @RequestBody DataSourceDTO dto) { DataSourceProperty dataSourceProperty = new DataSourceProperty(); BeanUtils.copyProperties(dto, dataSourceProperty); DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource; DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty); ds.addDataSource(dto.getPollName(), dataSource); return ds.getCurrentDataSources().keySet(); } @PostMapping("/addBasic(强烈不推荐,除了用了马上移除)") @ApiOperation(value = "添加基础数据源", notes = "调用Springboot内置方法创建数据源,兼容1,2") public Set<String> addBasic(@Validated @RequestBody DataSourceDTO dto) { DataSourceProperty dataSourceProperty = new DataSourceProperty(); BeanUtils.copyProperties(dto, dataSourceProperty); DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource; DataSource dataSource = basicDataSourceCreator.createDataSource(dataSourceProperty); ds.addDataSource(dto.getPollName(), dataSource); return ds.getCurrentDataSources().keySet(); } @PostMapping("/addJndi") @ApiOperation("添加JNDI数据源") public Set<String> addJndi(String pollName, String jndiName) { DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource; DataSource dataSource = jndiDataSourceCreator.createDataSource(jndiName); ds.addDataSource(pollName, dataSource); return ds.getCurrentDataSources().keySet(); } @PostMapping("/addDruid") @ApiOperation("基础Druid数据源") public Set<String> addDruid(@Validated @RequestBody DataSourceDTO dto) { DataSourceProperty dataSourceProperty = new DataSourceProperty(); BeanUtils.copyProperties(dto, dataSourceProperty); dataSourceProperty.setLazy(true); DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource; DataSource dataSource = druidDataSourceCreator.createDataSource(dataSourceProperty); ds.addDataSource(dto.getPollName(), dataSource); return ds.getCurrentDataSources().keySet(); } @PostMapping("/addHikariCP") @ApiOperation("基础HikariCP数据源") public Set<String> addHikariCP(@Validated @RequestBody DataSourceDTO dto) { DataSourceProperty dataSourceProperty = new DataSourceProperty(); BeanUtils.copyProperties(dto, dataSourceProperty); dataSourceProperty.setLazy(true);//3.4.0版本以下如果有此属性,需手动设置,不然会空指针。 DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource; DataSource dataSource = hikariDataSourceCreator.createDataSource(dataSourceProperty); ds.addDataSource(dto.getPollName(), dataSource); return ds.getCurrentDataSources().keySet(); } @PostMapping("/addBeeCp") @ApiOperation("基础BeeCp数据源") public Set<String> addBeeCp(@Validated @RequestBody DataSourceDTO dto) { DataSourceProperty dataSourceProperty = new DataSourceProperty(); BeanUtils.copyProperties(dto, dataSourceProperty); dataSourceProperty.setLazy(true);//3.4.0版本以下如果有此属性,需手动设置,不然会空指针。 DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource; DataSource dataSource = beeCpDataSourceCreator.createDataSource(dataSourceProperty); ds.addDataSource(dto.getPollName(), dataSource); return ds.getCurrentDataSources().keySet(); } @PostMapping("/addDbcp") @ApiOperation("基础Dbcp数据源") public Set<String> addDbcp(@Validated @RequestBody DataSourceDTO dto) { DataSourceProperty dataSourceProperty = new DataSourceProperty(); BeanUtils.copyProperties(dto, dataSourceProperty); dataSourceProperty.setLazy(true);//3.4.0版本以下如果有此属性,需手动设置,不然会空指针。 DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource; DataSource dataSource = dbcp2DataSourceCreator.createDataSource(dataSourceProperty); ds.addDataSource(dto.getPollName(), dataSource); return ds.getCurrentDataSources().keySet(); } @DeleteMapping @ApiOperation("删除数据源") public String remove(String name) { DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource; ds.removeDataSource(name); return "删除成功"; } }
原理:
(1)通过DynamicDataSourceAutoConfiguration自动配置类
(2)配置了DynamicRoutingDataSource 它就相当于我们之前自定义的那个DynamicDataSource,用来动态提供数据源
(3)配置DynamicDataSourceAnnotationAdvisor 就相当于之前自定义的一个切面类
(4)设置DynamicDataSourceAnnotationInterceptor 当前advisor的拦截器,把它理解成之前环绕通知
(5)当执行方法会调用DynamicDataSourceAnnotationInterceptor#invoke 来进行增强:
// 获取当前方法的DS注解的value值 String dsKey = determineDatasourceKey(invocation); // 设置当当前数据源的标识TheardLocal中 DynamicDataSourceContextHolder.push(dsKey); try { // 执行目标方法 return invocation.proceed(); } finally { DynamicDataSourceContextHolder.poll(); }
(6)在执行数据库操作时候, 就会调用DataSource.getConnection,此时的DataSource指的就是DynamicRoutingDataSource
(7)然后执行模板方法
@Override public DataSource determineDataSource() { // 拿到之前切换的数据源标识 String dsKey = DynamicDataSourceContextHolder.peek(); // 通过该标识获取对应的数据源 return getDataSource(dsKey); }