文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2023-08-16 16:32

关注

在这里插入图片描述

本文为 【Java多数据源实现教程】 相关知识,由于自己最近在做导师的项目的时候需要使用这种技术,于是自学了相关技术原理与实现,并将其整理如下,具体包含:多数据源的典型使用场景(包含业务复杂场景读写分离场景),多数据源实现原理及实现方法(包含通过AbstractRoutingDataSource实现动态数据源多数据源切换方式Spring集成多个MyBatis框架实现多数据源),多数据源事务控制(包含只使用主库TransactionManger一个方法开启2个事务),dynamic-datasource多数源组件等~

📌博主主页:小新要变强 的主页
👉Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~
👉算法刷题路线可参考:算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~
👉Java微服务开源项目可参考:企业级Java微服务开源项目(开源框架,用于学习、毕设、公司项目、私活等,减少开发工作,让您只关注业务!)


在这里插入图片描述

在实际开发中,经常可能遇到在一个应用中可能需要访问多个数据库的情况。以下是两种典型场景:

1️⃣业务复杂(数据量大)

数据分布在不同的数据库中,数据库拆了, 应用没拆。 一个公司多个子项目,各用各的数据库,涉及数据共享…
在这里插入图片描述

2️⃣读写分离

在这里插入图片描述

这里的架构与上图类似。不同的是,在读写分离中,主库和从库的数据库是一致的(不考虑主从延迟)。数据更新操作(insert、update、delete)都是在主库上进行,主库将数据变更信息同步给从库。在查询时,可以在从库上进行,从而分担主库的压力。

原理:

对于大多数的java应用,都使用了spring框架,spring-jdbc模块提供AbstractRoutingDataSource,其内部可以包含了多个DataSource,然后在运行时来动态的访问哪个数据库。这种方式访问数据库的架构图如下所示:

在这里插入图片描述

应用直接操作的是AbstractRoutingDataSource的实现类,告诉AbstractRoutingDataSource访问哪个数据库,然后由AbstractRoutingDataSource从事先配置好的数据源(ds1、ds2)选择一个,来访问对应的数据库。

在这里插入图片描述

1️⃣通过AbstractRoutingDataSource实现动态数据源

通过这个类可以实现动态数据源切换。如下是这个类的成员变量:

private Map targetDataSources;private Object defaultTargetDataSource;private Map resolvedDataSources;

而AbstractRoutingDataSource实现了InitializingBean接口,并实现了afterPropertiesSet方法。afterPropertiesSet方法是初始化bean的时候执行,通常用作数据初始化。(resolvedDataSources就是在这里赋值)

@Overridepublic void afterPropertiesSet() {...this.resolvedDataSources = new HashMap entry : this.targetDataSources.entrySet()) {Object lookupKey = resolveSpecifiedLookupKey(entry.getKey());DataSource dataSource = resolveSpecifiedDataSource(entry.getValue());this.resolvedDataSources.put(lookupKey, dataSource);}...}

总结,在整个代码中我们只需做4件大事:

什么到这还不会? 附上代码:

🍀(1)配置多数据源和 AbstractRoutingDataSource的自定义实现类:DynamicDataSource

配置多数据:

application.yml:

spring:  datasource:    type: com.alibaba.druid.pool.DruidDataSource    datasource1:      url: jdbc:mysql://127.0.0.1:3306/datasource1?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false      username: root      password: 123456      initial-size: 1      min-idle: 1      max-active: 20      test-on-borrow: true      driver-class-name: com.mysql.cj.jdbc.Driver    datasource2:      url: jdbc:mysql://127.0.0.1:3306/datasource2?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false      username: root      password: 123456      initial-size: 1      min-idle: 1      max-active: 20      test-on-borrow: true      driver-class-name: com.mysql.cj.jdbc.Driver

DynamicDataSourceConfig.java:

@Configurationpublic class DynamicDataSourceConfig {    @Bean    @ConfigurationProperties("spring.datasource.datasource1")    public DataSource firstDataSource(){        return DruidDataSourceBuilder.create().build();    }    @Bean    @ConfigurationProperties("spring.datasource.datasource2")    public DataSource secondDataSource(){        return DruidDataSourceBuilder.create().build();    }    @Bean    @Primary    public DynamicDataSource dataSource(DataSource firstDataSource, DataSource secondDataSource) {        Map targetDataSources = new HashMap<>(5);        targetDataSources.put(DataSourceNames.FIRST, firstDataSource);        targetDataSources.put(DataSourceNames.SECOND, secondDataSource);        return new DynamicDataSource(firstDataSource, targetDataSources);    }}

DynamicDataSource.java:

public class DynamicDataSource extends AbstractRoutingDataSource {        private static final ThreadLocal CONTEXT_HOLDER = new ThreadLocal<>();        public DynamicDataSource(DataSource defaultTargetDataSource, MaptargetDataSources) {        super.setDefaultTargetDataSource(defaultTargetDataSource);        super.setTargetDataSources(targetDataSources);        super.afterPropertiesSet();    }    @Override    protected Object determineCurrentLookupKey() {        return getDataSource();    }    public static void setDataSource(String dataSource) {        CONTEXT_HOLDER.set(dataSource);    }    public static String getDataSource() {        return CONTEXT_HOLDER.get();    }    public static void clearDataSource() {        CONTEXT_HOLDER.remove();    }}

2️⃣多数据源切换方式

多数据源切换方式需要根据我们的具体需求进行选择:

🍀(1)AOP+自定义注解

用于不同业务的数据源: 一般利用AOP,结合自定义注解动态切换数据源

@Target({ElementType.METHOD,ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)public @interface WR {    String value() default "W";}
@Component@Aspectpublic class DynamicDataSourceAspect {    // 前置通知    @Before("within(com.tuling.dynamic.datasource.service.impl.*) && @annotation(wr)")    public void before(JoinPoint joinPoint, WR wr){        System.out.println(wr.value());    }}
@Servicepublic class FrendImplService implements FrendService {    @Autowired    FrendMapper frendMapper;    @Override    @WR("R") // 库2    public List list() {        return frendMapper.list();    }    @Override    @WR("W") // 库1    public void save(Frend frend) {        frendMapper.save(frend);    }}

🍀(2)MyBatis插件

用于读写分离的数据源:如果是MyBatis可以结合插件实现读写分离动态切换数据源

@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();        MappedStatement ms = (MappedStatement) objects[0];        // 读方法        if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {            DynamicDataSource.name.set("R");        } else {            // 写方法            DynamicDataSource.name.set("W");        }        // 修改当前线程要选择的数据源的key        return invocation.proceed();    }    @Override    public Object plugin(Object target) {        if (target instanceof Executor) {            return Plugin.wrap(target, this);        } else {            return target;        }    }    @Override    public void setProperties(Properties properties) {    }}

3️⃣Spring集成多个MyBatis框架实现多数据源

在这里插入图片描述

WDataSourceConfig.java:

@MapperScan(basePackages = "com.tuling.dynamic.datasource.mapper.w", sqlSessionFactoryRef = "wSqlSessionFactory")public class WDataSourceConfig {    @Bean    @Primary    public SqlSessionFactory wSqlSessionFactory(@Qualifier("dataSource1") DataSource dataSource1) throws Exception {        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();        sessionFactory.setDataSource(dataSource1);        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/w        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();        configuration.setLogImpl(StdOutImpl.class);        sessionFactory.setConfiguration(configuration);        return sessionFactory.getObject();    }}

RDataSourceConfig.java:

@MapperScan(basePackages = "com.tuling.dynamic.datasource.mapper.r", sqlSessionFactoryRef = "rSqlSessionFactory")public class RMyBatisConfig {    @Bean    public SqlSessionFactory rSqlSessionFactory(@Qualifier("dataSource2") DataSource dataSource2) throws Exception {        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();        sessionFactory.setDataSource(dataSource2);        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/r        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();        configuration.setLogImpl(StdOutImpl.class);        sessionFactory.setConfiguration(configuration);        return sessionFactory.getObject();    }}

在多数据源下,由于涉及到数据库的多个读写。一旦发生异常就可能会导致数据不一致的情况, 在这种情况希望使用事务进行回退。
Spring的声明式事务在一次请求线程中只能使用一个数据源进行控制。
但是是对于多源数据库:

@Beanpublic DataSourceTransactionManager transactionManager1(DynamicDataSource dataSource){    DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();    dataSourceTransactionManager.setDataSource(dataSource);    return dataSourceTransactionManager;}@Beanpublic DataSourceTransactionManager transactionManager2(DynamicDataSource dataSource){    DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();    dataSourceTransactionManager.setDataSource(dataSource);    return dataSourceTransactionManager;}

1️⃣只使用主库TransactionManger

使用主库事务管理器,也就是说事务中产生异常时,只能回滚主库数据。但是因为数据操作顺序是先主后从,所以分一下三种情况:

@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; 从库插入之后出现异常, 只能回滚主库数据 ,从库数据是无法回滚的 , 数据将不一致}

当然这只是理想情况,例外情况:

2️⃣一个方法开启2个事务

spring编程式事务 :

// 读‐‐ 写库@Overridepublic 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;}

两三个数据源、事务场景比较少,基于 SpringBoot 的多数据源组件,功能强悍,支持 Seata 分布式事务。

🍀(1)约定

🍀(2)使用方法

(1)引入dynamic­datasource­spring­boot­starter。

    com.baomidou    dynamic‐datasource‐spring‐boot‐starter    ${version}

(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 now() {        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;        return ds.getCurrentDataSources().keySet();    }    //通用数据源会根据maven中配置的连接池根据顺序依次选择。    //默认的顺序为druid>hikaricp>beecp>dbcp>spring basic    @PostMapping("/add")    @ApiOperation("通用添加数据源(推荐)")    public Set 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 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 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 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 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 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 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 "删除成功";    }}

原理:

// 获取当前方法的DS注解的value值String dsKey = determineDatasourceKey(invocation);// 设置当当前数据源的标识TheardLocal中DynamicDataSourceContextHolder.push(dsKey);try {    // 执行目标方法    return invocation.proceed();} finally {    DynamicDataSourceContextHolder.poll();}
@Overridepublic DataSource determineDataSource() {    // 拿到之前切换的数据源标识    String dsKey = DynamicDataSourceContextHolder.peek();    // 通过该标识获取对应的数据源    return getDataSource(dsKey);}

在这里插入图片描述
👉Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~
👉算法刷题路线可参考:算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~

来源地址:https://blog.csdn.net/qq_42146402/article/details/127907963

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯