如何在SpringBoot中使用Guava Cache创建一个基于HikariCP和缓存的jdbc连接池?

x33g5p2x  于2022-09-21 转载在 Spring  
字(8.9k)|赞(0)|评价(0)|浏览(364)

Hikari连接池在性能和配置的灵活性方面是目前最好的。Hikari创建JDBC连接池并管理其生命周期。今天我将展示我们如何使用Hikari来创建JDBC连接池,以及如何创建多个连接池并将其缓存在google guava缓存中。

在云原生开发中,你可以有一个中央微服务,负责维护与多个数据库的通信。

与多个数据库的通信,每个数据库对应一个特定的客户。这篇博文展示了我们如何在运行时创建Hikari数据源,并在数据源缓存中管理多个Hikari数据源。为此目的,使用google guava缓存。这是因为我们可以使用一个Hikari数据源与一个特定的数据库连接。这个解决方案提供了一种连接多个客户端来连接多个数据库的方法。你可以把它想象成一个中间件,为N个客户和M个数据库管理Hikari连接池(N * M)。

为什么是HikariCP ?

HikariCP的一些好特点是

  1. 我们可以在Hikari数据源创建后动态地改变少数配置参数,如JDBC连接数。
  2. 我们可以改变JDBC连接的凭证(用户,密码)。Hikari将在现有的连接完成工作后改变现有连接中的凭证。

This blog对HikariCP进行了深入的分析,还展示了如何使用HikariCP创建JDBC连接池。

如何使用HikariCP创建一个JDBC连接池?

Hikari是一个高度可配置的API。使用Hikari,我们可以配置JDBC连接,从如何创建连接池到连接保持多长时间。有许多配置是可能的。首先,我们需要创建一个Hikari配置。使用该配置,我们可以创建一个Hikari数据源。一个数据源将一个连接池包裹在其中。一旦数据源被创建,我们就可以简单地从数据源中获取一个连接。我们甚至没有意识到Hikari正在管理一个连接池,并从连接池中回馈一个开放和自由的连接对象。

Hikari配置

首先,我们需要创建一个Hikari配置,告诉Hikari如何管理每个JDBC连接和整个JDBC连接池。请注意,每个连接池提供对一个数据库的连接。在Hikari中,我们称它为数据源。

我们可以在application.properties文件中定义这些参数,如下所示。

application.properties文件

hikari.cp.conf.autoCommit=true

hikari.cp.conf.connectionTimeout=300000

hikari.cp.conf.idleTimeout=600000

hikari.cp.conf.maxLifetime=1800000

hikari.cp.conf.minimumIdle=2

hikari.cp.conf.maximumPoolSize=12

然后我们可以在spring配置文件中初始化该配置。该配置将在运行时被应用程序使用,以使用Hikari创建一个JDBC连接池。

import java.unit.Properties;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.annotation.Bean;
import org.springframework.beans.factory.annotation.Configuration;
import org.springframework.beans.factory.annotation.Profile;
 
@Configuration
@Profile("cloud")
public class MyAppConfiguration {

    @Bean(name = "hikariDsConfiguration")
    public Properties  hikariDsConfiguration(
      @Value("${hikari.cp.conf.autoCommit}") String autoCommit ,
      @Value("${hikari.cp.conf.connectionTimeout}") String connectionTimeout ,
      @Value("${hikari.cp.conf.idleTimeout}") String idleTimeout,
      @Value("${hikari.cp.conf.maxLifetime}") String maxLifetime,
      @Value("${hikari.cp.conf.minimumIdle}") String minimumIdle,
      @Value("${hikari.cp.conf.maximumPoolSize}") String maximumPoolSize) {
         Properties properties = new  Properties();
          properties.setProperty("autoCommit", autoCommit);
          properties.setProperty("connectionTimeout", connectionTimeout);
          properties.setProperty("idleTimeout", idleTimeout); 
          properties.setProperty("maxLifetime", maxLifetime); 
          properties.setProperty("minimumIdle", minimumIdle); 
          properties.setProperty("maximumPoolSize", maximumPoolSize);
        return properties;    
    }
}

使用这个Hikari配置,我们可以创建一个Hikari数据源。在创建数据源的时候,HikariCP将建立配置中定义的最低数量的JDBC连接。在Hikari数据源被创建后,我们可以要求对数据源进行JDBC连接。我们将在JdbcManager类中创建数据源。

缓存配置

同样的,为了提供google guava缓存配置,我们可以在应用程序中定义参数值。属性

application.properties文件

datasource.cache.maximumSize=1000

datasource.cache.expireAfterWrite=10

datasource.cache.timeUnit=HOURS

在同一个spring配置文件中,我们正在初始化guava缓存。

import java.unit.concurrent.TimeUnit; 
import javax.sql.Datasource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.annotation.Bean;
import org.springframework.beans.factory.annotation.Configuration;
import org.springframework.beans.factory.annotation.Profile;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
 
 
@Configuration
@Profile("cloud")
public class MyAppConfiguration {


    @Bean(name= "hikariDataSourceCache")
    public cache<DataSourceKey, DataSource>  hikariDataSourceCache (
      @Value("${datasource.cache.maximumSize}") int maximumSize , 
      @Value(" ${datasource.cache.expireAfterWrite}") long  expireAfterWrite,  
      @Value(" ${datasource.cache.timeUnit}") String timeUnit)  {
          return CacheBuilder.newBuilder()
                                          .maximumSize(maximumSize)
                                          .expireAfterWrite(expireAfterWrite, TimeUnit.valueOf( timeUnit))
                                          .build();
    } 
}

由于我们想在缓存中存储许多数据源,数据源对象将是键值对的值。我们将需要定义一个键,为此我们要定义我们自己的键对象。DataSourceKey类提供了键的定义。由于我们在数据源缓存中使用这个对象作为key,我们需要覆盖equals和hashCode,这样guava缓存就可以正确地进行key比较。

缓存键

import java.unit.Properties;

public class DataSourceKey {
   private String clientID;
   private String dbHostUrl;
   private String userName;
   private String password;
   private Properties connProperties;  

   public DataSourceKey(String clientID,String dbHostUrl, String userName,
  String password) {
      this.clientID =  clientID;
      this.dbHostUrl = dbHostUrl;
      this.userName = userName;
      this.password = password;
      this.connProperties = generateConnProperties(dbHostUrl,userName,password);
   }
   //add getters
 
   public String getKey() { 
return clientID + "_" +  dbHostUrl + "_" +   userName;
   }
 
   private Properties generateConnProperties(String dbHostUrl, String userName, 
  String password ) {
      connectionProperties = new Properties();
      connectionProperties.setProperty("jdbc-url", dbHostUrl);
      connectionProperties.setProperty("user", userName ); 
      connectionProperties.setProperty("password", password );  
      return connectionProperties;
   }

 @Override
   public boolean equals(Object obj) {
       if(obj == null || !(obj instanceof DatasourceKey)) { return false; }
       if(obj == this) {return true};

       DataSourceKey foreignObject = (DataSourceKey) obj;
       if(foreignObject.getClientID().equals(this.clientID) &&
          foreignObject.getDbHostUrl().equals(this.dbHostUrl) &&
          foreignObject.getUserName().equals(this.userName) &&
          foreignObject.getPassword().equals(this.password) { 
            return true;
       } else {
            return false;
       } 
  }

  @Override 
  public int hashCode() {
       String hashString = this.clientID + this.dbHostUrl + 
this.userName +  this.password;
       return  hashString.hashCode();
  } 
}

现在让我们创建一个JdbcManager类,创建数据源缓存,Hikari数据源,并使用数据源提供JDBC连接。

import java.sql.Connection;
import java.util.Properties;
import java.util.concurrent.Callable;
import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.google.common.cache.Cache;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

@ Service  
public class JdbcManager {
    @Autowired
     private Cache<DataSourcekey, DataSource> hikariDataSourceCache;
    @Autowired
     private Properties hikariDSConfiguration;

     public  JdbcManager() {
     }
     public Connection getConnection( String clientID, String dbHostUrl, String userName, 
                                                         String password)  throws RuntimeException {
        DataSourceKey  dataSourceKey = new  DataSourceKey( clientId, dbHostUrl, userName, password );
        try {
           DataSource dataSouce = hikariDataSourceCache.get(dataSourceKey,  
createDataSource( dataSourceKey , hikariDSConfiguration));
           return dataSource.getConnection();
        } catch (Exception e) { 
          throw new RuntimeException(e.getMessage());
        }
     }
     Callable <DataSource> createDataSource(DataSourceKey dataSourceKey,
                                                                      Properties hikariDSConfiguration) {
        return new Callable<DataSource>() {
          @Override
          public DataSource call() throws Exception {
              HikariConfig config = new  HikariConfig(hikariDSConfiguration);
              config.setPoolName(dataSourceKey.getKey());
              config.setJdbcUrl(dataSourceKey.getConnectionProperties().getProperty("jdbc-url"));
              config.setUsername(dataSourceKey.getConnectionProperties().getProperty("user")); 
              config.setPassword(dataSourceKey.getConnectionProperties().getProperty("password"));  
              DataSource dataSource = new HikariDataSource(config);
              return  dataSource;
          }
        };
     }
}

执行查询

QueryExecuter类是一个REST控制器,它处理所有传入的请求,并使用JdbcManager服务来执行数据库的查询。这里QueryExecuter被用来获取所有雇员的详细信息。Employee类是一个普通的POJO,所以这里没有明确显示。当*/path/getEmployees* REST端点被调用时,雇员的列表被作为响应发送。Spring boot自动将雇员类转换为JSON。在这里,雇员列表将作为一个JSON数组被发送。

注意,我没有在这里指定数据库的URL、主机和端口。我把它留给你,开发者,从一个安全的地方获取这些细节。请记住,该解决方案是为多个数据库管理JDBC池,我们可以假设为客户提供的每个云应用实例都有自己的数据库。即使只有一个由不同用户共享的云应用,用户数据之间也应该有一些数据隔离。这可能是一个不同的数据库或同一数据库,不同的表或模式。clientID是一个标识符,使用它我们可以识别客户并可以获取相应的数据库细节。

import javax.ws.rs.core.Response;
import org.springframework.web.bind.annotation.RestController;  
import org.springframework.web.bind.annotation.RequestMapping; 
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestHeader; 
 
@RestController
public class QueryExecuter {

    private  JdbcManager  jdbcManager;
   @Autowired
   public  QueryExecuter( JdbcManager  jdbcManager) {
    //as its autowired the jdbc manager will be injected to the controller by spring boot 
    this.jdbcManager = jdbcManager ; 
   }

  @RequestMapping(value="/path/getEmployees", method= {RequestMethod.GET})
   public Response getResult(@RequestHeader HttpHeaders headers) {
     //fetch clientId from session context
     //fetch host, user, pass from some secure store for the clientId
     String SQL_QUERY = "select * from emp";
     List<Employee> employees = null; 
     try( Connection jdbcConn = 
jdbcManager.getConnection(<clientId>,<dbHostUrl>,<userName>,<password>);
       PreparedStatement pst = jdbcConn.prepareStatement( SQL_QUERY );
        ResultSet rs = pst.executeQuery();) {
            employees = new ArrayList<>();
            Employee employee;
            while ( rs.next() ) {
                employee = new Employee();
                employee.setEmpNo( rs.getInt( "empno" ) );
                employee.setEname( rs.getString( "ename" ) );
                employee.setJob( rs.getString( "job" ) );
                employee.setMgr( rs.getInt( "mgr" ) );
                employee.setHiredate( rs.getDate( "hiredate" ) );
                employee.setSal( rs.getInt( "sal" ) );
                employee.setComm( rs.getInt( "comm" ) );
                employee.setDeptno( rs.getInt( "deptno" ) );
                employees.add( employee );
            }
       }
       return  Response.ok(employees).build();
}

记住你将需要一个JDBC驱动插件来与你的数据库进行通信。该插件取决于你使用的数据库。无论使用哪种类型的数据库,这段代码都可以被利用。

相关文章

微信公众号

最新文章

更多