我有一个相当繁重的java webapp,每秒可以处理数千个请求,它使用一个主Postgresql数据库,通过流式(异步)复制将自身复制到一个辅助(只读)数据库.

因此,考虑到复制时间很短,我使用URL将请求从主数据库分离到辅助数据库(只读),以避免对bug主数据库的只读调用.

NOTE: I use one sessionFactory with a RoutingDataSource provided by spring that looks up db to use based on a key. I am interested in multitenancy as I am using hibernate 4.3.4 that supports it.

我有两个问题:

  1. 我不认为基于URL的拆分是有效率的
  2. 也许,不知何故,基于URL,我实现了某种程度的

我知道我可能不会得到一个完美的答案,因为这确实是一个宽泛的问题,但我只是想了解一下你的观点.

我的团队里有很多人:

  • 春天4
  • 冬眠
  • 夸脱2.2.
  • Java7/Tomcat7

请感兴趣.提前谢谢.

推荐答案

Spring事务路由

首先,我们将创建一个DataSourceType Java枚举,定义我们的事务路由选项:

public enum  DataSourceType {
    READ_WRITE,
    READ_ONLY
}

为了将读写事务路由到主 node ,将只读事务路由到副本 node ,我们可以定义连接到主 node 的ReadWriteDataSource和连接到副本 node 的ReadOnlyDataSource.

读写和只读事务路由由Spring AbstractRoutingDataSource抽象完成,由TransactionRoutingDatasource实现,如下图所示:

Read-write and read-only transaction routing with Spring

TransactionRoutingDataSource很容易实现,如下所示:

public class TransactionRoutingDataSource 
        extends AbstractRoutingDataSource {

    @Nullable
    @Override
    protected Object determineCurrentLookupKey() {
        return TransactionSynchronizationManager
            .isCurrentTransactionReadOnly() ?
            DataSourceType.READ_ONLY :
            DataSourceType.READ_WRITE;
    }
}

基本上,我们判断存储当前事务上下文的Spring TransactionSynchronizationManager类,以判断当前运行的Spring事务是否为只读.

determineCurrentLookupKey方法返回用于 Select 读写或只读JDBC DataSource的鉴别器值.

Spring读写和只读JDBC数据源配置

DataSource配置如下所示:

@Configuration
@ComponentScan(
    basePackages = "com.vladmihalcea.book.hpjp.util.spring.routing"
)
@PropertySource(
    "/META-INF/jdbc-postgresql-replication.properties"
)
public class TransactionRoutingConfiguration 
        extends AbstractJPAConfiguration {

    @Value("${jdbc.url.primary}")
    private String primaryUrl;

    @Value("${jdbc.url.replica}")
    private String replicaUrl;

    @Value("${jdbc.username}")
    private String username;

    @Value("${jdbc.password}")
    private String password;

    @Bean
    public DataSource readWriteDataSource() {
        PGSimpleDataSource dataSource = new PGSimpleDataSource();
        dataSource.setURL(primaryUrl);
        dataSource.setUser(username);
        dataSource.setPassword(password);
        return connectionPoolDataSource(dataSource);
    }

    @Bean
    public DataSource readOnlyDataSource() {
        PGSimpleDataSource dataSource = new PGSimpleDataSource();
        dataSource.setURL(replicaUrl);
        dataSource.setUser(username);
        dataSource.setPassword(password);
        return connectionPoolDataSource(dataSource);
    }

    @Bean
    public TransactionRoutingDataSource actualDataSource() {
        TransactionRoutingDataSource routingDataSource = 
            new TransactionRoutingDataSource();

        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put(
            DataSourceType.READ_WRITE, 
            readWriteDataSource()
        );
        dataSourceMap.put(
            DataSourceType.READ_ONLY, 
            readOnlyDataSource()
        );

        routingDataSource.setTargetDataSources(dataSourceMap);
        return routingDataSource;
    }

    @Override
    protected Properties additionalProperties() {
        Properties properties = super.additionalProperties();
        properties.setProperty(
            "hibernate.connection.provider_disables_autocommit",
            Boolean.TRUE.toString()
        );
        return properties;
    }

    @Override
    protected String[] packagesToScan() {
        return new String[]{
            "com.vladmihalcea.book.hpjp.hibernate.transaction.forum"
        };
    }

    @Override
    protected String databaseType() {
        return Database.POSTGRESQL.name().toLowerCase();
    }

    protected HikariConfig hikariConfig(
            DataSource dataSource) {
        HikariConfig hikariConfig = new HikariConfig();
        int cpuCores = Runtime.getRuntime().availableProcessors();
        hikariConfig.setMaximumPoolSize(cpuCores * 4);
        hikariConfig.setDataSource(dataSource);

        hikariConfig.setAutoCommit(false);
        return hikariConfig;
    }

    protected HikariDataSource connectionPoolDataSource(
            DataSource dataSource) {
        return new HikariDataSource(hikariConfig(dataSource));
    }
}

/META-INF/jdbc-postgresql-replication.properties资源文件提供了读写和只读JDBC DataSource组件的配置:

hibernate.dialect=org.hibernate.dialect.PostgreSQL10Dialect

jdbc.url.primary=jdbc:postgresql://localhost:5432/high_performance_java_persistence
jdbc.url.replica=jdbc:postgresql://localhost:5432/high_performance_java_persistence_replica

jdbc.username=postgres
jdbc.password=admin

jdbc.url.primary属性定义主 node 的URL,而jdbc.url.replica属性定义副本 node 的URL.

readWriteDataSource Spring组件定义读写JDBCDataSource,而readOnlyDataSource组件定义只读JDBCDataSource.

请注意,读写和只读数据源都使用HikariCP进行连接池.

actualDataSource充当读写和只读数据源的门面,并使用TransactionRoutingDataSource实用程序实现.

readWriteDataSource使用DataSourceType.READ_WRITE键注册,readOnlyDataSource使用DataSourceType.READ_ONLY键注册.

因此,当执行读写@Transactional方法时,将使用readWriteDataSource,而当执行@Transactional(readOnly = true)方法时,将使用readOnlyDataSource.

请注意,additionalProperties方法定义了hibernate.connection.provider_disables_autocommit Hibernate属性,我将其添加到Hibernate中,以推迟资源本地JPA事务的数据库获取.

不仅hibernate.connection.provider_disables_autocommit允许您更好地利用数据库连接,而且这是我们让这个示例工作的唯一方法,因为在没有这种配置的情况下,连接是在调用determineCurrentLookupKey方法TransactionRoutingDataSource之前获得的.

构建JPA EntityManagerFactory所需的其余Spring组件由AbstractJPAConfiguration基类定义.

基本上,actualDataSource由数据源代理进一步包装,并提供给JPA EntityManagerFactory.您可以查看source code on GitHub以了解更多详细信息.

测试时间

为了判断事务路由是否有效,我们将通过在postgresql.conf配置文件中设置以下属性来启用PostgreSQL查询日志(log):

log_min_duration_statement = 0
log_line_prefix = '[%d] '

log_min_duration_statement属性设置用于记录所有PostgreSQL语句,而第二个属性将数据库名称添加到SQL日志(log)中.

所以,当调用newPostfindAllPostsByTitle方法时,如下所示:

Post post = forumService.newPost(
    "High-Performance Java Persistence",
    "JDBC", "JPA", "Hibernate"
);

List<Post> posts = forumService.findAllPostsByTitle(
    "High-Performance Java Persistence"
);

我们可以看到PostgreSQL记录了以下消息:

[high_performance_java_persistence] LOG:  execute <unnamed>: 
    BEGIN

[high_performance_java_persistence] DETAIL:  
    parameters: $1 = 'JDBC', $2 = 'JPA', $3 = 'Hibernate'
[high_performance_java_persistence] LOG:  execute <unnamed>: 
    select tag0_.id as id1_4_, tag0_.name as name2_4_ 
    from tag tag0_ where tag0_.name in ($1 , $2 , $3)

[high_performance_java_persistence] LOG:  execute <unnamed>: 
    select nextval ('hibernate_sequence')

[high_performance_java_persistence] DETAIL:  
    parameters: $1 = 'High-Performance Java Persistence', $2 = '4'
[high_performance_java_persistence] LOG:  execute <unnamed>: 
    insert into post (title, id) values ($1, $2)

[high_performance_java_persistence] DETAIL:  
    parameters: $1 = '4', $2 = '1'
[high_performance_java_persistence] LOG:  execute <unnamed>: 
    insert into post_tag (post_id, tag_id) values ($1, $2)

[high_performance_java_persistence] DETAIL:  
    parameters: $1 = '4', $2 = '2'
[high_performance_java_persistence] LOG:  execute <unnamed>: 
    insert into post_tag (post_id, tag_id) values ($1, $2)

[high_performance_java_persistence] DETAIL:  
    parameters: $1 = '4', $2 = '3'
[high_performance_java_persistence] LOG:  execute <unnamed>: 
    insert into post_tag (post_id, tag_id) values ($1, $2)

[high_performance_java_persistence] LOG:  execute S_3: 
    COMMIT
    
[high_performance_java_persistence_replica] LOG:  execute <unnamed>: 
    BEGIN
    
[high_performance_java_persistence_replica] DETAIL:  
    parameters: $1 = 'High-Performance Java Persistence'
[high_performance_java_persistence_replica] LOG:  execute <unnamed>: 
    select post0_.id as id1_0_, post0_.title as title2_0_ 
    from post post0_ where post0_.title=$1

[high_performance_java_persistence_replica] LOG:  execute S_1: 
    COMMIT

使用high_performance_java_persistence前缀的日志(log)语句在主 node 上执行,而使用high_performance_java_persistence_replica前缀的日志(log)语句在副本 node 上执行.

所以,一切都像一个魅力!

所有的源代码都可以在我的High-Performance Java Persistence GitHub存储库中找到,所以你也可以try 一下.

结论

您需要确保为连接池设置了正确的大小,因为这会产生巨大的差异.为此,我建议使用Flexy Pool.

您需要非常勤奋,并确保相应地标记所有只读事务.不寻常的是,只有10%的事务是只读的.是因为您有这样一个写操作最多的应用程序,还是因为您使用的是只发出查询语句的写事务?

对于批处理,您肯定需要读写事务,因此请确保启用JDBC批处理,如下所示:

<property name="hibernate.order_updates" value="true"/>
<property name="hibernate.order_inserts" value="true"/>
<property name="hibernate.jdbc.batch_size" value="25"/>

对于批处理,您还可以使用单独的DataSource,该DataSource使用连接到主 node 的不同连接池.

只需确保所有连接池的总连接大小小于PostgreSQL配置的连接数.

每个批处理作业(job)必须使用专用事务,因此请确保使用合理的批处理大小.

此外,您希望持有锁并尽快完成事务.如果批处理程序正在使用并发处理工作进程,请确保关联的连接池大小等于工作进程的数量,这样他们就不会等待其他人释放连接.

Postgresql相关问答推荐

Postgis超过2个表的查询在运行时爆炸

在Ubuntu 18.04 Bionic上安装PostgreSQL(已删除回购)

PostGresql :正则表达式 Select 其中只有一个正斜杠的行

如何使 Postgres 优化引擎具有确定性

Upper() 不会大写重音字符

无法从外部连接到在 AWS EC2 实例上运行的 Postgres

PL/pgSQL 中 PL/SQL %ISOPEN 的类似功能是什么?

如何在 PostgreSQL 的回归测试中测试 TYPE 发送和接收函数

如何将两条线几何连接成一条记录?

PostgreSQL 返回准确或最接近查询日期的日期

获取 OperationalError: FATAL: sorry, too many clients already using psycopg2

knex: Select 特定日期范围内的行

使用 try/except 与 psycopg2 或with closing?

org.postgresql.util.PSQLException:错误:relation "app_user" does not exist

在查询中的复合外键/主键列上连接表

如何在容器内创建 postgres 扩展?

在远程机器上Restore dump

SQL:列为某个值时的唯一约束

使用 pg-promise 插入多条记录

psql 致命角色不存在