Spring Boot 集成Mybatis实现多数据源

2023-11-09

总体来说多数据源配置有两种方式,一种是静态的,一种是动态的。

静态的方式

我们以两套配置方式为例,在项目中有两套配置文件,两套mapper,两套SqlSessionFactory,各自处理各自的业务,这个两套mapper都可以进行增删改查的操作,在这两个主MYSQL后也可以各自配置自己的slave,实现数据的备份。如果在增加一个数据源就得从头到尾的增加一遍。

先看看两个配置文件:

## master 数据源配置
master1.datasource.url=jdbc:mysql://localhost:3306/learn?useUnicode=true&characterEncoding=utf8
master1.datasource.username=root
master1.datasource.password=
master1.datasource.driverClassName=com.mysql.jdbc.Driver


## slave 数据源配置
master2.datasource.url=jdbc:mysql://localhost:3308/learn?useUnicode=true&characterEncoding=utf8
master2.datasource.username=root
master2.datasource.password=
master2.datasource.driverClassName=com.mysql.jdbc.Driver

这两个数据源的配置不分主从,看网上很多这种配置方式,说是主从配置,个人认为既然什么都是两套就没有必要分出主从,分出读写了,根据业务的需求以及数据库服务器的性能进行划分即可。

两个配置类:

@Configuration
// 扫描 Mapper 接口并容器管理
@MapperScan(basePackages = Master1DataSourceConfig.PACKAGE, sqlSessionFactoryRef = "master1SqlSessionFactory")
public class Master1DataSourceConfig {
    // 精确到 master 目录,以便跟其他数据源隔离
    static final String PACKAGE = "com.hui.readwrite.mapper.master1";
    static final String MAPPER_LOCATION = "classpath:mapper/master1.xml";

    @Value("${master1.datasource.url}")
    private String url;

    @Value("${master1.datasource.username}")
    private String user;

    @Value("${master1.datasource.password}")
    private String password;

    @Value("${master1.datasource.driverClassName}")
    private String driverClass;

    @Bean(name = "master1DataSource")
    @Primary
    public DataSource masterDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driverClass);
        dataSource.setUrl(url);
        dataSource.setUsername(user);
        dataSource.setPassword(password);
        return dataSource;
    }

    @Bean(name = "master1TransactionManager")
    @Primary
    public DataSourceTransactionManager masterTransactionManager() {
        return new DataSourceTransactionManager(masterDataSource());
    }

    @Bean(name = "master1SqlSessionFactory")
    @Primary
    public SqlSessionFactory masterSqlSessionFactory(@Qualifier("master1DataSource") DataSource masterDataSource)
            throws Exception {
        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(masterDataSource);
        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources(Master1DataSourceConfig.MAPPER_LOCATION));
        return sessionFactory.getObject();
    }
}

 

@Configuration
// 扫描 Mapper 接口并容器管理
@MapperScan(basePackages = Master2DataSourceConfig.PACKAGE, sqlSessionFactoryRef = "master2SqlSessionFactory")
public class Master2DataSourceConfig {
        // 精确到 master 目录,以便跟其他数据源隔离
        static final String PACKAGE = "com.hui.readwrite.mapper.master2";
        static final String MAPPER_LOCATION = "classpath:mapper/master2.xml";

        @Value("${master2.datasource.url}")
        private String url;

        @Value("${master2.datasource.username}")
        private String user;

        @Value("${master2.datasource.password}")
        private String password;

        @Value("${master2.datasource.driverClassName}")
        private String driverClass;

        @Bean(name = "master2DataSource")
        public DataSource master2DataSource() {
            DruidDataSource dataSource = new DruidDataSource();
            dataSource.setDriverClassName(driverClass);
            dataSource.setUrl(url);
            dataSource.setUsername(user);
            dataSource.setPassword(password);
            return dataSource;
        }

        @Bean(name = "master2TransactionManager")
        public DataSourceTransactionManager master2TransactionManager() {
            return new DataSourceTransactionManager(master2DataSource());
        }

        @Bean(name = "master2SqlSessionFactory")
        public SqlSessionFactory clusterSqlSessionFactory(@Qualifier("master2DataSource") DataSource master2DataSource) throws Exception {
            final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
            sessionFactory.setDataSource(master2DataSource);
            sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
                    .getResources(Master2DataSourceConfig.MAPPER_LOCATION));
            return sessionFactory.getObject();
    }
}

@Primary 标志这个 Bean 如果在多个同类 Bean 候选时,该 Bean 优先被考虑。「多数据源配置的时候注意,必须要有一个主数据源,用 @Primary 标志该 Bean」

@MapperScan 扫描 Mapper 接口并容器管理,包路径精确到 master,为了和下面 cluster 数据源做到精确区分

@Value 获取全局配置文件 application.properties 的 kv 配置,并自动装配
sqlSessionFactoryRef 表示定义了 key ,表示一个唯一 SqlSessionFactory 实例

两个mapper接口的路径和xml文件的路径如下:

这样就可以实现同一个项目使用多个数据配置,缺点是不易维护和扩展。

动态方式

这种方式实现了一个写库多个读库,使用的是同一套Mapper接口和XML文件,这样就有很好的拓展性,具体代码如下:

先是生成不同的数据源,其中多个读数据源合并

@Configuration
public class DataBaseConfiguration{


    @Value("${spring.datasource.type}")
    private Class<? extends DataSource> dataSourceType;


    @Bean(name="writeDataSource", destroyMethod = "close", initMethod="init")
    @Primary
    @ConfigurationProperties(prefix = "spring.write.datasource")
    public DataSource writeDataSource() {
        return DataSourceBuilder.create().type(dataSourceType).build();
    }
    /**
     * 有多少个从库就要配置多少个
     * @return
     */
    @Bean(name = "readDataSourceOne")
    @ConfigurationProperties(prefix = "spring.read.one")
    public DataSource readDataSourceOne(){
        return DataSourceBuilder.create().type(dataSourceType).build();
    }


    @Bean(name = "readDataSourceTwo")
    @ConfigurationProperties(prefix = "spring.read.two")
    public DataSource readDataSourceTwo() {
        return DataSourceBuilder.create().type(dataSourceType).build();
    }


    @Bean("readDataSources")
    public List<DataSource> readDataSources(){
        List<DataSource> dataSources=new ArrayList<DataSource>();
        dataSources.add(readDataSourceOne());
        dataSources.add(readDataSourceTwo());
        return dataSources;
    }
}

生成一套SqlSessionFactory,进行动态切换

@Configuration
@ConditionalOnClass({EnableTransactionManagement.class})
@Import({ DataBaseConfiguration.class})
@MapperScan(basePackages={"com.hui.readwrite.mapper.master1"})
public class TxxsbatisConfiguration {


    @Value("${spring.datasource.type}")
    private Class<? extends DataSource> dataSourceType;


    @Value("${datasource.readSize}")
    private String dataSourceSize;


    @Resource(name = "writeDataSource")
    private DataSource dataSource;


    @Resource(name = "readDataSources")
    private List<DataSource> readDataSources;


    @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(roundRobinDataSouceProxy());
        sqlSessionFactoryBean.setTypeAliasesPackage("com.hui.readwrite.po");
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath:mapper/master1*//*.xml"));
        sqlSessionFactoryBean.getObject().getConfiguration().setMapUnderscoreToCamelCase(true);
        return sqlSessionFactoryBean.getObject();
    }
    /**
     * 有多少个数据源就要配置多少个bean
     * @return
     */
    @Bean
    public AbstractRoutingDataSource roundRobinDataSouceProxy() {
        int size = Integer.parseInt(dataSourceSize);
        TxxsAbstractRoutingDataSource proxy = new TxxsAbstractRoutingDataSource(size);
        Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
        targetDataSources.put(DataSourceType.write.getType(),dataSource);
        for (int i = 0; i < size; i++) {
            targetDataSources.put(i, readDataSources.get(i));
        }
        proxy.setDefaultTargetDataSource(dataSource);
        proxy.setTargetDataSources(targetDataSources);
        return proxy;
    }
}

进行选择,和读库的简单负载。Spring boot提供了AbstractRoutingDataSource根据用户定义的规则选择当前的数据库,这样我们可以在执行查询之前,设置读取从库,在执行完成后,恢复到主库。实现可动态路由的数据源,在每次数据库查询操作前执行

public class TxxsAbstractRoutingDataSource extends AbstractRoutingDataSource {


    private final int dataSourceNumber;
    private AtomicInteger count = new AtomicInteger(0);


    public TxxsAbstractRoutingDataSource(int dataSourceNumber) {
        this.dataSourceNumber = dataSourceNumber;
    }


    @Override
    protected Object determineCurrentLookupKey() {
        String typeKey = DataSourceContextHolder.getJdbcType();
        if (typeKey.equals(DataSourceType.write.getType()))
            return DataSourceType.write.getType();
        // 读 简单负载均衡
        int number = count.getAndAdd(1);
        int lookupKey = number % dataSourceNumber;
        return new Integer(lookupKey);
    }
}

利用AOP的方式实现,方法的控制

@Aspect
@Component
public class DataSourceAop {


    public static final Logger logger = LoggerFactory.getLogger(DataSourceAop.class);


    @Before("execution(* com.hui.readwrite.mapper..*.select*(..)) || execution(* com.hui.readwrite.mapper..*.get*(..))")
    public void setReadDataSourceType() {
        DataSourceContextHolder.read();
        logger.info("dataSource切换到:Read");
    }


    @Before("execution(* com.hui.readwrite.mapper..*.insert*(..)) || execution(* com.hui.readwrite.mapper..*.update*(..))")
    public void setWriteDataSourceType() {
        DataSourceContextHolder.write();
        logger.info("dataSource切换到:write");
    }
}

配置文件:

#一些总的配置文件
spring.aop.auto=true
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
datasource.readSize=2

# 主数据源,默认的
spring.write.datasource.driverClassName=com.mysql.jdbc.Driver
spring.write.datasource.url=jdbc:mysql://localhost:3306/learn?useUnicode=true&characterEncoding=utf8
spring.write.datasource.username=root
spring.write.datasource.password=root

# 从数据源
spring.read.one.driverClassName=com.mysql.jdbc.Driver
spring.read.one.url=jdbc:mysql://localhost:3307/learn?useUnicode=true&characterEncoding=utf8
spring.read.one.username=root
spring.read.one.password=root


spring.read.two.driverClassName=com.mysql.jdbc.Driver
spring.read.two.url=jdbc:mysql://localhost:3308/learn?useUnicode=true&characterEncoding=utf8
spring.read.two.username=root
spring.read.two.password=root

我们可以看到效果:

关于事务的观点

核心思想,spring提供了一个DataSource的子类,该类支持多个数据源

org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource

上述静态多个数据库的这种配置是不支持分布式事务的,也就是同一个事务中,不能操作多个数据库。

一些实时性要求很高的select语句,我们也可能需要放到master上执行,这些查询可能也需要放在master上执行,而不能放在slave上去执行,因为slave上可能存在延时。

如果有多台 master 或者有多台 slave。多台master组成一个HA,要实现当其中一台master挂了,自动切换到另一台master,这个功能可以使用LVS/Keepalived来实现,也可以通过进一步扩展ThreadLocalRountingDataSource来实现,可以另外加一个线程专门来每隔一秒来测试mysql是否正常来实现。

同样对于多台slave之间要实现负载均衡,同时当一台slave挂了时,要实现将其从负载均衡中去除掉,这个功能既可以使用LVS/Keepalived来实现,同样也可以通过近一步扩展ThreadLocalRountingDataSource来实现。

因为事务是依赖数据源的,你用注解切换数据源的操作跟用注解加事务控制的操作应该注意下先后关系。切换数据源的注解配置是要放在事务注解配置前面的,不然有问题。

解决方案:添加分布式的事务,Atomikos和spring结合来处理。

配置多个不同的数据源,使用一个sessionFactory,在业务逻辑使用的时候自动切换到不同的数据源,有一种是在拦截器里面根据不同的业务现切换到不同的datasource;有的会在业务层根据业务来自动切换。但这种方案在多线程并发的时候会出现一些问题,需要使用threadlocal等技术来实现多线程竞争切换数据源的问题。

由于我使用的是注解式事务,和我们的AOP数据源切面有一个顺序的关系。数据源切换必须先执行,数据库事务才能获取到正确的数据源。所以要明确指定 注解式事务和 我们AOP数据源切面的先后顺序。我们数据源切换的AOP是通过注解来实现的,只需要在AOP类上加上一个order(1)注解即可,其中1代表顺序号。


spring的事务管理,是基于数据源的,所以如果要实现动态数据源切换,而且在同一个数据源中保证事务是起作用的话,就需要注意二者的顺序问题,即:在事物起作用之前就要把数据源切换回来。

举一个例子:web开发常见是三层结构:controller、service、dao。一般事务都会在service层添加,如果使用spring的声明式事物管理,在调用service层代码之前,spring会通过aop的方式动态添加事务控制代码,所以如果要想保证事物是有效的,那么就必须在spring添加事务之前把数据源动态切换过来,也就是动态切换数据源的aop要至少在service上添加,而且要在spring声明式事物aop之前添加。

根据上面分析:

最简单的方式是把动态切换数据源的aop加到controller层,这样在controller层里面就可以确定下来数据源了。不过,这样有一个缺点就是,每一个controller绑定了一个数据源,不灵活。对于这种:一个请求,需要使用两个以上数据源中的数据完成的业务时,就无法实现了。

针对上面的这种问题,可以考虑把动态切换数据源的aop放到service层,但要注意一定要在事务aop之前来完成。这样,对于一个需要多个数据源数据的请求,我们只需要在controller里面注入多个service实现即可。但这种做法的问题在于,controller层里面会涉及到一些不必要的业务代码,例如:合并两个数据源中的list…

此外,针对上面的问题,还可以再考虑一种方案,就是把事务控制到dao层,然后在service层里面动态切换数据源。

源码地址:

http://download.csdn.net/detail/maoyeqiu/9885596

参考:

http://www.itwendao.com/article/detail/210530.html
http://www.jianshu.com/p/8813ec02926a
http://blog.csdn.net/dream_broken/article/details/72851329

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Spring Boot 集成Mybatis实现多数据源 的相关文章

随机推荐

  • Python基础入门(五)——关于一个Number类型的小实例

    Number类型数据是我们在python当中经常要用到的数据类型 先上一些小知识点 再上实例 相关知识点 Number类型 Number类型 Number类型的定义和删除 一句话 赋值就是定义 删除要用DEL 定义 var1 1 var2
  • 大语言模型的演进

    大语言模型的演进 借着上次科技树剪枝的话题 大语言模型为人工智能科技树再次剪枝 让我们再来看看大语言模型这个分枝是如何生长的 也是经历6年的Google和OpenAI两家公司几次大战后的结果 第一回合 2017年6月 Google的6500
  • 从代码生成到问题解答,InsCode AI助你成为编程及写作高手

    目录 前言 InsCode AI写作助手 创造更好的写作体验 InsCode AI 创作助手 如何帮助创作者高效创作文章 结论 个人感受 一 你平时会使用这类AI工具吗 你对这类型的工具有什么看法 二 你可以花几分钟体验一下InsCode
  • 新的刷脸支付方式掘起手机支付将会終结

    手机支付将会終结 新的支付方式掘起 新的支付方式对很多人还是很陌生的 这就要很好的推广和布置 现在推出了二代的蜻蜓刷脸设备 向商户销售出了舒缓的二代蜻蜓刷脸支付设备 超市 快餐厅 自动贩卖机都已经实现商业直播 相信很快在每个城市 都会发现这
  • 重载new/delete(C++中的new/delete与operator new/operator delete)

    转 http blog csdn net zhangxiao93 article details 50768025 原文 http www cnblogs com luxiaoxun archive 2012 08 10 2631812 h
  • k8s的dashboard日常操作

    k8s的dashboard日常操作 一 k8s的dashboard介绍 1 dashboard介绍 2 dashboard的功能 集群管理 工作负载 服务发现和负载均衡 存储 配置 日志视图 三 查看集群的所有角色 四 查看集群的命令空间
  • Python数据结构-----递归实现快速排序

    目录 前言 快速排序 1 概念 2 示例 Python代码实现 递归实现快速排序 前言 今天我们就来一起学习快速排序的思想方法 然后通过Python语言来实现快速排序的功能 下面我们就开始今天的学习吧 快速排序 1 概念 快速排序其实是冒泡
  • 使用ggplot2包绘制分组带状图实战

    使用ggplot2包绘制分组带状图实战 在数据可视化中 分组带状图是一种常用的图表类型 可以直观地展示多个组别之间的差异和变化趋势 而R语言中的ggplot2包提供了丰富的绘图函数 其中geom jitter函数可以用来创建分组带状图 下面
  • SpringBoot + Websocket 实现实时聊天

    SpringBoot WebSocket 实现实时聊天 最近有点小时间 上个项目正好用到了websocket实现广播消息来着 现在来整理一下之前的一些代码 分享给大家 WebSocket协议是基于TCP的一种新的网络协议 它实现了浏览器与服
  • ctfshow-web10 with rollup 绕过

    0x00 前言 CTF 加解密合集 CTF Web合集 网络安全知识库 文中工具皆可关注 皓月当空w 公众号 发送关键字 工具 获取 0x01 题目 0x02 Write Up 基本方法 到处点一点 点到取消的时候 突然发现 可以下载一个文
  • C语言中signed和unsigned怎么理解?C语言unsigned long、long long 取值范围???

    C语言中signed和unsigned signed意思为有符号的 也就是第一个位代表正负 剩余的代表大小 例如 signed int 大小区间为 128 127 unsigned意思为无符号的 所有的位都为大小 没有负数 例如 unsig
  • java实现下载excel读取与生成超详细

    背景 没啥背景 就是要做这个功能 创建ExcelUtil工具类 具体导入导出方法如下 excel导入 param inputStream 导入的excel文件 return public static List
  • 酱香咖啡喝了没?用数据分析揭秘瑞幸咖啡的7500万用户增长策略

    瑞幸 X 茅台 这波联名赢麻了 不仅狂卖 542 万杯 甚至带动茅台市值飙升200亿 瑞幸这几年联名搞了不少 又是线条小狗的爱情故事 又是椰树 维密 周大福 足球的 下面老李就从数据分析角度 带大家来看一下近几年 瑞幸咖啡的用户增长策略 是
  • 数据结构笔记之链式栈的基本操作

    include stdio h include stdlib h include io h include math h include time h define OK 1 define ERROR 0 define TRUE 1 def
  • VMWare安装

    1 1 VMWare简介 VMWare是一个虚拟技术的合集 它提供了众多的相关软件 类似于Parallels VMWare是商业应用 而且价格非常的贵 所以 通常我们使用的是网上别人破解的版本 而不是使用官方的正版 VMWare官网 VMw
  • 【UnicodeDecodeError: ‘utf-8‘ codec can‘t decode byte 0xd3 in position 0: invalid continuation byte】

    UnicodeDecodeError utf 8 codec can t decode byte 0xd3 in position 0 invalid continuation byte F jupyter work dir MMLAB m
  • 有一个公网IP,在内网如何架设多台服务器?

    进行内网ip到外网ip的映射 也就是pat 这个工作现在多半由防火墙来完成 不过如果没有防火墙 用路由器也可以完成 只不过会在高峰时加重路由器的负担 思科2600路由可以独立完成各种nat pat但是因为这款产品本身属于低端产品 所以能够担
  • Java8学习记录(一)——Lambda表达式

    这两天看了 Java8实战 做一下记录 目录 一 行为参数化 1 什么是行为参数化 二 函数式接口 1 概念 三 Lambda表达式 四 方法引用 注意点 1 静态方法引用 2 实例方法引用 重点来了 任意类型的实例方法引用 现有对象的实例
  • 【深度学习】树莓派Zero w深度学习模型Python推理

    在机器学习开发过程中 当模型训练好后 接下来就要进行模型推理了 根据部署环境可分为三类场景 边缘计算 一般指手机 嵌入式设备 直接在数据生成的设备上进行推理 因为能避免将采集到的数据上传到云端 所以实时性非常好 端计算 介于云和边缘设备之间
  • Spring Boot 集成Mybatis实现多数据源

    总体来说多数据源配置有两种方式 一种是静态的 一种是动态的 静态的方式 我们以两套配置方式为例 在项目中有两套配置文件 两套mapper 两套SqlSessionFactory 各自处理各自的业务 这个两套mapper都可以进行增删改查的操