SpringBoot 多数据源及事务解决方案

1. 背景

一个主库和N个应用库的数据源,并且会同时操作主库和应用库的数据,需要解决以下两个问题:

  • 如何动态管理多个数据源以及切换?

  • 如何保证多数据源场景下的数据一致性(事务)?

本文主要探讨这两个问题的解决方案,希望能对读者有一定的启发。

2. 数据源切换原理

通过扩展Spring提供的抽象类AbstractRoutingDataSource,可以实现切换数据源。其类结构如下图所示:

图片

  • targetDataSources&defaultTargetDataSource

项目上需要使用的所有数据源和默认数据源。

  • resolvedDataSources&resolvedDefaultDataSource

当Spring容器创建AbstractRoutingDataSource对象时,通过调用afterPropertiesSet复制上述目标数据源。由此可见,一旦数据源实例对象创建完毕,业务无法再添加新的数据源。

  • determineCurrentLookupKey

此方法为抽象方法,通过扩展这个方法来实现数据源的切换。目标数据源的结构为:Map<Object, DataSource>其key为lookup key

我们来看官方对这个方法的注释:

图片

lookup key通常是绑定在线程上下文中,根据这个key去resolvedDataSources中取出DataSource。

根据目标数据源的管理方式不同,可以使用基于配置文件和数据库表两种方式。基于配置文件管理方案无法后续添加新的数据源,而基于数据库表方案管理,则更加灵活。关注公z号:码猿技术专栏,回复关键词:1111 获取阿里内部Java性能调优手册!

3. 配置文件解决方案

根据上面的分析,我们可以按照下面的步骤去实现:

  • 定义DynamicDataSource类继承AbstractRoutingDataSource,重写determineCurrentLookupKey()方法。

  • 配置多个数据源注入targetDataSourcesdefaultTargetDataSource,通过afterPropertiesSet()方法将数据源写入resolvedDataSourcesresolvedDefaultDataSource

  • 调用AbstractRoutingDataSourcegetConnection()方法时,determineTargetDataSource()方法返回DataSource执行底层的getConnection()

其流程如下图所示:

图片

3.1 创建数据源

DynamicDataSource数据源的注入,目前业界主流实现步骤如下:

在配置文件中定义数据源

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driverClassName=com.mysql.jdbc.Driver
# 主数据源
spring.datasource.druid.master.url=jdbcUrl
spring.datasource.druid.master.username=***
spring.datasource.druid.master.password=***
# 其他数据源
spring.datasource.druid.second.url=jdbcUrl
spring.datasource.druid.second.username=***
spring.datasource.druid.second.password=***

在代码中配置Bean

@Configuration
public class DynamicDataSourceConfig {@Bean@ConfigurationProperties("spring.datasource.druid.master")public DataSource firstDataSource(){return DruidDataSourceBuilder.create().build();}@Bean@ConfigurationProperties("spring.datasource.druid.second")public DataSource secondDataSource(){return DruidDataSourceBuilder.create().build();}@Bean@Primarypublic DynamicDataSource dataSource(DataSource firstDataSource, DataSource secondDataSource) {Map<Object, Object> targetDataSources = new HashMap<>(5);targetDataSources.put(DataSourceNames.FIRST, firstDataSource);targetDataSources.put(DataSourceNames.SECOND, secondDataSource);return new DynamicDataSource(firstDataSource, targetDataSources);}
}

3.2 AOP处理

通过DataSourceAspect切面技术来简化业务上的使用,只需要在业务方法添加@SwitchDataSource注解即可完成动态切换:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface SwitchDataSource {String value();
}

DataSourceAspect拦截业务方法,更新当前线程上下文DataSourceContextHolder中存储的key,即可实现数据源切换。

3.3 方案不足

基于AbstractRoutingDataSource的多数据源动态切换,有个明显的缺点,无法动态添加和删除数据源。在我们的产品中,不能把应用数据源写死在配置文件。接下来分享一下基于数据库表的实现方案。

4. 数据库表解决方案

我们需要实现可视化的数据源管理,并实时查看数据源的运行状态。所以我们不能把数据源全部配置在文件中,应该将数据源定义保存到数据库表。参考AbstractRoutingDataSource的设计思路,实现自定义数据源管理。

4.1 设计数据源表

主库的数据源信息仍然配置在项目配置文件中,应用库数据源配置参数,则设计对应的数据表。表结构如下所示:

图片

这个表主要就是DataSource的相关配置参数,其相应的ORM操作代码在此不再赘述,主要是实现数据源的增删改查操作。

4.2 自定义数据源管理

4.2.1 定义管理接口

通过继承AbstractDataSource即可实现DynamicDataSource。为了方便对数据源进行操作,我们定义一个接口DataSourceManager,为业务提供操作数据源的统一接口。

public interface DataSourceManager {void put(String var1, DataSource var2);DataSource get(String var1);Boolean hasDataSource(String var1);void remove(String var1);void closeDataSource(String var1);Collection<DataSource> all();
}

该接口主要是对数据表中定义的数据源,提供基础管理功能。

4.2.2 自定义数据源

DynamicDataSource的实现如下图所示:

图片

根据前面的分析,AbstractRoutingDataSource是在容器启动的时候,执行afterPropertiesSet注入数据源对象,完成之后无法对数据源进行修改。DynamicDataSource则实现DataSourceManager接口,可以将数据表中的数据源加载到dataSources。

4.2.3 切面处理

这一块的处理跟配置文件数据源方案处理方式相同,都是通过AOP技术切换lookup key。

public DataSource determineTargetDataSource() {String lookupKey = DataSourceContextHolder.getKey();DataSource dataSource = Optional.ofNullable(lookupKey).map(dataSources::get).orElse(defaultDataSource);if (dataSource == null) {throw new IllegalStateException("Cannot determine DataSource for lookup key [" + lookupKey + "]");}return dataSource;}
4.2.4 管理数据源状态

在项目启动的时候,加载数据表中的所有数据源,并执行初始化。初始化操作主要是使用SpringBoot提供的DataSourceBuilder类,根据数据源表的定义创建DataSource。在项目运行过程中,可以使用定时任务对数据源进行保活,为了提升性能再添加一层缓存。

图片

AbstractRoutingDataSource 只支持单库事务,切换数据源是在开启事务之前执行。Spring使用 DataSourceTransactionManager进行事务管理。开启事务,会将数据源缓存到DataSourceTransactionObject对象中,后续的commit和 rollback事务操作实际上是使用的同一个数据源。

如何解决切库事务问题?借助Spring的声明式事务处理,我们可以在多次切库操作时强制开启新的事务:

@SwitchDataSource    
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)

这样的话,执行切库操作的时候强制启动新事务,便可实现多次切库而且事务能够生效。但是这种事务方式,存在数据一致性问题:

图片

假若ServiceB正常执行提交事务,接着返回ServiceA执行并且发生异常。因为两次处理是不同的事务,ServiceA这个事务执行回滚,而ServiceA事务已经提交。这样的话,数据就不一致了。接下来,我们主要讨论如何解决多库的事务问题。

6. 多库事务处理

6.1 关于事务的理解

首先有必要理解事务的本质。

1.提到Spring事务,就离不开事务的四大特性和隔离级别、七大传播特性。

事务特性和离级别是属于数据库范畴。Spring事务的七大传播特性是什么呢?它是Spring在当前线程内,处理多个事务操作时的事务应用策略,数据库事务本身并不存在传播特性。

图片

2.Spring事务的定义包括:begin、commit、rollback、close、suspend、resume等动作。

  • begin(事务开始): 可以认为存在于数据库的命令中,比如Mysql的start transaction命令,但是在JDBC编程方式中不存在。

  • close(事务关闭): Spring事务的close()方法,是把Connection对象归还给数据库连接池,与事务无关。

  • suspend(事务挂起): Spring中事务挂起的语义是:需要新事务时,将现有的Connection保存起来(还有尚未提交的事务),然后创建新的Connection2Connection2提交、回滚、关闭完毕后,再把Connection1取出来继续执行。

  • resume(事务恢复): 嵌套事务执行完毕,返回上层事务重新绑定连接对象到事务管理器的过程。

实际上,只有commit、rollback、close是在JDBC真实存在的,而其他动作都是应用的语意,而非JDBC事务的真实命令。因此,事务真实存在的方法是:setAutoCommit()commit()rollback()

close()语义为:

  • 关闭一个数据库连接,这已经不再是事务的方法了。

使用DataSource并不会执行物理关闭,只是归还给连接池。

6.2 自定义管理事务

为了保证在多个数据源中事务的一致性,我们可以手动管理Connetion的事务提交和回滚。考虑到不同ORM框架的事务管理实现差异,要求实现自定义事务管理不影响框架层的事务。

这可以通过使用装饰器设计模式,对Connection进行包装重写commit和rolllback屏蔽其默认行为,这样就不会影响到原生Connection和ORM框架的默认事务行为。其整体思路如下图所示:

图片

这里并没有使用前面提到的@SwitchDataSource,这是因为我们在TransactionAop中已经执行了lookupKey的切换。

6.2.1 定义多事务注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MultiTransaction {String transactionManager() default "multiTransactionManager";// 默认数据隔离级别,随数据库本身默认值IsolationLevel isolationLevel() default IsolationLevel.DEFAULT;// 默认为主库数据源String datasourceId() default "default";// 只读事务,若有更新操作会抛出异常boolean readOnly() default false;

业务方法只需使用该注解即可开启事务,datasourceId指定事务用到的数据源,不指定默认为主库。

6.2.3 包装Connection

自定义事务我们使用包装过的Connection,屏蔽其中的commit&rollback方法。这样我们就可以在主事务里进行统一的事务提交和回滚操作。

public class ConnectionProxy implements Connection {private final Connection connection;public ConnectionProxy(Connection connection) {this.connection = connection;}@Overridepublic void commit() throws SQLException {// connection.commit();}public void realCommit() throws SQLException {connection.commit();}@Overridepublic void close() throws SQLException {//connection.close();}public void realClose() throws SQLException {if (!connection.getAutoCommit()) {connection.setAutoCommit(true);}connection.close();}@Overridepublic void rollback() throws SQLException {if(!connection.isClosed())connection.rollback();}...
}

这里commit&close方法不执行操作,rollback执行的前提是连接执行close才生效。这样不管是使用哪个ORM框架,其自身事务管理都将失效。事务的控制就交由MultiTransaction控制了。

6.2.4 事务上下文管理
public class TransactionHolder {// 是否开启了一个MultiTransactionprivate boolean isOpen;// 是否只读事务private boolean readOnly;// 事务隔离级别private IsolationLevel isolationLevel;// 维护当前线程事务ID和连接关系private ConcurrentHashMap<String, ConnectionProxy> connectionMap;// 事务执行栈private Stack<String> executeStack;// 数据源切换栈private Stack<String> datasourceKeyStack;// 主事务IDprivate String mainTransactionId;// 执行次数private AtomicInteger transCount;// 事务和数据源key关系private ConcurrentHashMap<String, String> executeIdDatasourceKeyMap;}

每开启一个事物,生成一个事务ID并绑定一个ConnectionProxy。事务嵌套调用,保存事务ID和lookupKey至栈中,当内层事务执行完毕执行pop。这样的话,外层事务只需在栈中执行peek即可获取事务ID和lookupKey。

6.2.5 数据源兼容处理

为了不影响原生事务的使用,需要重写getConnection方法。当前线程没有启动自定义事务,则直接从数据源中返回连接。

@Overridepublic Connection getConnection() throws SQLException {TransactionHolder transactionHolder = MultiTransactionManager.TRANSACTION_HOLDER_THREAD_LOCAL.get();if (Objects.isNull(transactionHolder)) {return determineTargetDataSource().getConnection();}ConnectionProxy ConnectionProxy = transactionHolder.getConnectionMap().get(transactionHolder.getExecuteStack().peek());if (ConnectionProxy == null) {// 没开跨库事务,直接返回return determineTargetDataSource().getConnection();} else {transactionHolder.addCount();// 开了跨库事务,从当前线程中拿包装过的Connectionreturn ConnectionProxy;}}
6.2.6 切面处理

切面处理的核心逻辑是:维护一个嵌套事务栈,当业务方法执行结束,或者发生异常时,判断当前栈顶事务ID是否为主事务ID。如果是的话这时候已经到了最外层事务,这时才执行提交和回滚。详细流程如下图所示:

图片

package com.github.mtxn.transaction.aop;
@Aspect
@Component
@Slf4j
@Order(99999)
public class MultiTransactionAop {@Pointcut("@annotation(com.github.mtxn.transaction.annotation.MultiTransaction)")public void pointcut() {if (log.isDebugEnabled()) {log.debug("start in transaction pointcut...");}}@Around("pointcut()")public Object aroundTransaction(ProceedingJoinPoint point) throws Throwable {MethodSignature signature = (MethodSignature) point.getSignature();// 从切面中获取当前方法Method method = signature.getMethod();MultiTransaction multiTransaction = method.getAnnotation(MultiTransaction.class);if (multiTransaction == null) {return point.proceed();}IsolationLevel isolationLevel = multiTransaction.isolationLevel();boolean readOnly = multiTransaction.readOnly();String prevKey = DataSourceContextHolder.getKey();MultiTransactionManager multiTransactionManager = Application.resolve(multiTransaction.transactionManager());// 切数据源,如果失败使用默认库if (multiTransactionManager.switchDataSource(point, signature, multiTransaction)) return point.proceed();// 开启事务栈TransactionHolder transactionHolder = multiTransactionManager.startTransaction(prevKey, isolationLevel, readOnly, multiTransactionManager);Object proceed;try {proceed = point.proceed();multiTransactionManager.commit();} catch (Throwable ex) {log.error("execute method:{}#{},err:", method.getDeclaringClass(), method.getName(), ex);multiTransactionManager.rollback();throw ExceptionUtils.api(ex, "系统异常:%s", ex.getMessage());} finally {// 当前事务结束出栈String transId = multiTransactionManager.getTrans().getExecuteStack().pop();transactionHolder.getDatasourceKeyStack().pop();// 恢复上一层事务DataSourceContextHolder.setKey(transactionHolder.getDatasourceKeyStack().peek());// 最后回到主事务,关闭此次事务multiTransactionManager.close(transId);}return proceed;}}

7.总结

本文主要介绍了多数据源管理的解决方案(应用层事务,而非XA二段提交保证),以及对多个库同时操作的事务管理。

需要注意的是,这种方式只适用于单体架构的应用。

因为多个库的事务参与者都是运行在同一个JVM进行。

如果是在微服务架构的应用中,则需要使用分布式事务管理(譬如:Seata)。

最后说一句(求关注!别白嫖!)

如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。

关注公众号:woniuxgg,在公众号中回复:笔记  就可以获得蜗牛为你精心准备的java实战语雀笔记,回复面试、开发手册、有超赞的粉丝福利!

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

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

相关文章

全国植被类型分布数据

引言 全国植被类型分布数据利用 Landsat 卫星数据&#xff08;Landsat TM&#xff0c;ETM和 OLI&#xff09;完成了长时序的地表覆盖变化检测&#xff0c;并结合变化 检测结果实现了逐区域和逐期的地表覆盖动态更新&#xff0c;30米精细植被类型分布数据&#xff0c;共包含 2…

Python爬虫之爬取网页图片

当我们想要下载网页的图片时&#xff0c;发现网页的图片太多了&#xff0c;无从下手&#xff0c;那我们写一个脚本来爬取呗。 这次的脚本是专门针对某个外国网站使用的&#xff0c;因此仅供参考思路。 在测试的过程中&#xff0c;我发现网站使用了发爬虫机制&#xff0c;具体就…

FPGA工程师及其相关岗位招聘~

社区的招聘功能上线之后&#xff0c;许多企业都在上面发布了招聘岗位。 目前有30企业&#xff0c;岗位围绕FPGA工程师&#xff0c;涵盖嵌入式软件工程师、射频工程师、C语言开发、BMC工程师等等&#xff0c;入口放在这里&#xff1a;F学社-全球FPGA技术提升平台 登录账号后&a…

HCIP-Datacom(H12-821)题库补充(3/27)

最新 HCIP-Datacom&#xff08;H12-821&#xff09;完整题库请扫描上方二维码访问&#xff0c;持续更新中。 运行OSPF协议的路由器&#xff0c;所有接口必须属于同一个区域。 A&#xff1a;正确 B&#xff1a;错误 答案&#xff1a;B 解析&#xff1a;OSPF的邻居关系是基于…

Python环境下滚动轴承状态监测与故障诊断(NASA IMS轴承数据集)

智能维护系统IMS)滚动轴承数据是美国辛辛那提大学智能维护系统中心提供的全寿命周期数据&#xff0c;轴上安装了4个轴承。通过摩擦带将转速保持恒定在 2000r/min。包含3个数据集&#xff0c;每个数据集描述了一个测试到失败的实验。其中第一个实验装置的数据采集从 2003 年 10月…

鸿蒙OS开发实例:【工具类封装-emitter组件间通信】

import Emitter from ohos.events.emitter; import pasteboard from ohos.pasteboard; MyEmitterUtil 是一个针对 HarmonyOS 的事件驱动编程封装类&#xff0c;主要用于组件间的通信和数据传递。 使用要求&#xff1a; DevEco Studio 3.1.1 Release 或更高版本API 版本&…

get 请求中传递数组参数

文章目录 问题分析 问题 使用get请求传参时有参数是数组 分析 qs.stringify({ a: [b, c] }, { arrayFormat: indices }) // 输出结果&#xff1a;a[0]b&a[1]c qs.stringify({ a: [b, c] }, { arrayFormat: brackets }) // 输出结果&#xff1a;a[]b&a[]c qs.stringif…

Qt C++ | QTimer经验总结

QTimer Class QTimer类提供重复计时器和单次计时器 头文件: #include <QTimer> qmake: QT += core 继承自: QObject 定时器信号 void timeout() 公共函数 Qt::TimerType 枚举定义了 Qt 中不同类型的定时器。它包含以下值: **Qt::PreciseTimer:**高精度定时器,用…

国内ip地址推荐,畅享网络新体验!

在数字化时代&#xff0c;IP地址不仅是网络连接的基石&#xff0c;也是互联网产业发展的重要标志。国内作为全球互联网市场的重要参与者&#xff0c;拥有众多IP地址资源。虎观代理小二旨在探索并推荐一些国内IP地址&#xff0c;分析它们的价值所在&#xff0c;并探讨如何更好地…

hcia datacom课程学习(4):ICMP与ping命令

1.什么是ICMP ICMP是ip协议的一部分&#xff0c;常用的ping命令就是基于icmp协议的。 在防火墙策略中也能看到ICMP&#xff0c;如果将其禁用&#xff0c;那么其他主机就ping不通该主机了 2. ICMP数据报 2.1数据报构成 ICMP协议的报文包含在IP数据报的数据部分&#xff0c; …

[windows]Windows上缩放窗体的两种方式

第一种 直接上图 第二种 第二种方式时&#xff0c;快速拖动时&#xff0c;如果是chrome浏览器套壳软件就会出现拖动部分出现黑边的现象而原生的桌面应用程序则不会出现黑边。这现象在chrome浏览器本身就存在。如图 typora是用electron写的软件存在该问题

相位解包裹前识别有效区域和无效区域(条纹和背景区域区分)

对于不连续场进行相位解包的时候,首先要识别出图象中的哪些部分为有效数据,哪些部分为非有效数据"。这不仅关乎着相位解包算法的速度,更影响着解包算法的精度。因此在解包之前,对有效区域和无效区域的判断必须是首先要做的一件事情。下面就来介绍一下什么是有效区域和…

全国地质灾害点shp崩塌滑坡泥石流空间分布地质灾害详查等数据集

地质灾害是指在自然或者人为因素的作用下形成的&#xff0c;对人类生命财产造成的损失、对环境造成破坏的地质作用或地质现象。地质灾害在时间和空间上的分布变化规律&#xff0c;既受制于自然环境&#xff0c;又与人类活动有关&#xff0c;往往是人类与自然界相互作用的结果。…

LeetCode:1319. 连通网络的操作次数(并查集 Java)

目录 1319. 连通网络的操作次数 题目描述&#xff1a; 实现代码与解析&#xff1a; 并查集 原理思路&#xff1a; 1319. 连通网络的操作次数 题目描述&#xff1a; 用以太网线缆将 n 台计算机连接成一个网络&#xff0c;计算机的编号从 0 到 n-1。线缆用 connections 表示…

基于单片机的自动浇灌系统的设计

本文设计了一款由单片机控制的自动浇灌系统。本设计的硬件电路采用AT89C51单片机作为主控芯片,采用YL-69土壤湿度传感器检测植物的湿度。通过单片机将采集湿度值与设定值分析处理后,控制报警电路和水泵浇灌电路的开启,从而实现植物的自动浇灌。 1 设计目的 随着生活水平的…

怎么批量修改文件名中的一部分?

怎么批量修改文件名中的一部分&#xff1f;批量修改文件名中的一部分文字是我们在处理大量文件时经常需要做的任务之一。这项工作可以极大地提高工作效率&#xff0c;节省宝贵的时间。无论是对于个人用户还是企业组织来说&#xff0c;都是非常实用的技能。首先&#xff0c;批量…

【Hexo + Github 搭建自己的专属博客】

目录 一、前提环境配置 1. 安装Git和NodeJS 2. 安装Hexo 3. 加载主题 4. 修改主题配置 二、搭建博客 1. 将博客部署在GitHub上 2. 写文章并上传 3. 配置一些特效 三、最终成果 ​编辑 一、前提环境配置 1. 安装Git和NodeJS 在 Windows 上使用 Git &#xff0c;可以…

【深度学习】【机器学习】用神经网络进行入侵检测,NSL-KDD数据集,基于机器学习(深度学习)判断网络入侵

文章目录 下载数据集NSL-KDD数据集介绍输入的41个特征输出的含义数据处理&&训练技巧建神经网络&#xff0c;输入41个特征&#xff0c;输出是那种类别的攻击模型训练模型推理写gradio前端界面&#xff0c;用户自己输入41个特征&#xff0c;后端用模型推理计算后显示出是…

Jenkins--在Linux上使用Docker安装

一、Jenkins 简介 Jenkins是一个流行的开源自动化服务器&#xff0c;用于持续集成和持续交付&#xff08;CI/CD&#xff09;。Jenkins的核心功能主要包括以下几点&#xff1a; 持续集成&#xff1a;Jenkins可以监控版本控制系统&#xff08;如Git、SVN&#xff09;中的代码变…

模型训练----加载自己的数据集,train.txt包含“路径,类别”

今天想训练一个restnet&#xff0c;之前我是会用文件夹的方式加载数据集&#xff0c;但是师兄给我的数据是用.txt文件划分的训练集和验证集&#xff0c;知道是要把路径和标签提取出来&#xff0c;搞了半天没搞成&#xff0c;后边找到咋搞了。 点击查看 纯真丁一郎博客点击访问…