【Java多数据源实现教程】实现动态数据源、多数据源切换方式(下)

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 【Java多数据源实现教程】实现动态数据源、多数据源切换方式(下)

三、多数据源事务控制


在多数据源下,由于涉及到数据库的多个读写。一旦发生异常就可能会导致数据不一致的情况, 在这种情况希望使用事务进行回退。


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的快速集成。

提供对Mybatis­Plus,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)引入dynamic­datasource­spring­boot­starter。

<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);
}


相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
3天前
|
NoSQL Java 关系型数据库
Liunx部署java项目Tomcat、Redis、Mysql教程
本文详细介绍了如何在 Linux 服务器上安装和配置 Tomcat、MySQL 和 Redis,并部署 Java 项目。通过这些步骤,您可以搭建一个高效稳定的 Java 应用运行环境。希望本文能为您在实际操作中提供有价值的参考。
51 26
|
9天前
|
安全 Java 编译器
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
|
9天前
|
Java 开发工具 Android开发
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
|
16天前
|
Java 编译器 Android开发
Kotlin教程笔记(28) -Kotlin 与 Java 混编
Kotlin教程笔记(28) -Kotlin 与 Java 混编
25 2
|
9天前
|
Java 数据库连接 编译器
Kotlin教程笔记(29) -Kotlin 兼容 Java 遇到的最大的“坑”
Kotlin教程笔记(29) -Kotlin 兼容 Java 遇到的最大的“坑”
27 0
|
1月前
|
安全 Java 编译器
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
|
1月前
|
Java 开发工具 Android开发
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
|
1月前
|
Java 编译器 Android开发
Kotlin教程笔记(28) -Kotlin 与 Java 混编
Kotlin教程笔记(28) -Kotlin 与 Java 混编
|
2月前
|
JSON Java Maven
实现Java Spring Boot FCM推送教程
本指南介绍了如何在Spring Boot项目中集成Firebase云消息服务(FCM),包括创建项目、添加依赖、配置服务账户密钥、编写推送服务类以及发送消息等步骤,帮助开发者快速实现推送通知功能。
95 2
|
21天前
|
Java 编译器 Android开发
Kotlin教程笔记(28) -Kotlin 与 Java 混编
Kotlin教程笔记(28) -Kotlin 与 Java 混编
10 0