事务对于数据库动态切换的困扰

1.背景

在开发逃生平台时,会扫描并操作S3集群的元数据,而S3的元数据使用的是OceanBase0.5,最初非开源的版本,导致mybatis的连接器使用了很老的版本:

xml 复制代码
<dependency>  
<groupId>mysql</groupId>  
<artifactId>mysql-connector-java</artifactId>  
<version>5.1.34</version>  
</dependency>

由于一个集群对应一个ob集群,那么在访问集群时需要不停的切换连接地址,即ChunkServer对应的ip。本身公司有数据库中间件zebra,可以完成公司内部数据库的自动切换,但是ob是特殊的,所以自己来搭建动态切换源的能力。 此次,由于开发一个新功能,需要在逃生动作执行前,先预执行扫面哪些节点或者实体要逃生,那么就需要不停的扫面集群,最好再生成相关任务存储在平台数据库。 在service的内部逻辑上,一个任务通常对应多个mysql的表单,这其中为了防止形成脏数据或者错误的任务,进行了事务的控制。 当然,由于整体存在for循环,为了防止事务过大,先是在内部手动开启事务,拆分回滚,逻辑如下:

实际运行中发现,频繁有表单找不到的报错,经过查询验证发现是数据库本身就连接错误了,目标是访问集群b,获取b_config,但是一直连接在集群a上,无法切换。这个问题困扰了许久,特在此记录下,也是一次不错的学习机会。

2.如何实现动态数据源的

spring提供了AbstractRoutingDataSource抽象类实现了动态添加数据库路由的方法,它允许应用程序在运行时根据某些条件(如当前用户、事务类型等)动态选择不同的数据源。这在多租户系统、读写分离等场景中非常有用。

主要方法

  1. determineCurrentLookupKey() : 这是一个抽象方法,子类必须实现它。该方法返回当前线程上下文中用于查找数据源的键。Spring 会调用这个方法来决定使用哪个数据源。
  2. determineTargetDataSource() : 这个方法根据 determineCurrentLookupKey() 返回的键,从已配置的数据源映射中选择合适的数据源。 代码如下: 首先我们先定义一个threadlocal类来保证当前线程上下文的key:
package 复制代码
  
public class DynamicDataSourceContextHolder {  
  
private static final ThreadLocal<String> OBJECT_CONTEXT_HOLDER = new ThreadLocal<>();  
  
private static final ThreadLocal<String> STORE_CONTEXT_HOLDER = new ThreadLocal<>();  
  
  
public static void setObjectDataSourceKey(String dataSourceKey) {  
OBJECT_CONTEXT_HOLDER.set(dataSourceKey);  
}  
  
public static String getObjectDataSourceKey() {  
return OBJECT_CONTEXT_HOLDER.get();  
}  
  
public static void clearObjectDataSourceKey() {  
OBJECT_CONTEXT_HOLDER.remove();  
}  
  
public static void setStoreDataSourceKey(String dataSourceKey) {  
STORE_CONTEXT_HOLDER.set(dataSourceKey);  
}  
  
public static String getStoreDataSourceKey() {  
return STORE_CONTEXT_HOLDER.get();  
}  
  
public static void clearStoreDataSourceKey() {  
STORE_CONTEXT_HOLDER.remove();  
}  
}

然后再定一个类继承抽象类AbstractRoutingDataSource

public 复制代码
  
private Map<Object, Object> objectDataSources = new HashMap<>();  
  
@Override  
protected Object determineCurrentLookupKey() {  
return DynamicDataSourceContextHolder.getObjectDataSourceKey();  
}  
  
public void addObjectDataSource(String name, DataSource dataSource) {  
objectDataSources.put(name, dataSource);  
setTargetDataSources(objectDataSources);  
// 必须添加此行,否则新添加的数据源无法识别  
afterPropertiesSet();  
}  
  
public boolean containsObjectDataSource(String name) {  
return objectDataSources.containsKey(name);  
}  
  
public void setObjectDataSourcesMap(Map<Object, Object> dataSourcesMap) {  
objectDataSources.putAll(dataSourcesMap);  
}  
  
public Map<Object, Object> getObjectDataSources() {  
return objectDataSources;  
}  
  
public HikariDataSource getInUsedDataSource(String dataSourceName) {  
HikariDataSource hikariDataSource = (HikariDataSource) objectDataSources.get(dataSourceName);  
return hikariDataSource;  
}  
}

从上述代码我们可以看到,通过map维持一个自身的内存连接对象池,实现连接对象的复用。又通过threadlocal实现线程的隔离,访问不同的数据源。且可以看出,想要切换数据源,必须不停的DynamicDataSourceContextHolder.setObjectDataSourceKey更新当前的线程的连接池对象key,当访问数据库时,#### **AbstractRoutingDataSource.getConnection()**会调用 determineCurrentLookupKey() 方法,AbstractRoutingDataSource 会根据 key 从 resolvedDataSources 缓存中加载数据源。因此数据源的切换依赖threadlocal的key值变化决定。

3.事务的基本原理

. 开启事务
  • 在事务开始时,数据库会为当前操作分配一个事务上下文。
  • 在 Spring 中,事务的开启通常由事务管理器(如 DataSourceTransactionManager)负责。
  • 关键点 :事务开启时,Spring 会将数据源(DataSource)和数据库连接(Connection)绑定到当前线程的上下文中(通过 TransactionSynchronizationManager)。
事务的执行
  • 在方法执行过程中,Spring 会确保所有的数据库操作都在同一个事务上下文中执行。
  • 如果涉及到多数据源切换,Spring 的动态数据源(AbstractRoutingDataSource)会从 ThreadLocal 中获取当前数据源。
3. 事务的提交或回滚
  • 如果方法执行成功,代理对象会调用事务管理器的 commit() 方法提交事务。
  • 如果方法执行失败(如抛出异常),代理对象会调用事务管理器的 rollback() 方法回滚事务。
  • 在事务提交或回滚后,代理对象会清理线程上下文中的事务资源。

4.为什么没有正确的切换

其实在b方法中,会for循环切换数据源,再访问集群,但是切换完全失效:

具体原因就在于手动开启事务的情况:

  • 在事务开启后通过 ThreadLocal 切换数据源,事务管理器可能仍然使用最初绑定的数据源,而忽略你的 ThreadLocal 切换操作。
  • 这是因为事务管理器在事务开始时已经将数据源绑定到 ThreadLocal 中,并且在事务结束前不会重新读取 ThreadLocal 中的数据源信息`
  • 换句话说,事务管理器会"锁定"最初绑定的数据源,直到事务提交或回滚。

一句话概括:在一个事务没有结束前,线程上下文绑定的threadlocal都是固定不会变化的 那么就很好理解了,即使每次主动set当前值,也不会影响到事务管理器,且在调试时真切的看到threadlocal已经变化了。

5.为什么使用注解@Transactional可以切换

  • a 方法使用 @Transactional 注解时,Spring 会通过 AOP 代理管理事务。

  • a 方法执行前,Spring 会:

    1. 开启事务。
    2. 将数据源绑定到当前线程的上下文中(通过 TransactionSynchronizationManager)。
  • a 方法中调用 b 方法时:

    • b 方法切换数据源(通过 DynamicDataSourceContextHolder.setObjectDataSourceKey())。
    • Spring 的动态数据源(AbstractRoutingDataSource)会从 ThreadLocal 中获取当前数据源。
    • 由于 b 方法没有开启新事务,数据源切换会生效。
  • a 方法执行后,Spring 会:

    1. 提交或回滚事务。
    2. 清理线程上下文中的事务资源。

关键点:注解模式下,Spring 的代理机制会确保事务的开启、提交、回滚和资源清理是自动完成的,且数据源切换逻辑与事务管理逻辑是解耦的

相关推荐
LUCIAZZZ1 小时前
TCP基本入门-简单认识一下什么是TCP
java·网络·后端·网络协议·tcp/ip·计算机网络·spring
_未知_开摆2 小时前
2020年蓝桥杯Java B组第二场题目+部分个人解析
java·经验分享·后端·程序人生·蓝桥杯
m0_748234522 小时前
Spring Boot整合WebSocket
spring boot·后端·websocket
m0_748232392 小时前
SpringBoot Maven 项目 pom 中的 plugin 插件用法整理
spring boot·后端·maven
Asthenia04123 小时前
深入解析消息持久化实现机制:基于 `LocalMqBrokerPersist` 的简化实现
后端
yuhaiqiang3 小时前
解密如何快速搭建一套虚拟商品交易系统,推荐这个神奇的开源项目
后端
xidianhuihui3 小时前
go如何排查某个依赖是哪里引入的
开发语言·后端·golang
姜来可期3 小时前
【Golang】go语言异常处理快速学习
开发语言·笔记·后端·学习·golang
Toormi4 小时前
Go 1.24版本在性能方面有哪些提升?
开发语言·后端·golang
飘零未归人4 小时前
SpringBoot 整合mongoDB并自定义连接池,实现多数据源配置
spring boot·后端·mongodb