SpringBoot多数据源实践:基于场景的构建、实现和事务一体化研究

1. 多数据源应用场景剖析

1.1 业务驱动的多数据源需求

  • 数据量与业务复杂度引发的分库分表:在现代企业级应用中,随着业务的不断拓展和用户量的持续增长,数据量呈爆炸式增长。例如,在大型电商平台中,用户数据、订单数据、商品数据等各类数据海量积累。若将所有数据存储于单一数据库,查询和管理效率将急剧下降。以用户相关数据为例,用户的基本信息、购物偏好、历史订单等数据繁多,若与商品库存、评论等数据混合存储,每次查询用户信息时都需遍历大量无关数据,严重影响性能。因此,将与用户相关的数据(如用户信息、购物车、订单历史等)存储在专门的用户库中,而与商品相关的数据(如商品信息、库存、评论等)存放在商品库,这种按业务领域划分数据库的方式,能极大提高数据管理的效率和系统的可扩展性。
    • 数据分布在不同的数据库中,数据库拆了,应用没拆。一个公司多个子项目,各用各的数据库,涉及数据共享(这种情况也可以使用OpenFeign进行服务间调用,但是存在http调用网络损耗)
    • 分库分表,根据业务来划分不同的库,比如与用户相关的表在db_user库,与订单相关的表在db_order库。
  • 读写分离优化数据库性能:数据库的读写操作在性能和资源占用上存在显著差异。写操作通常需要对数据进行加锁,以确保数据的一致性和完整性,这在高并发场景下可能导致读操作被阻塞,严重影响系统的整体读性能。例如,在电商促销活动期间,大量用户同时下单(写操作),若读写不分离,查询商品信息(读操作)的用户可能会遇到长时间的延迟。为解决此问题,许多数据库采用主从架构。主库负责处理所有的写操作(如用户下单、更新库存等),从库则专门用于处理读操作(如查询商品信息、用户订单列表等)。通过这种方式,能有效提高系统的整体性能,满足高并发场景下的读写需求,提升用户体验。
    • master和slave模式,master库只用来写入数据,slave库只用来读取数据。
    • 为了解决数据库的读性能瓶颈(读比写性能更高,写锁会影响读阻塞,从而影响读的性能)。很多数据库拥有主从架构。也就是,一台主数据库服务器,是对外提供增删改业务的生产服务器;另一(多)台从数据库服务器,主要进行读的操作。
    • 可以通过中间件(ShardingSphere、mycat、mysql-proxy、TDDL…),但是有一些规模较小的公司,没有专门的中间件团队搭建读写分离基础设施,因此需要业务开发人员自行实现读写分离。

1.2 技术视角下的多数据源场景

  • 高性能主从架构:在追求高性能的场景中,主从架构是一种经典且有效的解决方案。主库作为数据的主要写入点,承担所有的写操作,确保数据的一致性和完整性。从库通过复制主库的数据,实现数据的冗余备份,同时分担读操作的负载。一主一从或一主多从的配置可根据实际业务需求灵活调整。例如,在一个内容管理系统中,主库负责更新文章内容、用户评论等写操作,多个从库可用于处理用户浏览文章、搜索等读操作,根据读操作的流量大小合理分配从库数量,以平衡系统的性能和成本。
  • 高可用主备模式:对于对数据库高可用性要求极高的场景,主备模式提供了可靠的保障。主数据库正常情况下对外提供服务,而备用数据库时刻处于待命状态。当主数据库出现故障(如硬件故障、网络问题等)时,备用数据库能迅速接管服务,确保业务的连续性。多主多备的配置进一步提高了系统的容错能力和可靠性。例如,在金融交易系统中,一旦主数据库发生故障,备用数据库能立即切换,避免交易中断,保障金融业务的稳定运行。
  • 同构与异构数据处理:在实际应用中,企业可能因历史遗留问题、业务并购或技术选型等原因,需要处理存储在不同类型数据库中的数据。同构数据处理涉及到多个相同类型数据库(如多个 MySQL 数据库)之间的数据交互,这种情况相对较为简单,主要挑战在于数据的同步和一致性维护。而异构数据处理则需要在不同数据库系统(如 MySQL 与 Oracle 或 PostgreSQL)之间进行数据整合和操作,这要求系统具备强大的跨数据库兼容性和数据转换能力。例如,企业在升级数据库系统时,可能需要将旧数据库中的数据迁移到新系统中,同时确保新老系统在一段时间内的数据同步。

1.3 实际项目中的多数据源案例

  • 多库连接的服务需求:在企业级应用开发中,一个服务往往需要与多个数据库进行交互以满足复杂的业务需求。以一个企业内部的综合管理系统为例,其中的 basic 服务既要连接菜单库获取用户操作菜单信息,又要连接用户库进行用户权限验证和用户个性化信息获取。若菜单库和用户库未分离,随着菜单数量和用户数据的增加,查询和管理的复杂性将呈指数级增长,影响系统整体性能和可维护性。
  • 风控系统的多库校验:风控系统在保障企业业务安全方面起着至关重要的作用,它需要对业务进行全方位的校验,这通常涉及到多个业务库。在金融领域的信贷业务中,风控系统在业务流程的不同阶段(如事前、事中、事后)从相应的业务库中获取数据,进行风险评估和控制。例如,事前校验可能需要从用户信用库中获取用户信用评分、信用记录等信息,以评估用户的信用风险;事中校验可能需要查询交易流水库,实时监控交易行为是否异常;事后校验可能涉及账户状态库,确保交易完成后账户状态的正确性。通过对多个业务库的综合校验,有效降低风险,保障金融业务的稳健运行。
  • 批量数据查询与直连数据库:在某些特定业务场景下,虽然接口提供了数据查询功能,但对于批量查询操作,直接连接数据库可能更为高效。例如,在就业业务中,社保数据查询可能涉及大量人员信息。若通过接口循环调用,由于网络传输的延迟和开销,性能将受到严重影响。在获得客户同意的情况下,直接连接对方的备份库进行查询可以大大提高效率,但需要谨慎操作,避免对生产库造成过大的负载。如在批量查询企业员工社保缴纳记录时,直连备份库能快速获取数据,减少用户等待时间,提升业务处理效率。
  • 新老系统数据库同步:在系统升级或更替过程中,新老数据库的同步是一个关键问题。以企业的人力资源管理系统为例,当经办系统进行更新换代时,友商的老系统数据库可能仍在为大数据中心提供实时服务。为了确保数据的一致性和业务的连续性,需要实时将新系统中的数据写入老系统数据库,或者反之。在一些情况下,如果实时同步不可行,也可采用数据库定时同步策略,但这可能会导致一定的数据延迟。例如,新系统中员工的最新人事变动信息需要及时同步到老系统,以保证整个企业人力资源数据的完整性和准确性。

2. 多数据源的实现策略

2.1 传统 AOP 事务拦截与数据源选择

在多数据源管理中,传统的实现方式通常采用 AOP(面向切面编程)进行事务拦截。当开启事务时,通过判断数据源的键(key)来决定使用哪个数据源。这种方式需要手动管理数据源的切换逻辑,增加了代码的复杂性和维护成本。

例如,开发人员需要编写切面类,在方法执行前根据业务规则选择合适的数据源,并将其绑定到当前线程的上下文中。以下是一个简单的 AOP 事务拦截实现示例:

@Aspect
@Component
public class DataSourceAspect {@Before("@annotation(yourTransactionAnnotation)")public void switchDataSource(JoinPoint joinPoint) {// 根据业务规则获取数据源键String dataSourceKey = determineDataSourceKey(joinPoint);// 从数据源池中获取相应数据源并绑定到当前线程DataSource dataSource = DataSourceManager.getDataSource(dataSourceKey);DataSourceContextHolder.setDataSource(dataSource);}private String determineDataSourceKey(JoinPoint joinPoint) {// 这里可以根据方法参数、类名等信息确定数据源键// 例如,如果方法参数中有特定标识,则选择对应的数据源Object[] args = joinPoint.getArgs();for (Object arg : args) {if (arg instanceof YourDataSourceIndicator) {return ((YourDataSourceIndicator) arg).getDataSourceKey();}}// 如果无法确定,则返回默认数据源键return "defaultDataSource";}
}

2.2 MyBatis Plus的@DS注解的优势

MyBatis Plus 提供的 @DS 注解简化了多数据源的管理过程。该注解可以直接标记在 Mapper 接口或 Service 方法上,指定要使用的数据源。其原理是基于动态数据源切换机制,在运行时根据注解的配置信息,自动从多个数据源中选择合适的数据源进行数据库操作。

例如,在一个包含用户库和订单库的系统中,通过在用户相关的 Mapper 接口上标注 @DS (“user”),在订单相关的 Mapper 接口上标注 @DS (“order”),可以轻松实现数据源的切换,而无需编写复杂的 AOP 拦截逻辑。

2.3 ThreadLocal在数据源管理中的角色

在多数据源切换过程中,ThreadLocal 起着关键作用。它用于存储当前线程的数据库键(key)变量,确保在同一线程内的数据库操作都使用相同的数据源。当一个线程进入带有 @DS 注解的方法时,@DS 注解会将对应的数据源键存入 ThreadLocal 中。在后续的数据库操作中,通过获取 ThreadLocal 中的数据源键,从数据源池中获取相应的数据库连接。这种方式保证了数据源的线程安全性,避免了不同数据源之间的干扰。

以下是一个简单的 ThreadLocal 使用示例:

public class DataSourceContextHolder {private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();public static void setDataSource(String dataSourceKey) {contextHolder.set(dataSourceKey);}public static String getDataSource() {return contextHolder.get();}public static void clearDataSource() {contextHolder.remove();}
}

3. 多数据源下的事务问题

3.1 @Transational和@DS注解冲突问题

3.1.1 问题模拟

  1. 先导入pom.xml依赖
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!-- 数据源切换依赖 --><dependency><groupId>com.baomidou</groupId><artifactId>dynamic-datasource-spring-boot-starter</artifactId><version>3.5.2</version></dependency><!-- MySQL依赖 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.29</version></dependency><!-- Mybatis依赖 --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.2</version></dependency></dependencies>
  1. yml文件配置了3个数据源,主数据源是master,从数据源是slave,后续临时加了个数据源temp,为了用于事务的测试,数据库均为MySQL。
server:port: 8080spring:datasource:dynamic:primary: masterdatasource:master:username: anarkhpassword: 123456url: jdbc:mysql://localhost:3306/anarkh_master?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTCdriver-class-name: com.mysql.cj.jdbc.Driverslave:username: anarkhpassword: 123456url: jdbc:mysql://localhost:3306/anarkh_slave?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTCdriver-class-name: com.mysql.cj.jdbc.Drivertemp:username: anarkhpassword: 123456url: jdbc:mysql://localhost:3306/temp?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTCdriver-class-name: com.mysql.cj.jdbc.Drivermybatis:mapper-locations: classpath:mapping/*.xml
  1. 分别编写主数据源和从数据源的Mapper层接口
@Mapper
@DS("master")
public interface MasterMapper {int insertUser(User user);
}
@Mapper
@DS("slave")
public interface SlaveMapper {int insertRole(Role role);
}
  1. 分别编写对应的XML文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.MasterMapper"><insert id="insertUser" parameterType="com.example.demo.bean.User">INSERT INTO user (username, password)VALUES(#{username}, #{password})</insert>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.SlaveMapper"><insert id="insertRole" parameterType="com.example.demo.bean.Role">INSERT INTO role (role)VALUES(#{role})</insert>
</mapper>
  1. 编写Service方法
@Service
public class UserService {@Resourceprivate MasterMapper masterMapper;@Resourceprivate SlaveMapper slaveMapper;@Transactionalpublic void Add(){User user = new User();user.setUsername("anarkh");user.setPassword("666666");masterMapper.insertUser(user);Role role = new Role();role.setRole("管理员");slaveMapper.insertRole(role);}
}
  1. 测试运行,报错如下
### Error updating database.  Cause: java.sql.SQLSyntaxErrorException: Table 'master.role' doesn't exist
### The error may exist in file [D:\JavaProjects\Java\demo\target\classes\mapping\SlaveMapper.xml]
### The error may involve com.example.demo.mapper.SlaveMapper.insertRole-Inline
### The error occurred while setting parameters
### SQL: INSERT INTO role (role)         VALUES(?)
### Cause: java.sql.SQLSyntaxErrorException: Table 'master.role' doesn't exist
; bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException: Table 'master.role' doesn't exist

从报错的表面上来看,是因为在主数据库当中不存在role这个表,可是我们已经切换了数据源呀,可为什么还是报错呢?

3.1.2 原因分析

在 Spring 框架中,@Transactional 注解用于开启事务管理。当一个方法被标注为 @Transactional 时,Spring 会在方法执行前从数据库连接池获取数据库连接,并将该事务与当前线程绑定。在事务执行过程中,数据库连接通过 ThreadLocal 与当前线程绑定。一旦事务开始,所有的数据库操作都将使用该绑定的连接。当 @DS 注解尝试切换数据源时,如果事务已经开始并且连接已经绑定,新的数据源切换请求可能会被忽略,导致实际执行数据库操作时仍然使用了错误的数据源。例如,在一个同时涉及主库写操作和从库读操作的事务方法中,如果主库事务已经开始并绑定了连接,后续的从库操作可能因为无法切换数据源而失败。

说回到上面的例子:@Transactional开启事务的时候,会先从数据库连接池获取是数据库的连接(基于Spring的AOP切面),我们UserService方法上面没有打上@DS注解,所以Spring默认采用的是主数据源,而且在这之后,这个事务会通过ThreadLocal跟当前线程绑定并也报错了connection连接,通俗的来讲,在进入UserService方法的时候,当前事务已经绑定了数据源Master,在运行到SlaveMapper接口时,因为当前事务的connection连接已经存在,所以拿到的数据源还是默认的Master,于是想找到Slave当中的role表,当然是不可能的,所以只能报错了。

3.1.3 解决方案

3.1.3.1 采用分布式事务

分布式事务是解决多数据源事务一致性问题的一种方法。在分布式系统中,多个数据源可能分布在不同的节点或服务中,传统的本地事务无法满足跨数据源事务的需求。分布式事务管理框架(如 Seata、Atomikos 等)提供了强大的事务协调能力,确保在多个数据源之间的操作要么全部成功提交,要么全部回滚。例如,在一个微服务架构中,订单服务可能涉及用户库、商品库和库存库等多个数据源的操作。通过分布式事务框架,可以协调这些数据源的事务,保证数据的一致性。然而,分布式事务的实现相对复杂,需要额外的配置和基础设施支持,并且可能会对系统性能产生一定的影响。以下是一个使用 Seata 实现分布式事务的简单示例:

  1. 引入 Seata 相关依赖:
<dependency><groupId>io.seata</groupId><artifactId>seata-spring-boot-starter</artifactId><version>1.4.2</version>
</dependency>
  1. 配置 Seata 相关参数(如注册中心地址、事务分组等):
seata.registry.type=eureka
seata.registry.eureka.application = seata-server
seata.registry.eureka.service-url = http://localhost:8761/eureka
seata.tx-service-group=my_tx_group
  1. 在服务中使用 @GlobalTransactional 注解开启分布式事务:
@Service
public class UnitServiceImpl implements UnitService {@Autowiredprivate UserService userService;@Autowiredprivate RoleService roleService;@GlobalTransactional@Overridepublic void Add() {User user = new User();user.setUsername("anarkh");user.setPassword("666666");userService.insertUser(user);Role role = new Role();role.setRole("管理员");roleService.insertRole(role);}
}
3.1.3.2 更改事务的传播机制(有问题)

Propagation.REQUIRES_NEW 机制原理:

更改事务的传播机制为 Propagation.REQUIRES_NEW 是解决 @Transactional 与 @DS 冲突的一种方法。这种机制会挂起当前事务,并创建一个新的事务,为新事务分配新的数据库连接。新事务与原事务相互独立,各自的提交和回滚操作不会相互影响。在多数据源操作中,这可以确保每个数据源操作都在正确的事务和连接下执行。例如,在主从数据源操作中,将从库操作的事务传播机制设置为 Propagation.REQUIRES_NEW,可以使从库操作在独立的事务中执行,不受主库事务的影响。

事务传播机制的注意事项与潜在问题:

虽然 Propagation.REQUIRES_NEW 可以解决数据源切换问题,但也带来了一些潜在问题。在同一个方法中,如果存在多个具有不同事务传播机制的子方法,事务的一致性管理会变得复杂。例如,当一个方法 A 中调用了一个具有 Propagation.REQUIRES_NEW 的方法 B,然后方法 B 中又调用了一个方法 C。如果方法 C 出现异常并回滚,方法 B 根据其事务机制可能已经提交,而方法 A 可能会因为异常而回滚,这就导致了事务的不一致性。因此,在使用这种机制时,需要仔细考虑事务的边界和异常处理逻辑,确保整个业务流程的事务一致性。

改进:

其实我们只要更改一下事务的传播机制,将它设置为:Propagation.REQUIRES_NEW即可,意思就是将原有的Spring事务挂起,并创建一个新的事务并分配的一个新的connection,两者不影响,具体操作如下:

  1. 修改原有的UserService代码

不用通过@DS指定数据源,因为默认是Master;将slave业务操作分离出来,封装到一个Service服务类当中,再通过@Resource注解注入进来,最后还是指定一下回滚策略,遇到异常就回滚。

@Service
public class UserService {@Resourceprivate MasterMapper masterMapper;@Resourceprivate SlaveService slaveService;@Transactional(rollbackFor = Exception.class)public void Add(){User user = new User();user.setUsername("anarkh");user.setPassword("666666");masterMapper.insertUser(user);slaveService.slave();}}
  1. 编写SlaveService服务代码

必须通过@DS指定一下数据源为slave,在slave方法上面重新修改一下事务的传播机制即可

@Service
@DS("slave")
public class SlaveService {@Resourceprivate SlaveMapper slaveMapper;@Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)public void slave(){Role role = new Role();role.setRole("管理员");slaveMapper.insertRole(role);}
}
  1. 其他的保持不变,最后我们再测试一下,看一下输出结果,成功了!

注意: 因为Propagation.REQUIRES_NEW是开启一个新的事务并重新分配一个新的数据库连接,在同一个方法中,有A方法和一个开启新的传播事务的B方法,如果B方法中出现了异常发生了回滚,那么A方法也会随之回滚,但是,但是,但是!如果B方法后面有一个新方法C,当C方法中出现了异常,C方法回滚了,但是B方法根据它事务机制并且已经提交了事务,那么就会出现A事务回滚了,B事务提交了,C事务回滚了,这样ABC三个方法出出现了事务不一致的问题,在下面的事务回滚机制的第五条有演示。

3.1.3.3 @DSTransational注解代替@Transactional
  • @DSTransactional 注解功能概述
    • @DSTransactional 注解是专门为解决多数据源事务问题而设计的。它可以直接替代 @Transactional 注解,在不改变原有代码结构的基础上,实现多数据源事务的正确管理。该注解内部实现了对数据源切换和事务管理的优化,确保在多数据源操作中事务的一致性和正确性。

使用 @DSTransactional 注解非常简单。只需在原来使用 @Transactional 的地方替换为 @DSTransactional,并确保项目中引入了相应的依赖。例如,在一个包含主从数据源操作的 Service 方法中,标注 @DSTransactional 后,该方法中的所有数据库操作(涉及不同数据源)将在一个事务中正确执行,并且在出现异常时能够自动回滚所有相关数据源的操作。

我们可以使用@DSTransactional注解代替@Transactional即可,其他什么都不用动,也是最简单的方法。

  1. 导入pom.xml依赖
    <dependency><groupId>com.baomidou</groupId><artifactId>dynamic-datasource-spring-boot-starter</artifactId><version>3.5.0</version></dependency>
  1. 修改UserService代码
@Service
public class UserService {@Resourceprivate MasterMapper masterMapper;@Resourceprivate SlaveMapper slaveMapper;@DSTransactionalpublic void Add(){User user = new User();user.setUsername("anarkh");user.setPassword("666666");masterMapper.insertUser(user);Role role = new Role();role.setRole("管理员");slaveMapper.insertRole(role);}
}
  1. 运行测试,查看输出结果,成功!

4. 事务回滚机制的详细解析

在多数据源事务中,事务回滚机制的行为取决于异常抛出的位置和事务传播机制的设置。以下是几种常见异常场景下的事务回滚行为分析:

4.1 在Master和Slave事务执行前抛出异常

UserService类:

@Transactional(rollbackFor = Exception.class)
public void Add(){User user = new User();user.setUsername("anarkh");user.setPassword("666666");int a = 1/0;masterMapper.insertUser(user);slaveService.slave();
}

结果:数据保持一致

原因:事务还未开始执行数据库操作,异常直接导致方法终止,事务不会被提交。

4.2、当master事务和slave事务中间抛出异常

UserService类:

@Transactional(rollbackFor = Exception.class)
public void Add(){User user = new User();user.setUsername("anarkh");user.setPassword("666666");masterMapper.insertUser(user);int a = 1/0;slaveService.slave();
}

结果:回滚master事务,slave事务无影响

原因:在事务执行过程中,当出现异常时,事务会根据异常情况决定是否回滚已经执行的操作。在这种情况下,master 事务已经执行了部分操作,由于异常发生在中间,根据事务的原子性,已执行的 master 事务操作将被回滚。

4.3、在slave方法中抛出异常

SlaveService类:

@Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
public void slave(){Role role = new Role();role.setRole("管理员");slaveMapper.insertRole(role);int a = 1/0;
}

结果:master和slave事务都会进行回滚

原因:slave 事务设置了 Propagation.REQUIRES_NEW,它会在独立的事务中执行,当 slave 事务中出现异常时,它会自行回滚,同时由于外层的 UserService 方法也在一个事务中(由 @Transactional 注解管理),并且 slave 事务的异常会传播到外层事务,导致外层事务也回滚,从而实现了 master 和 slave 事务的一致性回滚。

4.4、在master和slave事务之后

UserService类:

@Transactional(rollbackFor = Exception.class)
public void Add(){User user = new User();user.setUsername("anarkh");user.setPassword("666666");masterMapper.insertUser(user);slaveService.slave();int a = 1/0;
}

结果:master事务回滚,slave已经提交事务,入库

原因:在事务执行过程中,master 事务和 slave 事务在异常抛出前已经完成了提交操作,而事务的回滚通常是基于异常的捕获和处理机制。在这种情况下,异常发生在事务提交之后,master 事务由于受到 @Transactional 注解的管理,会根据异常情况进行回滚,但 slave 事务已经完成了提交,无法再进行回滚操作。

4.5、临时添加一个temp数据库,进行插入操作,并抛出异常

UserService类:

@Transactional(rollbackFor = Exception.class)
public void Add(){User user = new User();user.setUsername("anarkh");user.setPassword("666666");masterMapper.insertUser(user);slaveService.slave();tempService.temp();
}

TempService类:

@Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
public void temp(){Car car = new Car();car.setCar("Benz");tempMapper.insertCar(car);int a = 1/0;
}

结果:master回滚,slave事务提交,temp回滚

原因: temp 事务设置了 Propagation.REQUIRES_NEW,它在独立的事务中执行,当 temp 事务中出现异常时,它会自行回滚,而 master 事务由于受到 @Transactional 注解的管理,会根据异常情况进行回滚,slave 事务由于已经提交,不受影响。

4.6、嵌套

UserService类:

@Transactional(rollbackFor = Exception.class)
public void Add(){User user = new User();user.setUsername("anarkh");user.setPassword("666666");masterMapper.insertUser(user);slaveService.slave();
}

SlaveService类:

@Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
public void slave(){Role role = new Role();role.setRole("管理员");slaveMapper.insertRole(role);tempService.temp();
}

TempService类:

@Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
public void temp(){Car car = new Car();car.setCar("Benz");tempMapper.insertCar(car);int a = 1/0;
}

结果:master回滚,slave回滚,temp回滚

原因:在嵌套事务中,内层事务(TempService 中的事务)的异常会传播到外层事务(SlaveService 中的事务),导致 SlaveService 中的事务回滚,而 SlaveService 中的事务回滚又会传播到最外层的 UserService 中的事务,导致整个事务链中的所有事务都回滚,从而保证了数据的一致性。

4.7、使用@DSTransactional注解,在slave和temp之间抛出异常

UserService类

@DSTransactional
public void Add(){User user = new User();user.setUsername("anarkh");user.setPassword("666666");masterMapper.insertUser(user);Role role = new Role();role.setRole("管理员");slaveMapper.insertRole(role);int a=1/0;Car car = new Car();car.setCar("Benz");tempMapper.insertCar(car);
}

结果:master回滚,slave回滚、temp回滚

4.8、使用@DSTransactional注解,在最后抛出异常

@DSTransactional
public void Add(){User user = new User();user.setUsername("anarkh");user.setPassword("666666");masterMapper.insertUser(user);Role role = new Role();role.setRole("管理员");slaveMapper.insertRole(role);Car car = new Car();car.setCar("Benz");tempMapper.insertCar(car);int a=1/0;
}

结果:master回滚,slave回滚、temp回滚

5. 适用场景总结与技术选型建议

5.1 不同解决方案的适用场景总结

  • @DSTransactional 注解:适用于单个服务内的多数据源事务管理,简单易用,能够有效地保证事务的一致性和数据源的正确切换。在开发过程中,如果系统架构相对简单,且多数据源操作主要集中在一个服务内,@DSTransactional 是一个不错的选择。例如,在一个小型电商系统中,订单服务可能需要同时操作订单库和用户库,使用 @DSTransactional 注解可以方便地管理这两个数据源的事务,确保数据的完整性。
  • 分布式事务框架(如 Seata、Atomikos):当系统采用微服务架构,涉及多个服务之间的多数据源事务协作时,分布式事务框架是必要的选择。它能够提供强大的事务协调能力,确保跨服务、跨数据源的事务一致性,但需要投入更多的资源进行配置和维护。例如,在一个大型电商平台中,订单服务、库存服务、支付服务等多个服务可能涉及不同的数据源,此时使用分布式事务框架可以保证整个业务流程的事务正确性,避免数据不一致的情况发生。
  • 事务传播机制调整(Propagation.REQUIRES_NEW):在一些特定场景下,如需要对部分数据源操作进行独立事务管理时,可以使用事务传播机制调整。但需要注意其可能带来的事务不一致性问题,谨慎使用并确保对事务边界和异常处理有清晰的理解。比如,在一个复杂的业务逻辑中,某个子操作对数据源的写操作独立性要求较高,且不希望受外层事务影响时,可以考虑使用 Propagation.REQUIRES_NEW 来单独管理该子操作的事务,但同时要处理好与外层事务的交互和异常情况。

注:

只有一个服务,且用到多个数据源时,用@DSTransational注解比较方便,可以控制多数据源进行回滚。

为什么说只有一个服务采用@DSTransactional注解,多服务不行吗?不行。如果系统是微服务架构,db1、db2、db3都源于不同的服务,那么db3报错时,前面两个并不会回滚,因为他们都不在一个服务内,@DSTransactional注解此时派不上用场。此时只能采用分布式事务控制了(seata、Atomikos)。

5.2 技术选型决策因素与最佳实践

在选择多数据源事务管理解决方案时,需要综合考虑多个因素。首先是系统架构,微服务架构通常需要分布式事务框架来处理跨服务事务,而单体应用或简单的多模块应用可能更适合使用 @DSTransactional 注解。其次是性能要求,分布式事务框架可能会带来一定的性能开销,需要根据系统的实际性能需求进行评估。例如,如果系统对性能要求极高,且多数据源操作相对简单,可能需要谨慎考虑分布式事务框架的使用,避免性能下降。此外,开发团队的技术能力和维护成本也是重要的考虑因素。如果团队对分布式事务框架不熟悉,可能会在配置和维护过程中遇到困难,增加项目风险。最佳实践是在项目初期进行充分的技术调研和架构设计,根据系统的特点和需求选择最合适的事务管理方案,并在开发过程中遵循相关的设计原则和规范,确保事务的正确性和系统的稳定性。同时,建立完善的监控和测试机制,及时发现和解决事务管理中可能出现的问题。

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

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

相关文章

LabVIEW 中dde.llbDDE 通信功能

在 LabVIEW 功能体系中&#xff0c;位于 C:\Program Files (x86)\National Instruments\LabVIEW 2019\vi.lib\Platform\dde.llb 的 dde.llb 库占据着重要的地位。作为一个与动态数据交换&#xff08;DDE&#xff09;紧密相关的库文件&#xff0c;它为 LabVIEW 用户提供了与其他…

数据结构-栈和队列的应用

目录 前言一、栈的应用&#xff08;迷宫问题&#xff09;1.1 问题描述1.2 算法选择1.3 算法精化1.4 算法实现1.5 问题结果 二、队列的应用&#xff08;农夫过河问题&#xff09;2.1 问题描述2.2 算法选择2.3 算法精化2.4 算法实现2.5 问题结果 总结 前言 本篇文章使用两个例子…

SkyWalking 10.1.0 实战:从零构建全链路监控,解锁微服务性能优化新境界

文章目录 前言一、集成SkyWalking二、SkyWalking使用三、SkyWalking性能剖析四、SkyWalking 告警推送4.1 配置告警规则4.2 配置告警通知地址4.3 下发告警信息4.4 测试告警4.5 慢SQL查询 总结 前言 在传统监控系统中&#xff0c;我们通过进程监控和日志分析来发现系统问题&…

AIGC图生视频保姆级教程

一、AI文生图高阶技巧 推荐工具 ▸ MidJourney&#xff08;艺术感最强&#xff09; ▸ DALLE 3&#xff08;与ChatGPT深度联动&#xff09; ▸ Leonardo.ai&#xff08;精细化参数控制&#xff09; 核心策略 提示词架构&#xff1a; [主体描述][环境氛围][镜头语言][风格参数…

springboot整合mybatis-plus【详细版】

目录 一&#xff0c;简介 1. 什么是mybatis-plus2.mybatis-plus特点 二&#xff0c;搭建基本环境 1. 导入基本依赖&#xff1a;2. 编写配置文件3. 创建实体类4. 编写controller层5. 编写service接口6. 编写service层7. 编写mapper层 三&#xff0c;基本知识介绍 1. 基本注解 T…

利用亚马逊云科技RDS for SQL Server配置向量数据存储

生成式人工智能&#xff08;AI&#xff09;正迎来又一个快速发展期&#xff0c;引起了开发者们的广泛关注。将生成式能力集成到商业服务和解决方案中变得非常重要。当前的生成式AI解决方案是机器学习和深度学习模型逐步进化迭代的结果。从深度学习到生成式AI的质变飞跃主要是由…

c++ 多线程知识汇总

一、std::thread std::thread 是 C11 引入的标准库中的线程类&#xff0c;用于创建和管理线程 1. 带参数的构造函数 template <class F, class... Args> std::thread::thread(F&& f, Args&&... args);F&& f&#xff1a;线程要执行的函数&…

H5接入支付宝手机网站支付并实现

小程序文档 - 支付宝文档中心 1.登录 支付宝开放平台 创建 网页/移动应用 2.填写创建应用信息 3.配置开发设置 4.网页/移动应用&#xff1a;需要手动上线。提交审核后&#xff0c;预计 1 个工作日的审核时间。详细步骤可点击查看 上线应用 。应用上线后&#xff0c;还需要完成…

字节二面:DNS是什么?是什么原理?

写在前面 最近有个同学后台私信让我出一个DNS的工作原理&#xff0c;面试的时候居然问到了&#xff0c;所以就简单聊聊DNS的工作原理吧&#xff01; 1. DNS 的核心作用 DNS&#xff08;域名系统&#xff0c;Domain Name System&#xff09;是互联网中用于将人类可读的域名转…

【Unity3D】Jenkins Pipeline流水线自动构建Apk

目录 一、准备阶段 二、创建Pipeline流水线项目 三、注意事项 四、扩展 1、Pipeline添加SVN更新项目Stage阶段 一、准备阶段 1、安装tomcat 10.0.5 Index of apache-local/tomcat/tomcat-10 2、安装jdk 17 Java Archive Downloads - Java SE 17.0.13 and later 3、…

【数据结构】(9) 优先级队列(堆)

一、优先级队列 优先级队列不同于队列&#xff0c;队列是先进先出&#xff0c;优先级队列是优先级最高的先出。一般有两种操作&#xff1a;返回最高优先级对象&#xff0c;添加一个新对象。 二、堆 2.1、什么是堆 堆也是一种数据结构&#xff0c;是一棵完全二叉树&#xff0c…

2025.2.15

web [HNCTF 2022 Week1]Interesting_include&#xff1a; 直接打开 PHP代码片段包含两部分&#xff1a;一个主脚本和一个潜在的被包含文件。主脚本负责处理GET请求&#xff0c;特别是filter参数&#xff0c;而被包含文件&#xff08;假设为./flag.php&#xff09;似乎包含了我…

CentOS 7.8 安装MongoDB 7教程

文章目录 CentOS 7.8 安装MongoDB 7教程一、准备工作1. 系统更新2. 权限 二、添加MongoDB软件源1. 创建MongoDB的yum源文件2. 添加以下内容3. 保存并退出编辑器 三、安装MongoDB1. 更新yum缓存2. 安装MongoDB 四、启动MongoDB服务1. 启动MongoDB2. 设置MongoDB开机自启动 五、配…

ElasticSearch基础和使用

ElasticSearch基础 1 初识ES相关组件 &#xff08;1&#xff09;Elasticsearch是一款非常强大的开源搜索引擎&#xff0c;可以帮助我们从海量数据中快速找到需要的内容。Elasticsearch结合kibana、Logstash、Beats组件 也就是elastic stack&#xff08;ELK&#xff09; 广泛应…

[C++]多态详解

目录 一、多态的概念 二、静态的多态 三、动态的多态 3.1多态的定义 3.2虚函数 四、虚函数的重写&#xff08;覆盖&#xff09; 4.1虚函数 4.2三同 4.3两种特殊情况 &#xff08;1&#xff09;协变 &#xff08;2&#xff09;析构函数的重写 五、C11中的final和over…

【git-hub项目:YOLOs-CPP】本地实现01:项目构建

目录 写在前面 项目介绍 最新发布说明 Segmentation示例 功能特点 依赖项 安装 克隆代码仓库 配置 构建项目 写在前面 前面刚刚实现的系列文章: 【Windows/C++/yolo开发部署01】 【Windows/C++/yolo开发部署02】 【Windows/C++/yolo开发部署03】 【Windows/C++/yolo…

在WPS中通过JavaScript宏(JSA)调用本地DeepSeek API优化文档教程

既然我们已经在本地部署了DeepSeek,肯定希望能够利用本地的模型对自己软件开发、办公文档进行优化使用,接下来就先在WPS中通过JavaScript宏(JSA)调用本地DeepSeek API优化文档的教程奉上。 前提: (1)已经部署好了DeepSeek,可以看我的文章:个人windows电脑上安装DeepSe…

安装 Docker Desktop 修改默认安装目录到指定目录

Docker Desktop安装目录设置 Docker Desktop 默认安装位置 &#xff08;C:\Program Files\Docker\Docker) 是这个 &#xff0c;导致系统盘占用过大&#xff0c;大概2G ; 那么如何安装到其他磁盘呢&#xff1f; 根据docker desktop 官网 Docker Desktop install 我们可以看到&a…

DeepSeek官方发布R1模型推荐设置

今年以来&#xff0c;DeepSeek便在AI领域独占鳌头&#xff0c;热度一骑绝尘。其官方App更是创造了惊人纪录&#xff0c;成为史上最快突破3000万日活的应用&#xff0c;这一成绩无疑彰显了它在大众中的超高人气与强大吸引力。一时间&#xff0c;各大AI及云服务厂商纷纷投身其中&…

常见的IP地址分配方式有几种:深入剖析与适用场景‌

在数字互联的世界里&#xff0c;IP地址如同网络世界的“门牌号”&#xff0c;是设备间通信的基础。随着网络技术的飞速发展&#xff0c;IP地址的分配方式也日趋多样化&#xff0c;以适应不同规模、不同需求的网络环境。本文将深入探讨当前主流的几种IP地址分配方式&#xff0c;…