Spring -- HikariCP

JMH Benchmarks

详细见:HikariCP

One Connection Cycle is defined as single DataSource.getConnection()/Connection.close().

One Statement Cycle is defined as single Connection.prepareStatement(), Statement.execute(), Statement.close().

优化

详细见:Down-the-Rabbit-Hole

字节码级别优化

  1. In order to make HikariCP as fast as it is, we went down to bytecode-level engineering, and beyond.
  2. We pulled out every trick we know to help the JIT help you.
  3. We studied the bytecode output of the compiler, and even the assembly output of the JIT to limit key routines to less than the JIT inline-threshold.
  4. We flattened inheritance hierarchies, shadowed member variables, eliminated casts.

大量小优化

FastList

ArrayList was replaced with a custom class FastList which eliminates range checking and performs removal scans from tail to head.

range check
  1. One non-trivial (performance-wise) optimization was eliminating the use of an ArrayList instance in the ConnectionProxy used to track open Statement instances.
  2. When a Statement is closed, it must be removed from this collection
  3. When a Connection is closed, it must iterate the collection and close any open Statement instances, and finally must clear the collection.
  4. The Java ArrayList, wisely for general purpose use, performs a range check upon every get(int index) call.
  5. However, because we can provide guarantees about our ranges, this check is merely overhead.
remove
  1. Additionally, the remove(Object) implementation performs a scan from head to tail.
  2. However common patterns in JDBC programming are to close Statements immediately after use, or in reverse order of opening.
  3. For these cases, a scan that starts at the tail will perform better.

ConcurrentBag

  1. HikariCP contains a custom lock-free collection called a ConcurrentBag.
  2. The idea was borrowed from the C# .NET ConcurrentBag class, but the internal implementation quite different.
  3. Provides
    • A lock-free design
    • ThreadLocal caching
    • Queue-stealing
    • Direct hand-off optimizations
  4. Resulting
    • high degree of concurrency
    • extremely low latency
    • minimized occurrences of false-sharing

invokevirtual -> invokestatic

invokevirtual

In order to generate proxies for Connection, Statement, and ResultSet instances, HikariCP was initially using a singleton factory, held in the case of ConnectionProxy in a static field (PROXY_FACTORY).

There was a dozen or so methods resembling the following:

1
2
3
4
public final PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException
{
return PROXY_FACTORY.getProxyPreparedStatement(this, delegate.prepareStatement(sql, columnNames));
}

Using the original singleton factory, the generated bytecode looked like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
public final java.sql.PreparedStatement prepareStatement(java.lang.String, java.lang.String[]) throws java.sql.SQLException;
flags: ACC_PRIVATE, ACC_FINAL
Code:
stack=5, locals=3, args_size=3
0: getstatic #59 // Field PROXY_FACTORY:Lcom/zaxxer/hikari/proxy/ProxyFactory;
3: aload_0
4: aload_0
5: getfield #3 // Field delegate:Ljava/sql/Connection;
8: aload_1
9: aload_2
10: invokeinterface #74, 3 // InterfaceMethod java/sql/Connection.prepareStatement:(Ljava/lang/String;[Ljava/lang/String;)Ljava/sql/PreparedStatement;
15: invokevirtual #69 // Method com/zaxxer/hikari/proxy/ProxyFactory.getProxyPreparedStatement:(Lcom/zaxxer/hikari/proxy/ConnectionProxy;Ljava/sql/PreparedStatement;)Ljava/sql/PreparedStatement;
18: return

You can see that first there is a getstatic call to get the value of the static field PROXY_FACTORY, as well as (lastly) the invokevirtual call to getProxyPreparedStatement() on the ProxyFactory instance.

invokestatic

We eliminated the singleton factory (which was generated by Javassist) and replaced it with a final class having static methods (whose bodies are generated by Javassist).

The Java code became:

1
2
3
4
5
6
7
8
9
10
11
12
// com.zaxxer.hikari.pool.ProxyConnection
public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException
{
return ProxyFactory.getProxyPreparedStatement(this, trackStatement(delegate.prepareStatement(sql, columnNames)));
}

// com.zaxxer.hikari.pool.ProxyFactory
static PreparedStatement getProxyPreparedStatement(final ProxyConnection connection, final PreparedStatement statement)
{
// Body is replaced (injected) by JavassistProxyFactory
throw new IllegalStateException("You need to run the CLI build and you need target/classes in your classpath to run.");
}

Where getProxyPreparedStatement() is a static method defined in the ProxyFactory class. The resulting bytecode is:

1
2
3
4
5
6
7
8
9
10
11
12
private final java.sql.PreparedStatement prepareStatement(java.lang.String, java.lang.String[]) throws java.sql.SQLException;
flags: ACC_PRIVATE, ACC_FINAL
Code:
stack=4, locals=3, args_size=3
0: aload_0
1: aload_0
2: getfield #3 // Field delegate:Ljava/sql/Connection;
5: aload_1
6: aload_2
7: invokeinterface #72, 3 // InterfaceMethod java/sql/Connection.prepareStatement:(Ljava/lang/String;[Ljava/lang/String;)Ljava/sql/PreparedStatement;
12: invokestatic #67 // Method com/zaxxer/hikari/proxy/ProxyFactory.getProxyPreparedStatement:(Lcom/zaxxer/hikari/proxy/ConnectionProxy;Ljava/sql/PreparedStatement;)Ljava/sql/PreparedStatement;
15: areturn

There are three things of note here:

  • The getstatic call is gone.
  • The invokevirtual call is replaced with a invokestatic call that is more easily optimized by the JVM
  • Lastly, possibly not noticed at first glance is that the stack size is reduced from 5 elements to 4 elements.
    • This is because in the case of invokevirtual there is an implicit passing of the instance of ProxyFactory on the stack (i.e this), and there is an additional (unseen) pop of that value from the stack when getProxyPreparedStatement() was called.

常用配置

1
2
3
4
5
6
7
8
9
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.pool-name=MyHikariCp
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.connection-test-query=SELECT 1
1
2
3
4
5
6
2019-08-16 08:10:49.897  INFO 21357 --- [           main] com.zaxxer.hikari.HikariDataSource       : MyHikariCp - Starting...
2019-08-16 08:10:50.236 INFO 21357 --- [ main] com.zaxxer.hikari.HikariDataSource : MyHikariCp - Start completed.
2019-08-16 08:10:50.641 INFO 21357 --- [ main] me.zhongmingmao.hikaricp.PersonDao : dataSource=HikariDataSource (MyHikariCp)
2019-08-16 08:10:50.642 INFO 21357 --- [ main] me.zhongmingmao.hikaricp.PersonDao : connection=HikariProxyConnection@1545077099 wrapping conn0: url=jdbc:h2:mem:testdb user=SA
2019-08-16 08:10:50.678 INFO 21357 --- [ Thread-8] com.zaxxer.hikari.HikariDataSource : MyHikariCp - Shutdown initiated...
2019-08-16 08:10:50.681 INFO 21357 --- [ Thread-8] com.zaxxer.hikari.HikariDataSource : MyHikariCp - Shutdown completed.

参考链接

  1. HikariCP
  2. Down-the-Rabbit-Hole
  3. Springboot 2.0选择HikariCP
0%