vlambda博客
学习文章列表

读书笔记《hands-on-high-performance-with-spring-5》了解Spring数据库交互

Understanding Spring Database Interactions

在前面的章节中,我们了解了 Spring 的核心特性,例如 依赖注入 (DI) 及其配置。我们还看到了如何借助 Spring 面向方面编程 (AOP) 实现可重用代码。我们了解了如何在 Spring Model-View-Controller (MVC) 的帮助下开发松散耦合的 Web 应用程序,以及如何优化 Spring MVC 实现以取得更好的结果使用 asynchronous 特性、多线程和身份验证缓存。

在本章中,我们将学习与 Spring Framework 的数据库交互。数据库交互是应用程序性能的最大瓶颈。 Spring Framework 直接支持所有主要的数据访问技术,例如 Java 数据库连接 (JDBC)、任何对象-关系映射 (ORM ) 框架(例如 Hibernate)、Java Persistence API (JPA) 等。我们可以选择任何一种数据访问技术来持久化我们的应用程序数据。在这里,我们将探索与 Spring JDBC 的数据库交互。我们还将了解 Spring JDBC 的常见性能陷阱和数据库设计的最佳实践。然后我们将看看 Spring 事务管理和最佳连接池配置。

本章将涵盖以下主题:

  • Spring JDBC configuration
  • Database design for optimal performance
  • Transaction management
  • Declarative ACID using @Transactional
  • Optimal isolation levels
  • Optimal fetch size
  • Optimal connection pooling configuration
  • Tomcat JDBC connection pool versus HikariCP
  • Database design best practices

Spring JDBC configuration

如果不使用 JDBC,我们无法仅使用 Java 连接到数据库。 JDBC 将直接或间接参与连接数据库。但是如果 Java 程序员直接使用核心 JDBC,他们就会面临一些问题。让我们看看这些问题是什么。

Problems with core JDBC

下面说明了我们在使用核心 JDBC API 时必须面对的问题:

    String query = "SELECT COUNT(*) FROM ACCOUNT";
    
    try (Connection conn = dataSource.getConnection();
        Statement statement = conn.createStatement(); 
        ResultSet rsltSet = statement.executeQuery(query)) 
        {
        if(rsltSet.next()){ 
            int count = rsltSet.getInt(1);
            System.out.println("count : " + count);
        }
      } catch (SQLException e) {
        // TODO Auto-generated catch block
            e.printStackTrace();
      }      
  }

在前面的示例中,我突出显示了一些代码。只有粗体格式的代码很重要;其余的是管道代码。因此,我们每次都必须编写冗余代码来执行数据库操作。

让我们看看核心 JDBC 的其他一些问题:

  • JDBC API exceptions are checked that forces the developers to handle errors, which increases the code, as well as the complexity, of the application
  • In JDBC, we have to close the database connection; if the developer forgets to close the connection, then we get some connection issues in our application

Solving problems with Spring JDBC

为了克服核心 JDBC 的上述问题,Spring Framework 提供了与 Spring JDBC 模块的出色数据库集成。 Spring JDBC 提供了 JdbcTemplate 类,它可以帮助我们去除管道代码,也可以帮助开发人员只专注于 SQL 查询和参数。我们只需要使用 dataSource 配置 JdbcTemplate 并编写如下代码:

jdbcTemplate = new JdbcTemplate(dataSource);
int count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM CUSTOMER", Integer.class);

正如我们在前面的示例中看到的,Spring 提供了使用 JDBC 模板处理数据库访问的简化。 JDBC 模板在内部使用核心 JDBC 代码,并提供了一种新的高效的数据库处理方式。与核心 JDBC 相比,Spring JDBC 模板 具有以下优点:

  • The JDBC template cleans up the resources automatically, by releasing database connections
  • It converts the core JDBC SQLException into RuntimeExceptions, which provides a better error detection mechanism
  • The JDBC template provides various methods to write the SQL queries directly, so it saves a lot of work and time

下图显示了 Spring JDBC 模板的高级概述:

读书笔记《hands-on-high-performance-with-spring-5》了解Spring数据库交互

Spring JDBC 提供的各种访问数据库的方法如下:

  • JdbcTemplate
  • NamedParameterJdbcTemplate
  • SimpleJdbcTemplate
  • SimpleJdbcInsert
  • SimpleJdbcCall

Spring JDBC dependencies

以下是 pom.xml 文件中可用的 Spring JDBC 依赖项:

  • The following code is for the Spring JDBC dependency:
 <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-jdbc</artifactId>
   <version>${spring.framework.version}</version>
 </dependency>
  • The following code is for the PostgreSQL dependency:
 <dependency>
   <groupId>org.postgresql</groupId>
   <artifactId>postgresql</artifactId>
   <version>42.2.1</version>
 </dependency>

在前面的代码中,我们分别指定了 Spring JDBC 和 PostgreSQL 的依赖关系。其余的依赖项将由 Maven 自动解决。在这里,我使用 PostgreSQL 数据库进行测试,所以我添加了一个 PostgreSQL 依赖项。如果您正在使用其他一些 RDBMS,那么您应该相应地更改依赖项。

Spring JDBC example

在此示例中,我们使用的是 PostgreSQL 数据库。表架构如下:

CREATE TABLE account
(
  accountNumber numeric(10,0) NOT NULL, 
  accountName character varying(60) NOT NULL,
  CONSTRAINT accountNumber_key PRIMARY KEY (accountNumber)
)
WITH (
  OIDS=FALSE
);

我们将使用 DAO 模式进行 JDBC 操作,所以让我们创建一个 Java bean 来模拟我们的 Account 表:

package com.packt.springhighperformance.ch5.bankingapp.model;

public class Account {
  private String accountName;
  private Integer accountNumber;

  public String getAccountName() {
    return accountName;
  }

  public void setAccountName(String accountName) {
    this.accountName = accountName;
  }

  public Integer getAccountNumber() {
    return accountNumber;
  }

  public void setAccountNumber(Integer accountNumber) {
    this.accountNumber = accountNumber;
  }
  @Override
  public String toString(){
    return "{accountNumber="+accountNumber+",accountName
    ="+accountName+"}";
  }
}

下面的 AccountDao 接口声明了我们想要实现的操作:

public interface AccountDao { 
    public void insertAccountWithJdbcTemplate(Account account);
    public Account getAccountdetails();    
}

Spring bean配置类如下。 对于 bean 配置,只需使用 @Bean 注释对方法进行注释。当 JavaConfig 找到这样的方法时,它将执行该方法并将返回值注册为 BeanFactory 中的 bean。在这里,我们注册了 JdbcTemplatedataSourceAccountDao bean:

@Configuration
public class AppConfig{
  @Bean
  public DataSource dataSource() {
    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    // PostgreSQL database we are using...
    dataSource.setDriverClassName("org.postgresql.Driver");
    dataSource.setUrl("jdbc:postgresql://localhost:5432/TestDB");
    dataSource.setUsername("test");
    dataSource.setPassword("test");
    return dataSource;
  }

  @Bean
  public JdbcTemplate jdbcTemplate() {
    JdbcTemplate jdbcTemplate = new JdbcTemplate();
    jdbcTemplate.setDataSource(dataSource());
    return jdbcTemplate;
  }

  @Bean
  public AccountDao accountDao() {
    AccountDaoImpl accountDao = new AccountDaoImpl();
    accountDao.setJdbcTemplate(jdbcTemplate());
    return accountDao;
  }

}

在前面的配置文件中,我们创建了一个DriverManagerDataSource类的DataSource对象。此类提供了我们可以使用的 DataSource 的基本实现。我们还将 PostgreSQL 数据库 URL、用户名和密码作为属性传递给 dataSource bean。此外,dataSource bean 设置为 AccountDaoImpl bean,我们已准备好使用 Spring JDBC 实现。实现是松耦合的,如果我们想切换到其他实现或移动到另一个数据库服务器,那么我们只需要在 bean 配置中进行更改。这是 Spring JDBC 框架提供的主要优势之一。

这是 AccountDAO 的实现,我们使用 Spring JdbcTemplate 类将数据插入到表中:

@Repository
public class AccountDaoImpl implements AccountDao {
  private static final Logger LOGGER = 
  Logger.getLogger(AccountDaoImpl.class);
  
  private JdbcTemplate jdbcTemplate;

  public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;
  }

  @Override
  public void insertAccountWithJdbcTemplate(Account account) {
    String query = "INSERT INTO ACCOUNT (accountNumber,accountName) 
    VALUES (?,?)";
  
    Object[] inputs = new Object[] { account.getAccountNumber(), 
    account.getAccountName() };
    jdbcTemplate.update(query, inputs);
    LOGGER.info("Inserted into Account Table Successfully");
  }

  @Override
  public Account getAccountdetails() {
    String query = "SELECT accountNumber, accountName FROM ACCOUNT 
    ";
    Account account = jdbcTemplate.queryForObject(query, new 
    RowMapper<Account>(){
      public Account mapRow(ResultSet rs, int rowNum)
          throws SQLException {
            Account account = new Account();
            account.setAccountNumber(rs.getInt("accountNumber"));
            account.setAccountName(rs.getString("accountName")); 
            return account;
      }});
    LOGGER.info("Account Details : "+account);
    return account; 
  }
}

在前面的示例中,我们使用 org.springframework.jdbc.core.JdbcTemplate 类来访问持久性资源。 Spring JdbcTemplate 是 Spring JDBC 核心包中的中心类,提供了很多方法来执行查询并自动解析 ResultSet 以获取对象或对象列表。

以下是上述实现的测试类:

public class MainApp {

  public static void main(String[] args) throws SQLException {
    AnnotationConfigApplicationContext applicationContext = new                             
    AnnotationConfigApplicationContext(
    AppConfig.class);
    AccountDao accountDao = 
    applicationContext.getBean(AccountDao.class);
    Account account = new Account();
    account.setAccountNumber(101);
    account.setAccountName("abc");
    accountDao.insertAccountWithJdbcTemplate(account);
    accountDao.getAccountdetails();
    applicationContext.close();
  }
}

当我们运行之前的程序时,我们得到如下输出:

May 15, 2018 7:34:33 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@6d5380c2: startup date [Tue May 15 19:34:33 IST 2018]; root of context hierarchy
May 15, 2018 7:34:33 PM org.springframework.jdbc.datasource.DriverManagerDataSource setDriverClassName
INFO: Loaded JDBC driver: org.postgresql.Driver
2018-05-15 19:34:34 INFO AccountDaoImpl:36 - Inserted into Account Table Successfully
2018-05-15 19:34:34 INFO AccountDaoImpl:52 - Account Details : {accountNumber=101,accountName=abc}
May 15, 2018 7:34:34 PM org.springframework.context.support.AbstractApplicationContext doClose
INFO: Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@6d5380c2: startup date [Tue May 15 19:34:33 IST 2018]; root of context hierarchy

Database design for optimal performance

如今,借助现代工具和流程来设计数据库非常容易,但我们必须知道,它是我们应用程序中非常重要的一部分,它直接影响应用程序的性能。一旦使用不准确的数据库设计实现了应用程序,再修复它为时已晚。我们别无选择,只能购买昂贵的硬件来解决这个问题。因此,我们应该了解数据库表设计、数据库分区和良好索引的一些基本概念和最佳实践,以提高应用程序的性能。让我们看看开发高性能数据库应用程序的基本规则和最佳实践。

Table design

表设计类型可以规范化或非规范化,但每种类型都有其自身的优点。如果表设计规范化,就意味着消除了冗余数据,数据按照主键/外键关系进行逻辑存储,提高了数据的完整性。如果表设计是非规范化的,则意味着增加了数据冗余并在表之间创建了不一致的依赖关系。在非规范化类型中,查询的所有数据通常存储在表中的一行中;这就是为什么它可以更快地检索数据并提高查询性能。在规范化类型中,我们必须在查询中使用连接来从数据库中获取数据,并且由于连接,查询的性能会受到影响。我们应该使用规范化还是非规范化完全取决于我们的应用程序的性质和业务需求。通常,计划用于在线事务处理(OLTP)的数据库通常比计划用于在线分析处理(OLAP的数据库更规范化强>)。从性能的角度来看,规范化通常用于需要更多 INSERT/UPDATE/DELETE 操作的地方,而反规范化则用于更多< kbd>READ 操作是必需的。

Vertical partitioning of a table

在使用垂直分区时,我们将具有许多列的表拆分为具有特定列的多个表。例如,我们不能因为性能问题而定义非常宽的文本或二进制大对象(BLOB)数据列不经常查询的表。这些数据必须放在单独的表结构中,并且可以在查询表中使用指针。

下面是一个简单示例,说明我们如何在 customer 表上使用垂直分区并移动二进制数据类型列 customer_Image , 放到一个单独的表中:

CREATE TABLE customer
(
  customer_ID numeric(10,0) NOT NULL, 
  accountName character varying(60) NOT NULL,
  accountNumber numeric(10,0) NOT NULL,
  customer_Image bytea
);

垂直分区数据,如下:

CREATE TABLE customer
(
  customer_Id numeric(10,0) NOT NULL, 
  accountName character varying(60) NOT NULL,
  accountNumber numeric(10,0) NOT NULL
);

CREATE TABLE customer_Image
(
  customer_Image_ID numeric(10,0) NOT NULL, 
  customer_Id numeric(10,0) NOT NULL, 
  customer_Image bytea
);

在 JPA/Hibernate 中,我们可以轻松地将前面的示例映射到表之间的惰性一对多关系。 customer_Image 表的数据使用不频繁,可以设置为懒加载。当客户端请求关系的特定列时,将检索其数据。

Use indexing

我们应该为大表上的常用查询使用索引,因为索引功能是提高数据库模式读取性能的最佳方法之一。索引条目按排序顺序存储,这有助于处理 GROUP BYORDER BY 子句。如果没有索引,数据库必须在查询执行时执行排序操作。通过索引,我们可以最小化查询执行时间,提高查询性能,但是在表上创建索引时要小心;也有某些缺点。

我们不应该在频繁更新的表上创建太多索引,因为在对表进行任何数据修改时,索引也会更改。我们应该在一个表上最多使用四到五个索引。如果表是只读的,那么我们可以不用担心添加更多的索引。

以下是为您的应用程序构建最有效索引的指南,这些指南对每个数据库都有效:

  • In order to achieve the maximum benefits of indexes, we should use indexes on appropriate columns. Indexes should be used on those columns that are frequently used in WHERE, ORDER BY, or GROUP BY clauses in queries.
  • Always choose integer data type columns for indexing because they provide better performance than other data type columns. Keep indexes small because short indexes are processed faster in terms of I/O.
  • Clustered indexes are usually better for queries retrieving a range of rows. Non-clustered indexes are usually better for point queries.

Using the correct data type

数据类型决定了可以存储在数据库表列中的数据类型。当我们创建一个表时,我们应该根据它的存储要求为每列定义适当的数据类型。例如,一个 SMALLINT 占用 2 个字节的空间,而一个 INT 占用 4 个字节的空间。当我们定义 INT 数据类型时,这意味着我们必须每次都将所有 4 个字节存储到该列中。如果我们存储一个像 10 或 20 这样的数字,那么这就是浪费字节。这最终会使您的读取速度变慢,因为数据库必须读取磁盘的多个扇区。此外,选择正确的数据类型有助于我们将正确的数据存储到列中。例如,如果我们对列使用日期数据类型,那么数据库不允许在不代表日期的列中包含任何字符串和数字数据。

Defining column constraints

列约束对可以从表中插入/更新/删除的数据或数据类型实施限制。约束的全部目的是在 UPDATE/DELETE/INSERT 到表中保持数据完整性。但是,我们应该只在适当的地方定义约束;否则,我们将对性能产生负面影响。例如,定义 NOT NULL 约束不会在查询处理期间强加显着开销,但定义 CHECK 约束可能会对性能产生负面影响。

Using stored procedures

数据访问性能可以通过使用存储过程来处理数据库服务器中的数据以减少网络开销,也可以通过在应用程序中缓存数据以减少访问次数来调整。

Transaction management

数据库事务是任何应用程序的关键部分。数据库事务是被视为单个工作单元的一系列操作。这些操作要么完全完成,要么完全不生效。操作序列的管理称为事务管理。事务管理是任何面向 RDBMS 的企业应用程序的重要组成部分,以确保数据的完整性和一致性。事务的概念可以用四个关键属性来描述:原子性、一致性、隔离、和持久性(<强>酸)。

事务被描述为 ACID,代表以下内容:

  • Atomicity: A transaction should be treated as a single unit of operation, which means that either the entire sequence of operations is completed, or it takes no effect at all
  • Consistency: Once a transaction is completed and committed, then your data and resources will be in a consistent state that conforms to business rules
  • Isolation: If many transactions are being processed with the same dataset at the same time, then each transaction should be isolated from others to prevent data corruption
  • Durability: Once a transaction has completed, the results of the transaction are written to persistent storage and cannot be erased from the database due to system failure

Choosing a transaction manager in Spring

Spring 提供了不同的事务管理器,基于不同的平台。在这里,不同的平台意味着不同的持久化框架,例如 JDBC、MyBatis、Hibernate 和 Java Transaction API (JTA)。所以,我们必须相应地选择Spring提供的事务管理器。

下图描述了 Spring 提供的特定于平台的事务管理:

读书笔记《hands-on-high-performance-with-spring-5》了解Spring数据库交互

Spring 支持两种类型的事务管理:

  • Programmatic: This means that we can write our transactions using Java source code directly. This gives us extreme flexibility, but it is difficult to maintain.
  • Declarative: This means that we can manage transactions in either a centralized way, by using XML, or in a distributed way, by using annotations.

Declarative ACID using @Transactional

强烈建议使用声明式事务,因为它们将事务管理排除在业务逻辑之外并且易于配置。让我们看一个基于注解的声明式事务管理的例子。

让我们使用 Spring JDBC 部分中使用的相同示例。在我们的示例中,我们使用 JdbcTemplate 进行数据库交互。因此,我们需要在 Spring 配置文件中添加 DataSourceTransactionManager

下面是Spring bean配置类:

@Configuration
@EnableTransactionManagement
public class AppConfig {
    @Bean
    public DataSource dataSource() {
        DriverManagerDataSource dataSource = new 
        DriverManagerDataSource(); 
        dataSource.setDriverClassName("org.postgresql.Driver");
        dataSource.setUrl("jdbc:postgresql:
        //localhost:5432/TestDB");
        dataSource.setUsername("test");
        dataSource.setPassword("test");
        return dataSource;
    }
 
    @Bean
    public JdbcTemplate jdbcTemplate() {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource());
        return jdbcTemplate;
    }
 
    @Bean
    public AccountDao accountDao(){
      AccountDaoImpl accountDao = new AccountDaoImpl();
      accountDao.setJdbcTemplate(jdbcTemplate());
      return accountDao;
    }
    
    @Bean
    public PlatformTransactionManager transactionManager() {
        DataSourceTransactionManager transactionManager = new                                             
        DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource());
        return transactionManager;
    }
 
}

在前面的代码中,我们创建了一个 dataSource bean。它用于创建 DataSource 对象。在这里,我们需要提供数据库配置属性,例如DriverClassNameUrlUsernamePassword。您可以根据本地设置更改这些值。

我们正在使用 JDBC 与数据库进行交互;这就是我们创建 org.springframework.jdbc.datasource.DataSourceTransactionManager 类型的 transactionManager bean 的原因。

@EnableTransactionManagement 注释用于在我们的 Spring 应用程序中打开事务支持。

下面是一个 AccountDao 实现类,用于在 Account 表中创建记录:

@Repository
public class AccountDaoImpl implements AccountDao {
  private static final Logger LOGGER =             
  Logger.getLogger(AccountDaoImpl.class);  
  private JdbcTemplate jdbcTemplate; 

  public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;
  }

  @Override
  @Transactional
  public void insertAccountWithJdbcTemplate(Account account) {
    String query = "INSERT INTO ACCOUNT (accountNumber,accountName) 
    VALUES (?,?)";    
    Object[] inputs = new Object[] { account.getAccountNumber(),                                 
    account.getAccountName() };
    jdbcTemplate.update(query, inputs);
    LOGGER.info("Inserted into Account Table Successfully");
    throw new RuntimeException("simulate Error condition");
  }
}

在前面的代码中,我们通过使用 @Transactional 注释来注释 insertAccountWithJdbcTemplate() 方法来提供声明式事务管理。 @Transactional 注释可以与方法一起使用,也可以在类级别使用。在前面的代码中,我在插入Account后抛出了RuntimeException异常,来检查产生异常后事务如何回滚。

以下是检查我们的事务管理实现的 main 类:

public class MainApp {
  
  private static final Logger LOGGER = Logger.getLogger(MainApp.class);

  public static void main(String[] args) throws SQLException {
    AnnotationConfigApplicationContext applicationContext = new 
    AnnotationConfigApplicationContext(
    AppConfig.class);

    AccountDao accountDao = 
    applicationContext.getBean(AccountDao.class); 
    Account account = new Account();
    account.setAccountNumber(202);
    account.setAccountName("xyz");
    accountDao.insertAccountWithJdbcTemplate(account); 
    applicationContext.close();
  }
}

现在,当我们运行前面的代码时,我们会得到以下输出:

INFO: Loaded JDBC driver: org.postgresql.Driver
2018-04-09 23:24:09 INFO AccountDaoImpl:36 - Inserted into Account Table Successfully
Exception in thread "main" java.lang.RuntimeException: simulate Error condition at com.packt.springhighperformance.ch5.bankingapp.dao.Impl.AccountDaoImpl.insertAccountWithJdbcTemplate(AccountDaoImpl.java:37)

在之前的日志中,数据成功插入到Account表中。但是,如果您检查Account 表,则不会在其中找到一行,这意味着该事务在RuntimeException 之后完全回滚。只有当方法成功返回时,Spring Framework 才会提交事务。如果出现异常,它会回滚整个事务。

Optimal isolation levels

正如我们在上一节中所了解的,事务的概念是用 ACID 描述的。事务隔离级别是一个不限于 Spring Framework 的概念,它适用于任何与数据库交互的应用程序。隔离级别定义了一个事务对某些数据存储库所做的更改如何影响其他并发事务,以及更改的数据如何以及何时可用于其他事务。在 Spring Framework 中,我们定义了事务的隔离级别以及 @Transaction 注释。

以下片段是我们如何在事务方法中定义 isolation 级别的示例:

@Autowired
private AccountDao accountDao;

@Transactional(isolation=Isolation.READ_UNCOMMITTED)
public void someTransactionalMethod(User user) {

  // Interact with accountDao

} 

在前面的代码中,我们定义了一个事务 isolation 级别为 READ_UNCOMMITTED 的方法。这意味着该方法中的事务是使用该 isolation 级别执行的。

让我们在以下部分详细了解每个 isolation 级别。

Read uncommitted

未提交读是最低的隔离级别。这个隔离级别定义了一个事务可能读取到其他事务还没有提交的数据,这意味着该数据与表或查询的其他部分不一致。这种隔离级别确保了最快的性能,因为数据是直接从表块中读取的,不需要进一步的处理、验证或其他验证;但它可能会导致一些问题,例如脏读。

我们来看下图:

读书笔记《hands-on-high-performance-with-spring-5》了解Spring数据库交互

在上图中,事务 A 写入数据;同时,Transaction B 在 Transaction A 提交之前读取相同的数据。后来,事务 A 由于某些异常决定回滚。现在,事务 B 中的数据不一致。这里,事务 B 运行在 READ_UNCOMMITTED 隔离级别,因此它能够在提交发生之前从 事务 A 读取数据。

请注意,READ_UNCOMMITTED 也可能产生不可重复读取和幻读等问题。当事务隔离选择为 READ_COMMITTED 时,会发生不可重复读取。

让我们详细了解一下 READ_COMMITTED 隔离级别。

Read committed

已提交读隔离级别定义一个事务不能读取其他事务未提交的数据。这意味着脏读不再是问题,但可能会出现其他问题。

我们来看下图:

读书笔记《hands-on-high-performance-with-spring-5》了解Spring数据库交互

在此示例中,事务 A 读取了一些数据。然后,事务 B 写入相同的数据并提交。稍后,事务 A 再次读取相同的数据并可能获得不同的值,因为 事务 B 已经对该数据进行了更改并提交。这是一个不可重复的读取。

请注意,READ_COMMITTED 也可能产生幻读等问题。当事务隔离选择为 REPEATABLE_READ 时,会发生幻读。

让我们详细看看 REPEATABLE_READ 隔离级别

Repeatable read

REPEATABLE_READ 隔离级别定义如果一个事务从数据库中多次读取一条记录,那么所有这些读取操作的结果必须相同。这种隔离有助于我们防止脏读和不可重复读等问题,但它可能会产生另一个问题。

我们看下图:

读书笔记《hands-on-high-performance-with-spring-5》了解Spring数据库交互

在此示例中,事务 A 读取一系列数据。同时,Transaction B 在 Transaction A 最初获取并提交的同一范围内插入新数据。稍后,Transaction A再次读取相同的范围,也会得到Transaction B刚刚插入的记录。这是幻读。在这里,事务 A多次从数据库中获取了一系列记录,并得到了不同的结果集。

Serializable

可序列化的隔离级别是所有隔离级别中最高且限制性更强的。它可以防止脏的、不可重复的读取和幻读。事务在所有级别(read、range 和 write 锁定)上执行,因此它们看起来好像是在序列化中执行的方法。在可串行化隔离中,我们将确保不会发生任何问题,但并发执行的事务会发生串行执行,这会降低应用程序的性能。

下面总结一下隔离级别和读现象的关系:

Levels Dirty reads Non-repeatable reads Phantom reads
READ_UNCOMMITTED Yes Yes Yes
READ_COMMITTED No Yes Yes
REPEATABLE_READ No No Yes
SERIALIZABLE No No No

如果未明确设置隔离级别,则事务使用默认隔离级别,根据相关数据库。

Optimal fetch size

应用程序和数据库服务器之间的网络流量是影响应用程序性能的关键因素之一。如果我们可以减少流量,它将帮助我们提高应用程序的性能。提取大小是一次从数据库中检索的行数。这取决于 JDBC 驱动程序。大多数 JDBC 驱动程序的默认获取大小是 10。在正常的 JDBC 编程中,如果您想要检索 1,000 行,那么您将需要在应用程序和数据库服务器之间进行 100 次网络往返来检索所有行。它会增加网络流量,也会影响性能。但是如果我们将 fetch size 设置为 100,那么网络往返次数将是 10。这将大大提高您的应用程序的性能。

许多框架,例如 Spring 或 Hibernate,为您提供了非常方便的 API 来执行此操作。如果我们不设置获取大小,那么它将采用默认值并提供较差的性能。

下面使用标准 JDBC 调用设置 FetchSize

PreparedStatement stmt = null;
ResultSet rs = null;

try 
{
  stmt = conn. prepareStatement("SELECT a, b, c FROM TABLE");
  stmt.setFetchSize(200);

  rs = stmt.executeQuery();
  while (rs.next()) {
    ...
  }
}

在前面的代码中,我们可以在每个 StatementPreparedStatement,甚至在 ResultSet 上设置 fetch size。默认情况下,ResultSet使用Statement的fetch size; StatementPreparedStatement 使用特定 JDBC 驱动程序的提取大小。

我们还可以在 Spring JdbcTemplate 中设置 FetchSize

JdbcTemplate jdbc = new JdbcTemplate(dataSource);
jdbc.setFetchSize(200);

设置提取大小时应考虑以下几点:

  • Make sure that your JDBC driver supports configuring the fetch size
  • The fetch size should not be hardcoded; keep it configurable because it depends on JVM heap memory size, which varies with different environments
  • If the fetch size is large, the application might encounter an out of memory issue

Optimal connection pooling configuration

JDBC 在访问数据库时使用连接池。 连接池类似于任何其他形式的对象池。连接池通常很少或不涉及代码修改,但它可以在应用程序性能方面提供显着优势。数据库连接在创建时执行各种任务,例如在数据库中初始化会话、执行用户身份验证和建立事务上下文。创建连接不是一个零成本的过程;因此,我们应该以最佳方式创建连接并减少对性能的影响。连接池允许重用物理连接并最大限度地减少创建和关闭会话中的昂贵操作。此外,维护许多空闲连接对于数据库管理系统来说是昂贵的,并且池可以优化空闲连接的使用或断开不再使用的连接。

为什么池化有用?这里有几个原因:

  • Frequently opening and closing connections can be expensive; it is better to cache and reuse.
  • We can limit the number of connections to the database. This will stop from accessing a connection until it is available. This is especially helpful in distributed environments.
  • We can use multiple connection pools for common operations, based on our requirements. We can design one connection pool for OLAP and another for OLAP, each with different configurations.

在本节中,我们将了解最佳连接池配置是什么,以帮助提高性能。

下面是一个简单的 PostgreSQL 连接池配置:

<Resource type="javax.sql.DataSource" name="jdbc/TestDB" factory="org.apache.tomcat.jdbc.pool.DataSourceFactory" driverClassName="org.postgresql.Driver" url="jdbc:postgresql://localhost:5432/TestDB" username="test" password="test" />

Sizing the connection pool

我们需要使用以下属性来调整连接池的大小:

  • initialSize: The initialSize attribute defines the number of connections that will be established when the connection pool is started.
  • maxActive: The maxActive attribute can be used to limit the maximum number of established connections to the database.
  • maxIdle: The maxIdeal attribute is used to maintain the maximum number of idle connections in the pool at all times.
  • minIdle: The minIdeal attribute is used to maintain the minimum number of idle connections in the pool at all times.
  • timeBetweenEvictionRunsMillis: The validation/cleaner thread runs every timeBetweenEvictionRunsMillis milliseconds. It's a background thread that can test idle, abandoned connections, and resize the pool while the pool is active. The thread is also responsible for connection leak detection. This value should not be set below 1 second.
  • minEvictableIdleTimeMillis: The minimum amount of time an object may sit idle in the pool before it is eligible for eviction.

Validate connections

设置此配置的好处是永远不会使用无效的连接,它有助于我们防止客户端出错。这种配置的缺点是性能损失很小,因为要验证连接,需要往返数据库以检查会话是否仍然处于活动状态。验证是通过向服务器发送一个小查询来完成的,但是这个查询的成本可能会更低。

验证连接的配置参数如下:

  • testOnBorrow: When the testOnBorrow attribute is defined as true, the connection object is validated before use. If it fails to validate, it will be dropped into the pool, and another connection object will be chosen. Here, we need to make sure that the validationQuery attribute is not null; otherwise, there is no effect on configuration.
  • validationInterval: The validationInterval attribute defines the frequency of the validating connection. It should not be more than 34 seconds. If we set a larger value, it will improve the application performance, but will also increase the chances of a stale connection being present in our application.
  • validationQuery: The SELECT 1 PostgreSQL query is used to validate connections from the pool before sending them to serve a request.

Connection leaks

以下配置设置可以帮助我们检测连接泄漏:

  • removeAbandoned: This flag should be true. It means that abandoned connections are removed if they exceed removeAbandonedTimeout.
  • removeAbandonedTimeout: This is in seconds. A connection is considered abandoned if it's running more than removeAbandonedTimeout. The value depends on the longest running query in your applications.

因此,为了获得最佳池大小,我们需要修改我们的配置以满足以下条件之一:

<Resource type="javax.sql.DataSource" name="jdbc/TestDB" factory="org.apache.tomcat.jdbc.pool.DataSourceFactory" driverClassName="org.postgresql.Driver" url="jdbc:postgresql://localhost:5432/TestDB" username="test" password="test" initialSize="10" maxActive="100" maxIdle="50" minIdle="10" suspectTimeout="60" timeBetweenEvictionRunsMillis="30000" minEvictableIdleTimeMillis="60000" testOnBorrow="true" validationInterval="34000" validationQuery="SELECT 1" removeAbandoned="true" removeAbandonedTimeout="60" logAbandoned="true" />

Tomcat JDBC connection pool versus HikariCP

有许多可用的开源连接池库,例如 C3P0、Apache Commons DBCP、BoneCP、Tomcat、Vibur 和 Hikari。但是使用哪一个取决于某些标准。以下标准将有助于决定使用哪个连接池。

Reliability

性能总是好的,但库的可靠性总是比性能更重要。我们不应该使用性能更高但不可靠的库。选择库时应考虑以下事项:

  • How widely it is used?
  • How is the code maintained?
  • The number of outstanding bugs open in the library.
  • It's community of developers and users.
  • How active is the library development?

Performance

性能也被认为是重要的标准。库的性能取决于它的配置方式以及测试它的环境。我们需要确保我们选择的库在我们自己的环境中具有良好的性能,并具有我们自己的配置。

Features

查看库提供的功能也很重要。我们应该检查所有参数,如果我们不提供参数,还要检查参数的默认值。此外,我们需要查看一些连接策略,例如自动提交、隔离级别和语句缓存。

Ease of use

重要的是我们可以多么容易地使用库来配置连接池。此外,它应该有据可查并经常更新。

下表列出了 Tomcat JDBC 连接池和 HikariCP 之间的区别:

Tomcat JDBC HikariCP
Does not test connections on getConnection() by default. Tests connections on getConnection().
Does not close abandoned open statements. Tracks and closes abandoned connections.
Does not by default reset auto-commit and transaction levels for connections in the pool; users must configure custom interceptors to do this. Resets auto-commit, transaction isolation, and read-only status.
Pool prepared statement properties are not used. We can use pool prepared statement properties.
Does not, by default, execute a rollback() on connections returned to the pool. By default, executes a rollback() on connections returned to the pool.

Database interaction best practices

本节列出了开发人员在开发任何应用程序时应注意的一些基本规则。不遵守规则将导致应用程序性能不佳。

Using Statement versus PreparedStatement versus CallableStatement

StatementPreparedStatementCallableStatement 接口之间进行选择;这取决于您计划如何使用该界面。 Statement 接口针对 SQL 语句的单次执行进行了优化,而 PreparedStatement 对象针对将执行多次的 SQL 语句进行了优化,而 CallableStatement< /kbd> 通常首选用于执行存储过程:

  • Statement: The PreparedStatement is used to execute normal SQL queries. It is preferred when a particular SQL query is to be executed only once. The performance of this interface is very low.
  • PreparedStatement: The PreparedStatement interface is used to execute parametrized or dynamic SQL queries. It is preferred when a particular query is to be executed multiple times. The performance of this interface is better than the Statement interface (when used for multiple executions of the same query).
  • CallableStatement: The CallableStatement interface is preferred when the stored procedures are to be executed. The performance of this interface is high.

Using Batch instead of PreparedStatement

将大量数据插入数据库通常是通过准备 INSERT 语句并多次执行该语句来完成的。这会增加 JDBC 调用的数量并影响性能。为了减少 JDBC 调用次数并提高性能,您可以使用 PreparedStatement 对象的 addBatch 方法一次向数据库发送多个查询。

让我们看下面的例子:

PreparedStatement ps = conn.prepareStatement(
"INSERT INTO ACCOUNT VALUES (?, ?)");
for (n = 0; n < 100; n++) {
    ps.setInt(accountNumber[n]);
    ps.setString(accountName[n]);
    ps.executeUpdate();
}

在前面的示例中,PreparedStatement 用于多次执行 INSERT 语句。为了执行前面的 INSERT 操作,需要 101 次网络往返:一次用于准备语句,其余 100 次用于执行 INSERT SQL 语句。因此,插入和更新大量数据实际上会增加网络流量,并因此影响性能。

让我们看看如何使用 Batch 减少网络流量并提高性能:

PreparedStatement ps = conn.prepareStatement(
"INSERT INTO ACCOUNT VALUES (?, ?)");
for (n = 0; n < 100; n++) {
    ps.setInt(accountNumber[n]);
    ps.setString(accountName[n]);
    ps.addBatch();
}
ps.executeBatch();

在前面的示例中,我使用了 addBatch() 方法。它整合了所有 100 个 INSERT SQL,并仅通过两次网络往返执行整个操作:一次用于准备语句,另一次用于执行一批整合的 SQL。

Minimizing the use of database metadata methods

尽管几乎没有 JDBC 应用程序可以在没有数据库元数据方法的情况下编写,但与其他 JDBC 方法相比,数据库元数据方法速度较慢。当我们使用元数据方法时,SELECT 语句对数据库进行两次往返:一次用于元数据,第二次用于数据。这是非常昂贵的性能。我们可以通过最小化元数据方法的使用来提高性能。

An application should cache all metadata, as they will not change, so multiple executions are not needed.

Using get methods effectively

JDBC 提供了不同类型的方法来从结果集中检索数据,例如 getIntgetStringgetObjectgetObject 方法是通用方法,您可以将它用于所有数据类型。但是,我们应该始终避免使用 getObject,因为它的性能比其他的更差。当我们使用 getObject 获取数据时,JDBC 驱动程序必须执行额外的处理来确定要获取的值的类型并生成适当的映射。我们应该始终使用数据类型的特定方法;这提供了比使用像 getObject 这样的通用方法更好的性能。

我们还可以通过使用列号而不是列名来提高性能;例如,getInt(1)getString(2)getLong(3)。如果我们使用列名而不是列号(例如,getString("accountName")),那么数据库驱动程序首先将列名转换为大写(如果需要),然后比较 < kbd>accountName 包含结果集中可用的所有列。此处理时间直接影响性能。我们应该使用列号来减少处理时间。

When to avoid connection pooling

在某些类型的应用程序上使用连接池肯定会降低性能。如果您的应用程序具有以下任何特征,则它不适合连接池:

  • If an application restarts many times daily, we should avoid connection pooling because, based on the configuration of the connection pool, it may be populated with connections each time the application is started, which would cause a performance penalty up front.
  • If you have single-user applications, such as applications only generating reports (in this type of application, the user only uses the application three to four times daily, for generating reports), then avoid connection pooling. The memory utilization for establishing a database connection three to four times daily is low, compared to the database connection associated with a connection pool. In such cases, configuring a connection pool degrades the overall performance of the application.
  • If an application runs only batch jobs, there is no advantage to using connection pooling. Normally, a batch job is run at the end of the day or month or year, during the off hours, when performance is not as much of a concern.

Choose commit mode carefully

当我们提交事务时,数据库服务器必须将事务所做的更改写入数据库。这涉及昂贵的磁盘输入/输出,并且驱动程序需要通过套接字发送请求。

在大多数标准 API 中,默认提交模式是自动提交。在自动提交模式下,数据库对每个 SQL 语句执行一次提交,例如 INSERTUPDATEDELETE SELECT 语句。数据库驱动程序在每个 SQL 语句操作之后向数据库发送一个提交请求。此请求需要一次网络往返。即使 SQL 语句执行未对数据库进行任何更改,也会发生到数据库的往返。例如,即使执行 SELECT 语句,驱动程序也会进行网络往返。自动提交模式通常会影响性能,因为提交每个操作需要大量的磁盘输入/输出。

因此,我们将自动提交模式设置为关闭以提高应用程序的性能,但也不建议让事务处于活动状态。保持事务处于活动状态可以通过在行上保持锁定的时间超过必要的时间并阻止其他用户访问这些行来降低吞吐量。以允许最大并发的时间间隔提交事务。

对于某些应用程序,也不建议将自动提交模式设置为关闭并进行手动提交。例如,考虑一个允许用户将钱从一个帐户转移到另一个帐户的银行应用程序。为了保护该工作的数据完整性,需要在两个帐户都更新为新金额后提交交易。

Summary

在本章中,我们对 Spring JDBC 模块有了一个清晰的认识,并了解了 Spring JDBC 如何帮助删除我们在核心 JDBC 中使用的样板代码。我们还学习了如何设计我们的数据库以获得最佳性能。我们在 Spring 中看到了事务管理的各种好处。我们了解了各种配置技术,例如隔离级别、获取大小和连接池,它们可以提高应用程序的性能。最后,我们研究了数据库交互的最佳实践,这可以帮助我们提高应用程序的性能。

在下一章中,我们将看到使用 ORM 框架(例如 Hibernate)的数据库交互,我们将了解 Spring 中的 Hibernate 配置、常见的 Hibernate 陷阱和 Hibernate 性能调优。