Springboot+mybatis-plus+dynamic-datasource+Druid 多数据源 分布式事务

Springboot+mybatis-plus+dynamic-datasource+Druid 多数据源事务,分布式事务

文章目录

  • Springboot+mybatis-plus+dynamic-datasource+Druid 多数据源事务,分布式事务
  • 0.前言
  • 1. 基础介绍
      • ConnectionFactory
      • AbstractRoutingDataSource 动态路由数据源的抽象类
    • DynamicLocalTransactionInterceptor 动态的本地事务拦截器
  • 3. 使用步骤示例
  • 4. 官方源码分析
  • 5. 参考资料

0.前言

背景
处理多数据源事务一直是一个复杂而棘手的问题,通常我们有两种主流的解决方法。

第一种是通过Atomikos手动创建多数据源事务,这种方法更适合数据源数量较少,参数配置不复杂,对性能要求不高的项目。然而,这种方法的最大困难在于需要手动配置大量设置,这可能会消耗大量时间。


第二种是通过使用Seata等分布式事务解决方案。这种方法的难点在于需要建立并维护像Seata-server这样的统一管理中心。

今天我们使用Springboot+mybatis-plus+dynamic-datasource+Druid 多数据源 实现分布式事务和本地多数据源事务。
每种解决方案都有其适用的场景,然而在实际操作中,我经常接到如下的问题:
“我为什么在添加了事务注解之后,数据源切换还是失败了?”
“我了解到这涉及到分布式事务,但我并不想使用Seata。我的场景比较简单,有没有不需要依赖第三方的解决方案?”
这些问题突显出在现实工作中,我们可能需要更灵活、更简便的解决方案来处理多数据源事务问题。
在这里插入图片描述
在这里插入图片描述

1. 基础介绍

自从3.3.0开始,由seata的核心贡献者https://github.com/a364176773 贡献了基于connection代理的方案。
完整代码 https://github.com/baomidou/dynamic-datasource-spring-boot-starter/commit/f0cbad193528296eeb64faa76c79743afbdd811d
建议从3.4.0版本开始使用,其修复了一个功能,老版本不加@DS只加@DSTransactional会报错。
在这里插入图片描述
核心的几处代码

    @Role(value = BeanDefinition.ROLE_INFRASTRUCTURE)@ConditionalOnProperty(prefix = DynamicDataSourceProperties.PREFIX, name = "seata", havingValue = "false",matchIfMissing = true)@Beanpublic Advisor localTransactionAdvisor() {AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();pointcut.setExpression("@annotation(com.baomidou.dynamic.datasource.annotation.DSTransactional)");return new DefaultPointcutAdvisor(pointcut, new DynamicTransactionAdvisor());}

我们可以看到通过spring.datasource.dynamic.seata=true配置来启用条件注解。这个是dynamic-datasource支持seata事务的开发和入口。

ConnectionFactory

ConnectionFactory 是一个工厂类,主要的作用是管理数据库连接,并提供获取和存储数据库连接的功能。

  1. 存储每个线程独立的数据库连接:ConnectionFactory 使用 ThreadLocal 为每个线程提供其自己的数据库连接池,这样可以防止在多线程环境中数据库连接的混乱。

  2. 提供获取数据库连接的方法:ConnectionFactory 提供 getConnection 方法,使得在同一个线程中的多个模块可以共享同一个数据库连接。

  3. 提供存储数据库连接的方法:ConnectionFactory 提供 putConnection 方法,可以存储新的数据库连接到当前线程的数据库连接池中。

  4. 提供通知数据库连接的方法:ConnectionFactory 提供 notify 方法,可以对当前线程的所有数据库连接进行统一的操作,比如提交或者回滚事务。

通过这些功能,ConnectionFactory 实现了数据库连接的有效管理,保证了在同一线程中对多个数据库进行操作时,可以共享同一连接,实现事务管理。核心代码如下。大家可以借鉴

package com.baomidou.dynamic.datasource.tx;import java.sql.SQLException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;/*** @author funkye*/
public class ConnectionFactory {// 使用ThreadLocal来保存与当前线程相关的数据库连接信息,以Map形式存储,Map中的key为数据源名称,value为对应的数据库连接代理类private static final ThreadLocal<Map<String, ConnectionProxy>> CONNECTION_HOLDER =new ThreadLocal<Map<String, ConnectionProxy>>() {@Overrideprotected Map<String, ConnectionProxy> initialValue() {return new ConcurrentHashMap<>(8);}};// 存储数据库连接到当前线程的连接池中,如果当前线程的连接池中没有该数据源的连接,则新建一个并放入public static void putConnection(String ds, ConnectionProxy connection) {Map<String, ConnectionProxy> concurrentHashMap = CONNECTION_HOLDER.get();if (!concurrentHashMap.containsKey(ds)) {try {connection.setAutoCommit(false);} catch (SQLException e) {e.printStackTrace();}concurrentHashMap.put(ds, connection);}}// 从当前线程的连接池中获取指定数据源的数据库连接public static ConnectionProxy getConnection(String ds) {return CONNECTION_HOLDER.get().get(ds);}// 对当前线程的所有数据库连接执行通知操作,根据参数state决定是提交还是回滚,如果在执行过程中发生错误,则在所有连接处理完后抛出public static void notify(Boolean state) throws Exception {Exception exception = null;try {Map<String, ConnectionProxy> concurrentHashMap = CONNECTION_HOLDER.get();for (ConnectionProxy connectionProxy : concurrentHashMap.values()) {try {connectionProxy.notify(state);} catch (SQLException e) {exception = e;}}} finally {CONNECTION_HOLDER.remove(); //清除当前线程的连接池if (exception != null) {throw exception;}}}}

AbstractRoutingDataSource 动态路由数据源的抽象类

动态路由数据源的抽象类,用于根据不同的业务需要,动态地选择需要使用的数据源。关键的方法是getConnection()getConnection(String username, String password),这两个方法会根据当前是否存在全局事务来动态地选择获取原始的数据库连接还是数据库连接代理。

public abstract class AbstractRoutingDataSource extends AbstractDataSource {// 抽象方法,子类需要实现该方法以确定数据源protected abstract DataSource determineDataSource();// 抽象方法,子类需要实现该方法以确定默认的数据源名称protected abstract String getPrimary();// 获取数据库连接,根据事务上下文中是否有XID来判断是否需要获取代理连接@Overridepublic Connection getConnection() throws SQLException {String xid = TransactionContext.getXID();if (StringUtils.isEmpty(xid)) {// 如果没有XID,说明当前不处于全局事务中,直接获取原始连接return determineDataSource().getConnection();} else {// 如果有XID,说明当前处于全局事务中,需要获取代理连接String ds = DynamicDataSourceContextHolder.peek();ds = StringUtils.isEmpty(ds) ? getPrimary() : ds;ConnectionProxy connection = ConnectionFactory.getConnection(ds);return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection()) : connection;}}// 与上面的方法类似,只不过这个方法可以传入用户名和密码来获取数据库连接@Overridepublic Connection getConnection(String username, String password) throws SQLException {String xid = TransactionContext.getXID();if (StringUtils.isEmpty(xid)) {return determineDataSource().getConnection(username, password);} else {String ds = DynamicDataSourceContextHolder.peek();ds = StringUtils.isEmpty(ds) ? getPrimary() : ds;ConnectionProxy connection = ConnectionFactory.getConnection(ds);return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection(username, password)): connection;}}// 创建数据库连接代理,并将代理连接放入连接工厂private Connection getConnectionProxy(String ds, Connection connection) {ConnectionProxy connectionProxy = new ConnectionProxy(connection, ds);ConnectionFactory.putConnection(ds, connectionProxy);return connectionProxy;}// 获取指定类型的代理对象@Override@SuppressWarnings("unchecked")public <T> T unwrap(Class<T> iface) throws SQLException {if (iface.isInstance(this)) {return (T) this;}return determineDataSource().unwrap(iface);}// 判断是否是指定类型的代理对象@Overridepublic boolean isWrapperFor(Class<?> iface) throws SQLException {return (iface.isInstance(this) || determineDataSource().isWrapperFor(iface));}
}

DynamicLocalTransactionInterceptor 动态的本地事务拦截器

动态的本地事务拦截器。基本思想是在方法调用前后添加事务处理的逻辑。当这个拦截器被应用到某个方法时,那么在调用这个方法时,会首先检查当前是否已经存在事务,如果存在则直接调用原始方法。如果不存在,则会先开启一个新的事务,然后调用原始方法,方法结束后根据方法执行的结果来提交或回滚事务。入口在这,看一眼就懂了。
在这里插入图片描述

// 实现MethodInterceptor接口定义拦截器
public class DynamicLocalTransactionInterceptor implements MethodInterceptor {@Override// invoke方法会在原方法执行前后进行拦截public Object invoke(MethodInvocation methodInvocation) throws Throwable {// 如果当前上下文中已存在事务,则直接调用原方法,不进行拦截处理if (!StringUtils.isEmpty(TransactionContext.getXID())) {return methodInvocation.proceed();}// 定义一个状态标志,标记事务是否执行成功boolean state = true;Object o;// 开启一个新的事务LocalTxUtil.startTransaction();try {// 调用原始方法o = methodInvocation.proceed();} catch (Exception e) {// 如果原方法执行抛出异常,则标记事务执行失败state = false;throw e;} finally {// 根据事务执行状态,提交或回滚事务if (state) {LocalTxUtil.commit();} else {LocalTxUtil.rollback();}}// 返回原方法的执行结果return o;}
}

3. 使用步骤示例

官方示例:https://github.com/dynamic-datasource/dynamic-datasource-samples/tree/master/tx-samples/tx-local-sample

完整示例项目 数据库都已准备好,可以直接运行测试。http://localhost:8080/doc.html

示例项目A,B,C分别对应OrderService,ProductService,AccountService。分别是独立的数据库。

用户下单分别调用产品库扣库存,账户库扣余额。
如果库存不足,或用户余额不足都抛出RuntimeException,触发整体回滚。

@Slf4j
@Service
@AllArgsConstructor
public class OrderService {private final OrderMapper orderMapper;private final AccountService accountService;private final ProductService productService;//@DS("order") 这里不需要,因为order是默认库,如果开启事务的不是默认库则必须加@DSTransactional //注意这里开启事务public void placeOrder(PlaceOrderRequest request) {log.info("=============ORDER START=================");Long userId = request.getUserId();Long productId = request.getProductId();Integer amount = request.getAmount();log.info("收到下单请求,用户:{}, 商品:{},数量:{}", userId, productId, amount);log.info("当前 XID: {}", TransactionContext.getXID());Order order = Order.builder().userId(userId).productId(productId).status(OrderStatus.INIT).amount(amount).build();orderMapper.insert(order);log.info("订单一阶段生成,等待扣库存付款中");// 扣减库存并计算总价Double totalPrice = productService.reduceStock(productId, amount);// 扣减余额accountService.reduceBalance(userId, totalPrice);order.setStatus(OrderStatus.SUCCESS);order.setTotalPrice(totalPrice);orderMapper.updateById(order);log.info("订单已成功下单");log.info("=============ORDER END=================");}
}
@Slf4j
@Service
@RequiredArgsConstructor
public class ProductService {private final ProductMapper productMapper;@DS("product")public Double reduceStock(Long productId, Integer amount) {log.info("=============PRODUCT START=================");log.info("当前 XID: {}", TransactionContext.getXID());// 检查库存Product product = productMapper.selectById(productId);Assert.notNull(product, "商品不存在");Integer stock = product.getStock();log.info("商品编号为 {} 的库存为{},订单商品数量为{}", productId, stock, amount);if (stock < amount) {log.warn("商品编号为{} 库存不足,当前库存:{}", productId, stock);throw new RuntimeException("库存不足");}log.info("开始扣减商品编号为 {} 库存,单价商品价格为{}", productId, product.getPrice());// 扣减库存int currentStock = stock - amount;product.setStock(currentStock);productMapper.updateById(product);double totalPrice = product.getPrice() * amount;log.info("扣减商品编号为 {} 库存成功,扣减后库存为{}, {} 件商品总价为 {} ", productId, currentStock, amount, totalPrice);log.info("=============PRODUCT END=================");return totalPrice;}
}
@Slf4j
@Service
@RequiredArgsConstructor
public class AccountService {private final AccountMapper accountMapper;@DS("account")public void reduceBalance(Long userId, Double price) {log.info("=============ACCOUNT START=================");log.info("当前 XID: {}", TransactionContext.getXID());Account account = accountMapper.selectById(userId);Assert.notNull(account, "用户不存在");Double balance = account.getBalance();log.info("下单用户{}余额为 {},商品总价为{}", userId, balance, price);if (balance < price) {log.warn("用户 {} 余额不足,当前余额:{}", userId, balance);throw new RuntimeException("余额不足");}log.info("开始扣减用户 {} 余额", userId);double currentBalance = account.getBalance() - price;account.setBalance(currentBalance);accountMapper.updateById(account);log.info("扣减用户 {} 余额成功,扣减后用户账户余额为{}", userId, currentBalance);log.info("=============ACCOUNT END=================");}
}

4. 官方源码分析

5. 参考资料

  1. dynamic-datasource GitHub 仓库 ↗:dynamic-datasource 的官方 GitHub 仓库,包含源代码、文档和示例等资源。

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

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

相关文章

C++中数组作为参数进行传递方法

文章目录 基础&#xff1a;数组作为函数形参示例&#xff1a;1、一维数组的传递&#xff08;1&#xff09;直接传递&#xff08;2&#xff09;指针传递&#xff08;3&#xff09;引用传递 2、二维数组的传递&#xff08;1&#xff09;直接传递&#xff08;2&#xff09;指针传递…

华为云Stack的学习(二)

三、华为云Stack产品组件 FunsionSphere CPS 提供云平台的基础管理和业务资源&#xff08;包括计算资源和存储资源&#xff09;。采用物理服务器方式部署在管理节点。可以做集群的配置&#xff0c;扩容和运维管理。 Service OM 提供云服务的运维能力&#xff0c;采用虚拟化方…

【Java架构-包管理工具】-Maven进阶(二)

本文摘要 Maven作为Java后端使用频率非常高的一款依赖管理工具&#xff0c;在此咱们由浅入深&#xff0c;分三篇文章&#xff08;Maven基础、Maven进阶、私服搭建&#xff09;来深入学习Maven&#xff0c;此篇为开篇主要介绍Maven进阶知识&#xff0c;包含坐标、依赖、仓库、生…

网络地址转换NAT-动态NAT的使用范围和配置-思科EI,华为数通

网络地址转换NAT-动态NAT的使用范围和配置 什么是动态NAT&#xff1f; 使用公有地址池&#xff0c;并以先到先得的原则分配这些地址。当具有私有 IP 地址的主机请求访问 Internet 时&#xff0c;动态 NAT 从地址池中选择一个未被其它主机占用的 IP 地址一对一的转化。当数据会话…

Go几种读取配置文件的方式

比较有名的方案有 使用viper管理配置[1] 支持多种配置文件格式&#xff0c;包括 JSON,TOML,YAML,HECL,envfile&#xff0c;甚至还包括Java properties 支持为配置项设置默认值 可以通过命令行参数覆盖指定的配置项 支持参数别名 viper[2]按照这个优先级&#xff08;从高到低&am…

Linux驱动 - 20230828

练习. 驱动代码&#xff1a; #include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/slab.h> #include <linux/uaccess.h> #include <…

Viobot硬件组成和接口

Viobot主要由主板、传感器板和外壳组成。具体尺寸可以在用户手册上面找到。 传感器板上面是双目摄像头和IMU&#xff0c;摄像头已经打了胶固定在外壳的前脸&#xff0c;由于涉及到传感器外参标定&#xff0c;所以不可自行拆卸。 LED补光灯版本&#xff1a; TOF版本&#xff1a…

2023-8-28 n-皇后问题

题目链接&#xff1a;n-皇后问题 第一种搜索顺序 #include <iostream>using namespace std;const int N 20;int n; char g[N][N]; bool row[N], col[N], dg[N], udg[N];void dfs(int x, int y, int s) {if(y n) y 0, x ;if(x n){if(s n){for(int i 0; i < n;…

【 Python 全栈开发 - 人工智能篇 - 45 】集成算法与聚类算法

文章目录 一、集成算法1.1 概念1.2 常用集成算法1.2.1 Bagging1.2.2 Boosting1.2.2.1 AdaBoost1.2.2.2 GBDT1.2.2.3 XgBoost 1.2.3 Stacking 二、聚类算法2.1 概念2.2 常用聚类算法2.2.1 K-means2.2.2 层次聚类2.2.3 DBSCAN算法2.2.4 AP聚类算法2.2.5 高斯混合模型聚类算法 一、…

第三方系统访问微搭低代码的后端API

微搭低代码使用的是文档型数据库&#xff0c;有时候会有数据集成的需求&#xff0c;将低代码的数据库中的数据抽取到第三方系统中。本篇就介绍一下本地抽取数据以及云函数中抽取数据 1 本地抽取数据 本地抽取数据我们以nodejs作为后端服务&#xff0c;本机要求先安装好nodejs…

构建数据可视化(基于Echarts,python)

构建数据可视化&#xff08;基于Echarts,python) 本文目录&#xff1a; 一、写在前面的题外话 二、数据可视化概念 三、用Python matplotlib库绘制数据可视化图 四、基于Echarts构建大数据可视化 4.1、安装echarts.js 4.2、数据可视化折线图制作 4.2.1、基础折线图 4.2…

JWT-Token

一、JWT 需要在 HTTP 这种无状态的机制下&#xff0c;记录下&#xff08;标识&#xff09;出来是不是连续&#xff08;逻辑上的连续&#xff09;的请求。 思路&#xff1a;如果多次请求&#xff0c;携带了相同的标识型数据&#xff0c;则认为是逻辑上连续的。这个标识&#xff…

【数据结构】 栈与队列的相互实现

文章目录 &#x1f30f;引言&#x1f340;[队列实现栈](https://leetcode.cn/problems/implement-stack-using-queues/)&#x1f431;‍&#x1f3cd;题目描述&#xff1a;&#x1f4cc;注意事项&#xff1a;&#x1f4cc;示例与提示&#xff1a; &#x1f431;‍&#x1f409;…

云计算在线实训系统建设方案

一、 人工智能与云计算系统概述 人工智能&#xff08;Artificial Intelligence&#xff0c;简称AI&#xff09;是一种模拟人类智能的科学和工程&#xff0c;通过使用计算机系统来模拟、扩展和增强人类的智能能力。人工智能涉及多个领域&#xff0c;包括机器学习、深度学习、自然…

2023全国大学生数学建模竞赛B题思路模型代码

目录 一.思路模型见文末名片&#xff0c;比赛开始9.7晚上第一时间更新 二.国赛常用算法之随机森林 3.思路获取见此 一.思路模型见文末名片&#xff0c;比赛开始9.7晚上第一时间更新 二.国赛常用算法之随机森林 # -*- coding: utf-8 -*- """ author: Administ…

实用调试技巧(1)

文章目录 目录1. 什么是bug&#xff1f;2. 调试是什么&#xff1f;有多重要&#xff1f;2.1 调试是什么&#xff1f;2.2 调试的基本步骤2.3 Debug和Release的介绍 3. Windows环境调试介绍3.1 调试环境的准备3.2 学会快捷键3.3 调试的时候查看程序当前信息3.3.1 查看临时变量的值…

身为程序员,你有哪些提高写代码效率的工具?

首先&#xff0c;每个程序员都是会利用工具的人&#xff0c;也有自己囊里私藏的好物。独乐乐不如众乐乐&#xff0c;今天笔者整理了3个辅助我们写代码的黑科技&#xff0c;仅供参考。如果你有更好的工具&#xff0c;欢迎评论区分享。 1、Google/Stackoverflow——搜索解决方案的…

作为Android 开发,该如何写好自己的简历……

不管在那个行业、那家公司、那个岗位&#xff0c;你去应聘面试&#xff0c;首先人家第一眼看的肯定是你的简历&#xff0c;先通过简历来初步判断你的能力&#xff0c;再到通知面试进行考察。所以简历是一张很重要牌&#xff0c;就看你怎么打了&#xff1f; 笔者本科毕业&#…

Vue2向Vue3过度Vuex核心概念mutations

目录 1 核心概念-mutations1.定义mutations2.格式说明3.组件中提交 mutations4.练习5.总结 2 带参数的 mutations1.目标&#xff1a;2.语法2.1 提供mutation函数&#xff08;带参数&#xff09;2.2 提交mutation 3 练习-mutations的减法功能1.步骤2.代码实现 4 练习-Vuex中的值…

EXSI技术--Exsi简介与安装

1.EXSI简介 了解可直接安装到您的物理服务器的、可靠的裸机 Hypervisor。通过直接访问并控制底层资源,VMware ESXi可有效地对硬件进行分区,以便整合应用并降低成本。它是业界领先的高效体系架构,在可靠性、性能和支持方面树立了行业标杆。(裸金属架构) VMware vSphere的虚拟…