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

这篇具有很好参考价值的文章主要介绍了Springboot+mybatis-plus+dynamic-datasource+Druid 多数据源 分布式事务。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

0.前言

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

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


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

今天我们使用Springboot+mybatis-plus+dynamic-datasource+Druid 多数据源 实现分布式事务和本地多数据源事务。
每种解决方案都有其适用的场景,然而在实际操作中,我经常接到如下的问题:
“我为什么在添加了事务注解之后,数据源切换还是失败了?”
“我了解到这涉及到分布式事务,但我并不想使用Seata。我的场景比较简单,有没有不需要依赖第三方的解决方案?”
这些问题突显出在现实工作中,我们可能需要更灵活、更简便的解决方案来处理多数据源事务问题。
Springboot+mybatis-plus+dynamic-datasource+Druid 多数据源 分布式事务,Spring Boot 知识集锦,Spring boot 核心源码浅析系列,Mybatis-Plus进阶系列,spring boot,mybatis,分布式,微服务
Springboot+mybatis-plus+dynamic-datasource+Druid 多数据源 分布式事务,Spring Boot 知识集锦,Spring boot 核心源码浅析系列,Mybatis-Plus进阶系列,spring boot,mybatis,分布式,微服务

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会报错。
Springboot+mybatis-plus+dynamic-datasource+Druid 多数据源 分布式事务,Spring Boot 知识集锦,Spring boot 核心源码浅析系列,Mybatis-Plus进阶系列,spring boot,mybatis,分布式,微服务
核心的几处代码

    @Role(value = BeanDefinition.ROLE_INFRASTRUCTURE)
    @ConditionalOnProperty(prefix = DynamicDataSourceProperties.PREFIX, name = "seata", havingValue = "false",
        matchIfMissing = true)
    @Bean
    public 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>>() {
                @Override
                protected 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来判断是否需要获取代理连接
    @Override
    public 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;
        }
    }

    // 与上面的方法类似,只不过这个方法可以传入用户名和密码来获取数据库连接
    @Override
    public 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);
    }

    // 判断是否是指定类型的代理对象
    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return (iface.isInstance(this) || determineDataSource().isWrapperFor(iface));
    }
}

DynamicLocalTransactionInterceptor 动态的本地事务拦截器

动态的本地事务拦截器。基本思想是在方法调用前后添加事务处理的逻辑。当这个拦截器被应用到某个方法时,那么在调用这个方法时,会首先检查当前是否已经存在事务,如果存在则直接调用原始方法。如果不存在,则会先开启一个新的事务,然后调用原始方法,方法结束后根据方法执行的结果来提交或回滚事务。入口在这,看一眼就懂了。
Springboot+mybatis-plus+dynamic-datasource+Druid 多数据源 分布式事务,Spring Boot 知识集锦,Spring boot 核心源码浅析系列,Mybatis-Plus进阶系列,spring boot,mybatis,分布式,微服务

// 实现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,触发整体回滚。文章来源地址https://www.toymoban.com/news/detail-677929.html

@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 仓库,包含源代码、文档和示例等资源。

到了这里,关于Springboot+mybatis-plus+dynamic-datasource+Druid 多数据源 分布式事务的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包赞助服务器费用

相关文章

  • shardingsphere5.x整合springboot+dynamic-datasource多数据源实战

    本文是在springboot整合分库分表的基础上添加了多数据源,建议先看上一篇shardingsphere5.x整合springboot分库分表实战_任人人人呢的博客-CSDN博客 pom.xml配置: yml配置: 添加多数据源配置类: 添加多数据源常量类: Mapper文件

    2024年02月13日
    浏览(15)
  • Springboot 多数据源 dynamic-datasource动态添加移除数据源

    Springboot 多数据源 dynamic-datasource动态添加移除数据源

    上一篇文章我们讲了如何通过多数据源组件,在Spring boot Druid 连接池项目中配置多数据源,并且通过@DS注解的方式切换数据源,《Spring Boot 配置多数据源【最简单的方式】》。但是在多租户的业务场景中,我们通常需要手动的切换数据源,那么本文将解答你的额疑惑。 dynam

    2024年02月13日
    浏览(13)
  • SpringBoot+MybatisPlus+dynamic-datasources实现连接Postgresql和mysql多数据源

    SpringBoot+MybatisPlus+dynamic-datasources实现连接Postgresql和mysql多数据源

    dynamic-datasource-spring-boot-starter实现动态数据源Mysql和Sqlserver: dynamic-datasource-spring-boot-starter实现动态数据源Mysql和Sqlserver_dynamic-datasource-spring-boot-starter mysql sqlse-CSDN博客 SpringBoot中整合MybatisPlus快速实现Mysql增删改查和条件构造器: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/13

    2024年01月21日
    浏览(21)
  • SpringBoot+dynamic-datasource实现多数据源(msyql、sqlserver、postgresql)手动切换

    SpringBoot+dynamic-datasource实现多数据源(msyql、sqlserver、postgresql)手动切换

    SpringBoot+MybatisPlus+dynamic-datasources实现连接Postgresql和mysql多数据源: SpringBoot+MybatisPlus+dynamic-datasources实现连接Postgresql和mysql多数据源-CSDN博客 上面实现通过注解和配置文件的方式去进行多数据源操作。 如果业务需求,比如查询第三方接口时提供的是sqlserver的视图连接方式时,

    2024年01月20日
    浏览(16)
  • dynamic-datasource can not find primary datasource

    dynamic-datasource can not find primary datasource

    动态数据源找不到主数据源 可能导入多数据源依赖导致 把依赖注释 运行成功

    2024年02月08日
    浏览(9)
  • 【源码解析】多数据源 dynamic-datasource快速入门及源码解析

    【源码解析】多数据源 dynamic-datasource快速入门及源码解析

    启动的时候,会加载在 dynamic-datasource-spring-boot-starter 的jar包中的 spring.factories 在 DynamicDataSourceAutoConfiguration 会注入 DynamicRoutingDataSource DynamicRoutingDataSource#afterPropertiesSet ,系统启动的时候会加载所有的数据源 在 DynamicDataSourceAutoConfiguration 会注入 DynamicDataSourceProvider AbstractData

    2023年04月21日
    浏览(13)
  • dynamic-datasource Please check the setting of primary解决方案

    dynamic-datasource Please check the setting of primary解决方案

    报这个错的原因是数据库使用多数据源没有指定主数据源导致的错误。  解决方法如图所示,通过配置指定dynamic的primary指定主数据库的配置 切记箭头两处的名称一样

    2024年02月13日
    浏览(19)
  • MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master)

    MyBatis Plus 插件 动态数据源实现原理与源码讲解 (dynamic-datasource-spring-boot-starter-master)

    目录 1. 介绍 2. 基本原理 3. 源码介绍 3.1 使用 AOP 拦截,方法执行前获取到当前方法要用的数据源 3.2 实现自定义 DataSource 接口,实现 DataSource 接口的 getConnect 方法做动态处理 多数据源即一个项目中同时存在多个不同的数据库连接池。 比如 127.0.0.1:3306/test   127.0.0.1:3307/test 

    2024年02月07日
    浏览(16)
  • Mybatis-Plus+SpringBoot框架详解

    Mybatis-Plus+SpringBoot框架详解

    1、SpringBoot 简介 SpringBoot 提供了一种快速使用 Spring 的方式,基于约定优于配置的思想,可以让开发人员不必在配置与逻辑业务之间进行思维的切换,全身心的投入到逻辑业务的代码编写中,从而大大提高了开发的效率,一定程度上缩短了项目周期。 2014 年 4 月,Spring Boot 1

    2023年04月08日
    浏览(13)
  • SpringBoot整合Mybatis-Plus(SpringBoot3)

    SpringBoot整合Mybatis-Plus(SpringBoot3)

    依赖pom.xml: pom.xml resource包下的Application.yml: Aollication.yml pojo包下的实体类User: User mapper包下的接口UserMapper: UserMapper 主启动类DemoPlusApplication DemoPlusApplication 测试类DemoApplicationTest: DemoApplicationTest 实现结果 检测数据库连接: C(Create): D(Delete): U(Update) R(Read)

    2024年03月20日
    浏览(11)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包