后台管理项目的多数据源方案

引言

在互联网开发公司中,往往伴随着业务的快速迭代,程序员可能没有过多的时间去思考技术扩展的相关问题,长久下来导致技术过于单一。为此最近在学习互联网思维,从相对简单的功能开始做总结,比如非常常见的基础数据的后台管理,那么涉及到多数据源的情况又会有哪些问题呢?

思考1:在业务中如何更加灵活方便的切换数据源呢?

思考2:多数据源之间的事务如何保证呢?

思考3:这种多数据源的分布式事务实现思路有哪些?

本篇文章的重点,也就是多数据源问题总结为以下三种方式:

使用Spring提供的AbstractRoutingDataSource

准备工作:

首先AbstractRoutingDataSource是jdbc包提供的,需要引入依赖:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

自定义一个类继承AbstractRoutingDataSource,重写关键方法:

@Component
@Primary
public class DynamicDatasource extends AbstractRoutingDataSource
{public static ThreadLocal<String> dataSourceName = new ThreadLocal<>();@AutowiredDataSource dataSource1;@AutowiredDataSource dataSource2;@Overrideprotected Object determineCurrentLookupKey(){return dataSourceName.get();}@Overridepublic void afterPropertiesSet(){Map<Object, Object> targetDataSources = new HashMap<>();targetDataSources.put("W", dataSource1); //写数据库,主库targetDataSources.put("R", dataSource2); //读数据库,从库super.setTargetDataSources(targetDataSources);super.setDefaultTargetDataSource(dataSource1);super.afterPropertiesSet();}
}

配置多个数据源:

@Configuration
public class DataSourceConfig
{@Bean@ConfigurationProperties(prefix = "spring.datasource.datasource1")public DataSource dataSource1(){return DruidDataSourceBuilder.create().build();}@Bean@ConfigurationProperties(prefix = "spring.datasource.datasource2")public DataSource dataSource2(){return DruidDataSourceBuilder.create().build();}@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;}
}

application.yml参考配置:

spring:datasource:type: com.alibaba.druid.pool.DruidDataSourcedatasource1:url: jdbc:mysql://127.0.0.1:3306/datasource1?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=falseusername: rootpassword: 123666initial-size: 1min-idle: 1max-active: 20test-on-borrow: truedriver-class-name: com.mysql.cj.jdbc.Driverdatasource2:url: jdbc:mysql://127.0.0.1:3306/datasource2?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=falseusername: rootpassword: 123666initial-size: 1min-idle: 1max-active: 20test-on-borrow: truedriver-class-name: com.mysql.cj.jdbc.Driver

min-idle,最小空闲连接,不会被销毁的数据库连接。注意:如果设置的太小,当客户端连接多的情况下,就需要新创建数据库连接,是一个比较耗时的操作,可能导致客户端连接超时。设置太大会占用系统资源

max-active:最大活跃数,官网建议配置:正在使用的数据库连接数 / 配置的这个值 = 85%

test-on-borrow:每次连接时都进行检查,生产上配置为true会影响性能,建议false,默认也是false

test-on-return:每次归还连接时进行检查,同样影响性能同上

生产上建议上面两个参数设置false,testWhileIdle设置为true,间隔一段时间检查连接是否可用:

空闲时间大于timeBetweenEvictionRunsMillis(默认1分钟)检查一次,检查发现连接失效也不会马上删除,而是空闲时间超过minEvictableIdleTimeMillis(最小空闲时间,默认30分钟)自动删除

maxEvictableIdleTimeMillis:最大空闲时间,默认7小时。空闲连接时间过长,数据库就会自动把连接关闭,Druid为了防止从连接池中拿到被数据库关闭的连接,设置了这个参数,超过时间强行关闭连接

使用测试:

方案一,直接在方法中设置数据源标识,简单实现功能,缺点也很明显

@Service
public class FriendServiceImpl implements FriendService
{@AutowiredFriendMapper friendMapper;@Overridepublic List<Friend> list(){DynamicDatasource.dataSourceName.set("R");return friendMapper.list();}@Overridepublic void save(Friend friend){DynamicDatasource.dataSourceName.set("W");friendMapper.save(friend);}
}

方案二,使用自定义注解+AOP实现,适合不同业务的多数据源场景

// 1、自定义注解:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface WR
{String value() default "W";
}// 2、切面配置类
@Component
@Aspect
public class DynamicDataSourceAspect
{@Before("within(com.example.dynamicdatasource.service.impl.*) && @annotation(wr)")public void before(JoinPoint joinPoint, WR wr){String value = wr.value();DynamicDatasource.dataSourceName.set(value);System.out.println(value);}
}// 3、使用注解测试
@Service
public class FriendServiceImpl implements FriendService
{@AutowiredFriendMapper friendMapper;@WR("R")@Overridepublic List<Friend> list(){
//        DynamicDatasource.dataSourceName.set("R");return friendMapper.list();}@WR("W")@Overridepublic void save(Friend friend){
//        DynamicDatasource.dataSourceName.set("W");friendMapper.save(friend);}
}

方案三,使用MyBatis插件,适合相同业务读写分离的业务场景

import com.example.dynamicdatasource.DynamicDatasource;
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 java.util.Properties;@Component
@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
{@Overridepublic Object intercept(Invocation invocation)throws Throwable{Object[] objects = invocation.getArgs();MappedStatement ms = (MappedStatement)objects[0];if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)){DynamicDatasource.dataSourceName.set("R");}else{DynamicDatasource.dataSourceName.set("W");}return invocation.proceed();}@Overridepublic Object plugin(Object target){if (target instanceof Executor){return Plugin.wrap(target, this);}else{return target;}}@Overridepublic void setProperties(Properties properties){}
}

使用MyBatis注册多个sqlSessionFactory

实现思路:Spring集成多个MyBatis框架,指定不同的扫描包、不同的数据源

准备工作:

读库和写库分别添加配置类,扫描不同的包路径:

@Configuration
@MapperScan(basePackages = "com.example.dynamicmybatis.mapper.r", sqlSessionFactoryRef = "rSqlSessionFactory")
public class RMyBatisConfig
{@Bean@ConfigurationProperties(prefix = "spring.datasource.datasource2")public DataSource dataSource2(){return DruidDataSourceBuilder.create().build();}@Beanpublic DataSourceTransactionManager rTransactionManager(){DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();dataSourceTransactionManager.setDataSource(dataSource2());return dataSourceTransactionManager;}@Beanpublic SqlSessionFactory rSqlSessionFactory()throws Exception{final SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();sqlSessionFactoryBean.setDataSource(dataSource2());return sqlSessionFactoryBean.getObject();}@Beanpublic TransactionTemplate rTransactionTemplate(){return new TransactionTemplate(rTransactionManager());}
}@Configuration
@MapperScan(basePackages = "com.example.dynamicmybatis.mapper.w", sqlSessionFactoryRef = "wSqlSessionFactory")
public class WMyBatisConfig
{@Bean@ConfigurationProperties(prefix = "spring.datasource.datasource1")public DataSource dataSource1(){return DruidDataSourceBuilder.create().build();}@Beanpublic DataSourceTransactionManager wTransactionManager(){DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();dataSourceTransactionManager.setDataSource(dataSource1());return dataSourceTransactionManager;}@Beanpublic SqlSessionFactory wSqlSessionFactory()throws Exception{final SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();sqlSessionFactoryBean.setDataSource(dataSource1());return sqlSessionFactoryBean.getObject();}@Beanpublic TransactionTemplate wTransactionTemplate(){return new TransactionTemplate(wTransactionManager());}
}

写库和读库使用两个Mapper类,在不同的包下:

public interface RFriendMapper
{@Select("select * from friend")List<Friend> list();@Insert("insert into friend(name) values(#{name})")void save(Friend friend);
}public interface WFriendMapper
{@Select("select * from friend")List<Friend> list();@Insert("insert into friend(name) values(#{name})")void save(Friend friend);
}

使用测试:

@Service
public class FriendServiceImpl implements FriendService
{@AutowiredRFriendMapper rFriendMapper;@AutowiredWFriendMapper wFriendMapper;@Overridepublic List<Friend> list(){return rFriendMapper.list();}@Overridepublic void save(Friend friend){wFriendMapper.save(friend);}
}

思考:多数据源的事务问题

public void saveW(Friend friend)
{friend.setName("gaoW");wFriendMapper.save(friend);
}public void saveR(Friend friend)
{friend.setName("gaoR");rFriendMapper.save(friend);
}@Transactional
// @Transactional(transactionManager = "wTransactionManager")
@Override
public void saveAll(Friend friend)
{saveW(friend);saveR(friend);int a = 1 / 0;
}

存在多个事务管理器的情况直接使用@Transactional注解是不行的,Spring不知道使用哪个事务管理器会报错。但是指定了事务管理器后,仅当前事务管理器负责的部分支持回滚,还是存在问题。

在特定场景下,直接指定事务管理器名称的方式可以生效(保证数据一致的意思):

@Transactional(transactionManager = "wTransactionManager")
@Override
public void saveAll(Friend friend)
{saveW(friend);saveR(friend);int a = 1 / 0;
}

1、saveW方法内部异常,saveW发生异常事务不提交,数据一致

2、saveR方法内部异常,事务管理器回滚saveW的更新,saveR异常未提交,数据一致

3、saveW和saveR方法中间的业务发生异常,事务管理器回滚saveW的更新,saveR未提交,数据一致

4、saveW和saveR方法后面的业务发生异常,事务管理器回滚saveW的更新,saveR已提交,数据不一致

Spring提供的编程式事务解决方案:

@Service
public class FriendServiceImpl implements FriendService
{@AutowiredRFriendMapper rFriendMapper;@AutowiredWFriendMapper wFriendMapper;@AutowiredTransactionTemplate rTransactionTemplate;@AutowiredTransactionTemplate wTransactionTemplate;public void saveW(Friend friend){friend.setName("gaoW");wFriendMapper.save(friend);}public void saveR(Friend friend){friend.setName("gaoR");rFriendMapper.save(friend);}@Overridepublic void saveAll2(Friend friend){wTransactionTemplate.execute(wstatus -> {rTransactionTemplate.execute(rstatus -> {try{saveW(friend);saveR(friend);
//                    int a = 1 / 0;}catch (Exception e){e.printStackTrace();wstatus.setRollbackOnly();rstatus.setRollbackOnly();return false;}return true;});return true;});}
}

Spring支持的声明式事务解决方案(分布式事务变种实现):

@Service
public class FriendServiceImpl implements FriendService
{@AutowiredRFriendMapper rFriendMapper;@AutowiredWFriendMapper wFriendMapper;@AutowiredTransactionTemplate rTransactionTemplate;@AutowiredTransactionTemplate wTransactionTemplate;@Overridepublic List<Friend> list(){return rFriendMapper.list();}@Overridepublic void save(Friend friend){wFriendMapper.save(friend);}public void saveW(Friend friend){friend.setName("gaoW");wFriendMapper.save(friend);}public void saveR(Friend friend){friend.setName("gaoR");rFriendMapper.save(friend);}@Transactional(transactionManager = "wTransactionManager")@Overridepublic void saveAll1(Friend friend){FriendService friendService = (FriendService)AopContext.currentProxy();friendService.saveAllR(friend);}@Transactional(transactionManager = "rTransactionManager")@Overridepublic void saveAllR(Friend friend){saveW(friend);saveR(friend);
//        int a = 1 / 0;}
}@EnableAspectJAutoProxy(exposeProxy = true) //暴露代理对象
public class DynamicMybatisApplication {

注意:调用saveAllR方式时,需要使用代理对象,直接调用本类的其他方法事务不会生效

@Autowired自动注入自己获取代理对象,这种方式在springboot2.6以后有循环依赖报错,需要改配置,按照错误提示添加配置,设置参数为true即可

上面这两种事务的解决方式适用场景:

只涉及到两三个数据源,并且多数据源事务的场景不多,同时公司又不希望引入其他组件(安全性问题考虑),那么就可以使用这种方式实现分布式事务。当然分布式事务最好的解决方案肯定是通过第三方组件比如Seata

使用dynamic-datasource框架

dynamic-datasource是属于苞米豆生态圈的

基于Springboot的多数据源组件,功能强悍,支持Seata事务

  • 支持数据源分组,适用多库、读写分离、一主多从(实现了负载均衡,轮询/随机)等场景

  • 提供自定义数据源方案,比如从数据库加载

  • 提供项目启动后动态增加和删除数据源方案,可以添加管理后台页面灵活调整

  • 提供MyBatis环境下的纯读写分离方案

  • 提供本地多数据源事务方案

  • 提供基于Seata的分布式事务方案,注意不能和原生spring事务混用

  • 等等

多数据源实现方式:就是通过继承AbstractRoutingDataSource的这种方式

数据源切换:通过AOP+自定义注解实现的

使用示例:

@Service
public class FriendServiceImpl implements FriendService
{@AutowiredFriendMapper friendMapper;@Override@DS("slave")public List<Friend> list(){return friendMapper.list();}@Override@DS("master")@DSTransactionalpublic void save(Friend friend){friendMapper.save(friend);}@DS("master")@DSTransactionalpublic void saveAll(){// 执行多数据源的操作}
}

application.yml参考配置:

spring:datasource:dynamic:#设置默认的数据源或者数据源组,默认值即为masterprimary: master#严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源strict: falsedatasource:master:url: jdbc:mysql://127.0.0.1:3306/datasource1?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=falseusername: rootpassword: 123666initial-size: 1min-idle: 1max-active: 20test-on-borrow: truedriver-class-name: com.mysql.cj.jdbc.Driverslave_1:url: jdbc:mysql://127.0.0.1:3306/datasource2?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=falseusername: rootpassword: 123666initial-size: 1min-idle: 1max-active: 20test-on-borrow: truedriver-class-name: com.mysql.cj.jdbc.Driver

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/593992.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第二百四十三回 再分享一个Json工具

文章目录 1. 概念介绍2. 分析与比较2.1 分析问题2.2 比较差异 3. 使用方法4. 内容总结 我们在上一章回中介绍了"分享三个使用TextField的细节"相关的内容&#xff0c;本章回中将再 分享一个Json插件.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我…

案例087:基于微信小程序的社区养老服务平台设计与实现

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;SSM JDK版本&#xff1a;JDK1.8 数据库&#xff1a;mysql 5.7 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.5.4 小程序框架&#xff1a;uniapp 小程序开发软件&#xff1a;HBuilder X 小程序…

python | PYTHON正则表达式

操作符说明实例.表示任何单个字符[]字符集&#xff0c;对单个字符给出取值范围[abc]表示a、b、c&#xff0c;[a-z]表示a到z单个字符[^ ]非字符集&#xff0c;对单个字符给出排除范围[^abc]表示非a或b或c的单个字符*前一个字符0次或无限次扩充abc* 表示ab&#xff0c;abc&#x…

阿里云免费证书SSL三个月的解决方法

阿里云免费SSL证书签发有效期从12个月缩短至3个月&#xff1a;尊敬的用户&#xff0c;根据供应商变更要求&#xff0c;免费证书&#xff08;默认证书&#xff09;的签发有效期将由12个月缩短至3个月。 免费证书&#xff08;升级证书&#xff09;的有效期不会改变。 没错&#…

Linux iptables实现(SNAT)源地址转换

实验要求一&#xff08;实验要求SNAT:内网主机访问外网主机&#xff0c;通过iptables进行源地址转换&#xff0c;允许访问外网的httpd和ping&#xff09; 1、开启防火墙转发功能&#xff08;两个方法二选一即可&#xff09; 方法一&#xff1a; [rootiptabels ~]#echo net.ipv…

Java 17 中的 Switch 表达式模式匹配与记录类型

Switch 表达式模式匹配 在 Java 17 中&#xff0c;switch 表达式得到了增强&#xff0c;引入了模式匹配&#xff0c;使得代码编写更加简洁。以下是一个简单的例子&#xff1a; package com.lfsun.newswitch;import static com.lfsun.newswitch.ShapeExample.ShapeType.CIRCLE…

小秋SLAM入门实战C++所有文章汇总

文章目录 线程和锁用法 线程和锁用法 C中互斥量、锁有什么用&#xff1f; 创建一个C线程需要传入几个参数&#xff1f; 如何理解和使用C线程循环 C 类 函数 变量 进程 线程 C关于锁和互斥量你真的理解了吗 C 代码中如何使用互斥锁std::mutex和独占锁std::unique_lock 如何更好…

自学路上的绊脚石---没有方向

现在我描述一个目前碰到的问题点&#xff0c;比较困扰我 我觉得我现在的事情特别多&#xff0c; 1.整理十套源码&#xff0c;然后看看能不能买卖看 2.完成自己的博客系统&#xff0c;使用之前的新经资讯的模板&#xff0c;这样才能够融汇贯通 3.继续将爬虫的课程学完&#x…

polar CTF 简单rce

一、题目 <?php /*PolarD&N CTF*/ highlight_file(__FILE__); function no($txt){if(!preg_match("/cat|more|less|head|tac|tail|nl|od|vim|uniq|system|proc_open|shell_exec|popen| /i", $txt)){return $txt;}else{ die("whats up");}} $yyds(…

B端产品经理学习-B端产品系统调研的工具

系统性调研目标的工具 系统性调研的目标 相对于背景调研&#xff0c;系统行调研是对公司可控因素&#xff08;公司内部&#xff09;和直接作用力&#xff08;消费者、竞争者&#xff09;进行的调研。系统性调研需要输出结论&#xff0c;为达成产品或公司的战略目标而制定行动的…

【Java进阶篇】Java中Timer实现定时调度的原理(解析)

Java中Timer实现定时调度的原理 ✔️ 引言✔️JDK 中Timer类的定义✔️拓展知识仓✔️优缺点 ✔️ 引言 Java中的Timer类是用于计划执行一项任务一次或重复固定延迟执行的简单工具。它使用一个名为TaskQueue的内部类来存储要执行的任务&#xff0c;这些任务被封装为TimerTask对…

Python搭建代理IP池实现存储IP的方法

目录 前言 1. 介绍 2. IP存储方法 2.1 存储到数据库 2.2 存储到文件 2.3 存储到内存 3. 完整代码示例 总结 前言 代理IP池是一种常用的网络爬虫技术&#xff0c;可以用于反爬虫、批量访问目标网站等场景。本文介绍了使用Python搭建代理IP池&#xff0c;并实现IP存储的…

三菱结构化While指令的使用

最近在交流群中&#xff0c;有人就while指令使用错误进行了讨论&#xff0c;问题的总的原因是对While指令理解不到位导致&#xff0c;PLC看门狗报错&#xff01; 错误使用While指令导致看门狗报错 下面就While指令的使用进行说明 WHILE语句。 WHILE语句执行时首先检测条件。…

Go到底能做什么?不能做什么?

首先&#xff0c;让我表达一下我对Golang的喜爱。作为一名科技博主和程序员&#xff0c;我个人非常喜欢Golang&#xff0c;主要有以下几点原因&#xff1a; 1、简洁易用&#xff1a;Go语言非常简洁&#xff0c;没有繁杂的语法&#xff0c;读起来非常流畅。同时&#xff0c;它的…

SQL SELECT DISTINCT 语句

SELECT DISTINCT 语句用于返回唯一不同的值。 SQL SELECT DISTINCT 语句 在表中&#xff0c;一个列可能会包含多个重复值&#xff0c;有时您也许希望仅仅列出不同&#xff08;distinct&#xff09;的值。 DISTINCT 关键词用于返回唯一不同的值。 SQL SELECT DISTINCT 语法 …

成为一名合格的前端架构师,前端知识技能与项目实战教学

一、教程描述 本套前端架构师教程&#xff0c;大小35.94G&#xff0c;共有672个文件。 二、教程目录 01.node介绍和环境配置&#xff08;共6课时&#xff09; 02.ES6语法&#xff08;共5课时&#xff09; 03.node基础&#xff08;共29课时&#xff09; 04.Express框架&am…

大语言模型LLM微调技术:P-Tuning

1 引言 Bert时代&#xff0c;我们常做预训练模型微调&#xff08;Fine-tuning&#xff09;&#xff0c;即根据不同下游任务&#xff0c;引入各种辅助任务loss和垂直领域数据&#xff0c;将其添加到预训练模型中&#xff0c;以便让模型更加适配下游任务的方式。每个下游任务都存…

阿里云免费SSL证书有效期3个月有什么解决方法?

阿里云免费SSL证书签发有效期从12个月缩短至3个月&#xff1a;尊敬的用户&#xff0c;根据供应商变更要求&#xff0c;免费证书&#xff08;默认证书&#xff09;的签发有效期将由12个月缩短至3个月。 免费证书&#xff08;升级证书&#xff09;的有效期不会改变。 没错&#…

Redis:原理速成+项目实战——Redis实战4(解决Redis缓存穿透、雪崩、击穿)

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位大四、研0学生&#xff0c;正在努力准备大四暑假的实习 &#x1f30c;上期文章&#xff1a;Redis&#xff1a;原理项目实战——Redis实战3&#xff08;Redis缓存最佳实践&#xff08;问题解析高级实现&#xff09;&#x…

计算器——可支持小数的任意四则运算(中缀表达式转为后缀表达式算法)

中缀表达式转为后缀表达式的原理过程主要包括以下步骤&#xff1a; 1. 初始化两个栈&#xff0c;一个用于存储操作数&#xff0c;一个用于存储运算符。2. 从左到右扫描中缀表达式的每个字符。3. 如果遇到数字&#xff0c;则直接将其压入操作数栈。4. 如果遇到运算符&#xff0c…