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抽象类实现了动态添加数据库路由的方法,它允许应用程序在运行时根据某些条件(如当前用户、事务类型等)动态选择不同的数据源。这在多租户系统、读写分离等场景中非常有用。
主要方法
- determineCurrentLookupKey() : 这是一个抽象方法,子类必须实现它。该方法返回当前线程上下文中用于查找数据源的键。Spring 会调用这个方法来决定使用哪个数据源。
- 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 会:- 开启事务。
- 将数据源绑定到当前线程的上下文中(通过
TransactionSynchronizationManager
)。
-
在
a
方法中调用b
方法时:b
方法切换数据源(通过DynamicDataSourceContextHolder.setObjectDataSourceKey()
)。- Spring 的动态数据源(
AbstractRoutingDataSource
)会从ThreadLocal
中获取当前数据源。 - 由于
b
方法没有开启新事务,数据源切换会生效。
-
在
a
方法执行后,Spring 会:- 提交或回滚事务。
- 清理线程上下文中的事务资源。
关键点:注解模式下,Spring 的代理机制会确保事务的开启、提交、回滚和资源清理是自动完成的,且数据源切换逻辑与事务管理逻辑是解耦的