Spring AbstractRoutingDatasource实现多数据源指南

如何同时连接到多个数据库并获取新的数据库连接?在这篇简短的文章中,我们将了解 Spring 的AbstractRoutingDatasource ,以根据当前上下文动态确定 实际的DataSource。

因此,我们将看到我们可以将数据源查找逻辑保留在数据访问代码之外。

什么是AbstractRoutingDatasource
AbstractRoutingDatasource需要信息来了解要路由到哪个实际数据源。该信息通常称为上下文。

虽然与AbstractRoutingDatasource一起使用的Context可以是任何对象,但使用枚举来定义它们。在我们的示例中,我们将使用ClientDatabase的概念作为上下文,并进行以下实现:

public enum ClientDatabase {
    CLIENT_A, CLIENT_B
}

值得注意的是,在实践中,上下文可以是对相关领域有意义的任何内容。

例如,另一个常见用例涉及使用环境的概念来定义上下文。在这种情况下,上下文可以是包含PRODUCTION、DEVELOPMENT和TESTING 的枚举。

什么是Context Holder
上下文持有者实现是一个容器,它将当前上下文存储为 ThreadLocal 引用。

除了保存引用外,它还应包含用于设置、获取和清除引用的静态方法。AbstractRoutingDatasource 将查询 ContextHolder 中的上下文,然后使用上下文查找实际的数据源。

在此使用 ThreadLocal 至关重要,这样上下文就与当前执行线程绑定。

采用这种方法非常重要,这样当数据访问逻辑跨越多个数据源并使用事务时,行为才会可靠:

public class ClientDatabaseContextHolder {

    private static ThreadLocal<ClientDatabase> CONTEXT
      = new ThreadLocal<>();

    public static void set(ClientDatabase clientDatabase) {
        Assert.notNull(clientDatabase, "clientDatabase cannot be null");
        CONTEXT.set(clientDatabase);
    }

    public static ClientDatabase getClientDatabase() {
        return CONTEXT.get();
    }

    public static void clear() {
        CONTEXT.remove();
    }
}

什么是Datasource Router?
我们定义 ClientDataSourceRouter 来扩展 Spring AbstractRoutingDataSource。我们实现必要的 determineCurrentLookupKey 方法,以查询我们的 ClientDatabaseContextHolder 并返回相应的键。

AbstractRoutingDataSource 实现会为我们处理其余工作,并透明地返回相应的数据源:

public class ClientDataSourceRouter
  extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return ClientDatabaseContextHolder.getClientDatabase();
    }
}

如何配置?
首先,我们在pom.xml中将spring-context、spring-jdbc、spring-test和h2声明为依赖项:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>6.1.1</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>6.1.1</version>
    </dependency>

    <dependency> 
        <groupId>org.springframework</groupId> 
        <artifactId>spring-test</artifactId>
        <version>6.1.1</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <version>2.1.214</version>
    </dependency>
</dependencies>

如果您使用 Spring Boot,我们可以使用 Spring Data 和 Test 的启动器:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency> 
        <groupId>org.springframework.boot</groupId> 
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <version>2.1.214</version>
        <scope>test</scope>
    </dependency>
</dependencies>


我们需要一个上下文到数据源对象的映射表来配置 AbstractRoutingDataSource。如果没有设置上下文,我们还可以指定默认数据源。

我们使用的数据源可以来自任何地方,但通常会在运行时创建或使用 JNDI 查找:

@Configuration
public class RoutingTestConfiguration {

    @Bean
    public ClientService clientService() {
        return new ClientService(new ClientDao(clientDatasource()));
    }
 
    @Bean
    public DataSource clientDatasource() {
        Map<Object, Object> targetDataSources = new HashMap<>();
        DataSource clientADatasource = clientADatasource();
        DataSource clientBDatasource = clientBDatasource();
        targetDataSources.put(ClientDatabase.CLIENT_A, 
          clientADatasource);
        targetDataSources.put(ClientDatabase.CLIENT_B, 
          clientBDatasource);

        ClientDataSourceRouter clientRoutingDatasource 
          = new ClientDataSourceRouter();
        clientRoutingDatasource.setTargetDataSources(targetDataSources);
        clientRoutingDatasource.setDefaultTargetDataSource(clientADatasource);
        return clientRoutingDatasource;
    }

    // ...
}

使用 Spring Boot 时,可以在 application.properties 文件中配置数据源(即 ClientA 和 ClientB):

database details for CLIENT_A
client-a.datasource.name=CLIENT_A
client-a.datasource.script=SOME_SCRIPT.sql

database details for CLIENT_B
client-b.datasource.name=CLIENT_B
client-b.datasource.script=SOME_SCRIPT.sql

然后,您就可以创建 POJO,为数据源保存属性:

@Component
@ConfigurationProperties(prefix = "client-a.datasource")
public class ClientADetails {

    private String name;
    private String script;

   
// Getters & Setters
}

并使用它们来构建数据源 Bean:

@Autowired
private ClientADetails clientADetails;
@Autowired
private ClientBDetails clientBDetails;

private DataSource clientADatasource() {
EmbeddedDatabaseBuilder dbBuilder = new EmbeddedDatabaseBuilder();
    return dbBuilder.setType(EmbeddedDatabaseType.H2)
      .setName(clientADetails.getName())
      .addScript(clientADetails.getScript())
      .build();
}

private DataSource clientBDatasource() {
EmbeddedDatabaseBuilder dbBuilder = new EmbeddedDatabaseBuilder();
    return dbBuilder.setType(EmbeddedDatabaseType.H2)
      .setName(clientBDetails.getName())
      .addScript(clientBDetails.getScript())
      .build();
}

如何使用
使用 AbstractRoutingDataSource 时,我们首先设置上下文,然后执行操作。我们使用的服务层会将上下文作为一个参数,在将其委托给数据访问代码并在调用后清除上下文之前对其进行设置。

作为在服务方法中手动清除上下文的替代方法,AOP 切点可以处理清除逻辑。

重要的是要记住上下文是线程绑定的,尤其是在数据访问逻辑将跨越多个数据源和事务的情况下:

public class ClientService {

    private ClientDao clientDao;

    // standard constructors

    public String getClientName(ClientDatabase clientDb) {
        ClientDatabaseContextHolder.set(clientDb);
        String clientName = this.clientDao.getClientName();
        ClientDatabaseContextHolder.clear();
        return clientName;
    }
}

结论
在本教程中,我们学习了如何使用 Spring AbstractRoutingDataSource 的示例。我们使用客户端的概念实现了一个解决方案,其中每个客户端都有自己的数据源。